Having a centralized repository (such as Cloud/S3) is crucial for data backup and recovery purposes. It also provides ease when needed to restore the data over different machines/instances. MinIO is one such S3-compatible storage solution that can be deployed anywhere (on-premise, private cloud, etc.), unlike GCP/AWS S3 buckets, which need to be deployed in their respective environments.
In this blog post, we will cover topics like taking backups on MinIO and how we can restore the data over both “local machine” and “k8s/percona postgres operator” based environments. For quick testing in a local setup, MinIO comes in very handy and can be configured with minimal steps.
Let’s discuss this in detail.
Setting up MinIO storage
We are deploying MinIO in a Docker container, which runs over port “9000”. Another port, “9001,” is used for Web console/UI access.
| 1 2 3 4 5 6 7 8 | shell> docker run -d    --name minio    -p 9000:9000    -p 9001:9001    -e MINIO_ROOT_USER=minioadmin    -e MINIO_ROOT_PASSWORD=minioadmin    -v ~/minio-certs:/root/.minio/certs    quay.io/minio/minio server /data --console-address ":9001" | 
| 1 2 3 | shell> docker ps CONTAINER ID   IMAGE                            COMMAND                  CREATED       STATUS       PORTS                                                             NAMES 6e74aede6572   quay.io/minio/minio              "/usr/bin/docker-ent…"   2 days ago    Up 7 hours   0.0.0.0:9000-9001->9000-9001/tcp, [::]:9000-9001->9000-9001/tcp   minio | 
The Web console is accessed at the address below.
| 1 | https://127.0.0.1:9001/ | 
Here, from the UI itself, we have created a bucket named “ajtest”, which we will use to store/retrieve backups.

Setting up pgBackRest repository in MinIO
| 1 | shell> cat /opt/homebrew/etc/pgbackrest/pgbackrest.conf | 
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | [global] repo1-type=s3 repo1-s3-bucket=ajtest repo1-s3-endpoint=127.0.0.1 repo1-storage-port=9000 repo1-s3-region=us-east-1 repo1-s3-key=minioadmin repo1-s3-key-secret=minioadmin  repo1-s3-uri-style=path repo1-storage-verify-tls=n  [pg0app] pg1-path=/opt/homebrew/var/postgresql@17 pg1-port=5432 | 
Note: The MinIO user and password (“minioadmin”) will be used as Key/Secrets. The “repo1-s3-region” can be anything, as MinIO is set up locally.
1) Creating the stanza
| 1 2 3 4 | shell> pgbackrest --config=/opt/homebrew/etc/pgbackrest/pgbackrest.conf    --stanza=pg0app    --log-level-console=info    stanza-create | 
Output:
| 1 2 3 4 | 2025-10-25 13:01:09.525 P00   INFO: stanza-create command begin 2.56.0: --config=/opt/homebrew/etc/pgbackrest/pgbackrest.conf --exec-id=6328-d2b26a07 --log-level-console=info --pg1-path=/opt/homebrew/var/postgresql@17 --pg1-port=5432 --repo1-s3-bucket=ajtest --repo1-s3-endpoint=127.0.0.1 --repo1-s3-key=<redacted> --repo1-s3-key-secret=<redacted> --repo1-s3-region=us-east-1 --repo1-s3-uri-style=path --repo1-storage-port=9000 --no-repo1-storage-verify-tls --repo1-type=s3 --stanza=pg0app 2025-10-25 13:01:09.572 P00   INFO: stanza-create for stanza 'pg0app' on repo1 2025-10-25 13:01:09.580 P00   INFO: stanza 'pg0app' already exists on repo1 and is valid 2025-10-25 13:01:09.580 P00   INFO: stanza-create command end: completed successfully (61ms) | 
2) Performing pgBackRest health check and connectivity.
| 1 | shell> pgbackrest --config=/opt/homebrew/etc/pgbackrest/pgbackrest.conf check --stanza=pg0app  --log-level-console=info | 
Output:
| 1 2 3 4 5 6 | ... 2025-10-25 13:05:28.451 P00   INFO: check repo1 configuration (primary) 2025-10-25 13:05:28.765 P00   INFO: check repo1 archive for WAL (primary) 2025-10-25 13:05:29.082 P00   INFO: WAL segment 0000000100000001000000B7 successfully archived to '/var/lib/pgbackrest/archive/pg0app/17-1/0000000100000001/0000000100000001000000B7-fa737ca9ea6b61cda2a67d73608c50e775cee5a8.gz' on repo1 2025-10-25 13:05:29.082 P00   INFO: check command end: completed successfully (655ms) | 
3) Taking the backups.
| 1 | shell> pgbackrest --config=/opt/homebrew/etc/pgbackrest/pgbackrest.conf backup --stanza=pg0app --type=full --log-level-console=info | 
Output:
| 1 2 3 4 5 6 7 8 9 10 | 2025-10-25 13:12:29.257 P00   INFO: backup command begin 2.56.0: --config=/opt/homebrew/etc/pgbackrest/pgbackrest.conf --exec-id=7459-777134ce --log-level-console=info --pg1-path=/opt/homebrew/var/postgresql@17 --pg1-port=5432 --repo1-s3-bucket=ajtest --repo1-s3-endpoint=127.0.0.1 --repo1-s3-key=<redacted> --repo1-s3-key-secret=<redacted> --repo1-s3-region=us-east-1 --repo1-s3-uri-style=path --repo1-storage-port=9000 --no-repo1-storage-verify-tls --repo1-type=s3 --stanza=pg0app --type=full 2025-10-25 13:12:29.257 P00   WARN: option 'repo1-retention-full' is not set for 'repo1-retention-full-type=count', the repository may run out of space                                     HINT: to retain full backups indefinitely (without warning), set option 'repo1-retention-full' to the maximum. 2025-10-25 13:12:29.301 P00   INFO: execute non-exclusive backup start: backup begins after the next regular checkpoint completes 2025-10-25 13:12:29.674 P00   INFO: backup start archive = 0000000100000001000000B9, lsn = 1/B9000060 ... 2025-10-25 13:12:43.706 P00   INFO: backup command end: completed successfully (14455ms) 2025-10-25 13:12:43.706 P00   INFO: expire command begin 2.56.0: --config=/opt/homebrew/etc/pgbackrest/pgbackrest.conf --exec-id=7459-777134ce --log-level-console=info --repo1-s3-bucket=ajtest --repo1-s3-endpoint=127.0.0.1 --repo1-s3-key=<redacted> --repo1-s3-key-secret=<redacted> --repo1-s3-region=us-east-1 --repo1-s3-uri-style=path --repo1-storage-port=9000 --no-repo1-storage-verify-tls --repo1-type=s3 --stanza=pg0app 2025-10-25 13:12:43.708 P00   INFO: option 'repo1-retention-archive' is not set - archive logs will not be expired 2025-10-25 13:12:43.708 P00   INFO: expire command end: completed successfully (2ms) | 
The backup will be listed in the minIO web console URL.

Also, we can list the backup using the mc-minio tool.
| 1 2 | shell> mc-minio alias set localminio https://127.0.0.1:9000 minioadmin minioadmin --insecure Added `localminio` successfully. | 
| 1 2 3 4 5 6 | shell> mc-minio --insecure ls localminio/ajtest/var/lib/pgbackrest/backup/pg0app [2025-10-25 13:12:43 IST] 1.7KiB STANDARD backup.info [2025-10-25 13:12:43 IST] 1.7KiB STANDARD backup.info.copy [2025-10-25 13:38:23 IST]     0B 20251023-230455F/ [2025-10-25 13:38:23 IST]     0B 20251025-131229F/ [2025-10-25 13:38:23 IST]     0B backup.history/ | 
Similarly, we can also set up pgBackRest with MinIO in the K8s/Percona postgres operator environment.
Setting up pgBackRest repository with MinIO in Percona Operator for PostgreSQL
1) MinIO credentials setup.
| 1 2 3 4 5 | shell> cat <<EOF | base64 -b 0 [global] repo1-s3-key=minioadmin repo1-s3-key-secret=minioadmin EOF | 
Output:
| 1 | W2dsb2JhbF0KcmVwbzEtczMta2V5PW1pbmlvYWRtaW4KcmVwbzEtczMta2V5LXNlY3JldD1taW5pb2FkbWluCg== | 
2) Creating the secret “cluster1-pgbackrest-secrets.yaml”.
| 1 2 3 4 5 6 7 | apiVersion: v1 kind: Secret metadata:   name: cluster1-pgbackrest-secrets type: Opaque data:   s3.conf: W2dsb2JhbF0KcmVwbzEtczMta2V5PW1pbmlvCnJlcG8xLXMzLWtleS1zZWNyZXQ9bWluaW8xMjMK | 
| 1 | shell> kubectl apply -f cluster1-pgbackrest-secrets.yaml -n postgres-operator | 
3) Adding the bucket, endpoint, and repo details in the “cr.yaml” file.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | ... backups:   pgbackrest:     image: docker.io/percona/percona-pgbackrest:2.55.0     configuration:       - secret:           name: cluster1-pgbackrest-secrets     global:       repo1-path: /pgbackrest/postgres-operator/cluster1/repo1       repo1-s3-uri-style: path       repo1-s3-verify-tls: 'n'     manual:       repoName: repo1       options:         - --type=full     repos:       - name: repo1         s3:           bucket: "ajtest"           endpoint: "https://minio:9000"           region: "us-east-1" ... | 
| 1 | shell> Kubectl apply -f cr.yaml -n postgres-operator | 
4) Inside the pgBackRest repo pod, we can check the status and perform the backup and other related tasks.
| 1 2 | shell> kubectl exec -it cluster1-repo-host-0  -n postgres-operator -- sh bash> pgbackrest check --stanza=db  --log-level-console=info | 
| 1 2 3 4 5 6 7 | bash> sh-4.4$ pgbackrest backup --type=full --stanza=db  --log-level-console=info ... 2025-10-30 17:34:54.699 P00   INFO: backup command end: completed successfully (17516ms) 2025-10-30 17:34:54.699 P00   INFO: expire command begin 2.51: --exec-id=211787-6dd3f744 --log-level-console=info --log-path=/pgbackrest/repo1/log --repo1-path=/pgbackrest/repo1 --stanza=db 2025-10-30 17:34:54.708 P00   INFO: option 'repo1-retention-archive' is not set - archive logs will not be expired 2025-10-30 17:34:54.709 P00   INFO: expire command end: completed successfully (10ms) | 
So, till here we performed the pgBackRest backup in MinIO under both local and Percona Operator setups. Next, we will perform restoration similarly in both local and K8s/operator environments.
Restoring pgBackRest in the local environment
1) Stopping the existing database service and cleaning the data directory.
| 1 2 | shell> brew services stop postgresql@17 shell> rm -rf /opt/homebrew/var/postgresql@17 | 
2) Restoring the backup.
| 1 | shell> pgbackrest --config=/opt/homebrew/etc/pgbackrest/pgbackrest.conf restore --stanza=pg0app --log-level-console=info | 
Output:
| 1 2 3 4 5 6 7 | 2025-10-25 16:38:49.446 P00   INFO: restore command begin 2.56.0: --config=/opt/homebrew/etc/pgbackrest/pgbackrest.conf --exec-id=17439-a3384a6b --log-level-console=info --pg1-path=/opt/homebrew/var/postgresql@17 --repo1-s3-bucket=ajtest --repo1-s3-endpoint=127.0.0.1 --repo1-s3-key=<redacted> --repo1-s3-key-secret=<redacted> --repo1-s3-region=us-east-1 --repo1-s3-uri-style=path --repo1-storage-port=9000 --no-repo1-storage-verify-tls --repo1-type=s3 --stanza=pg0app 2025-10-25 16:38:49.476 P00   INFO: repo1: restore backup set 20251025-131229F, recovery will start at 2025-10-25 13:12:29 2025-10-25 16:38:49.476 P00   WARN: unknown group 'admin' in backup manifest mapped to current group 2025-10-25 16:38:51.319 P00   INFO: write updated /opt/homebrew/var/postgresql@17/postgresql.auto.conf 2025-10-25 16:38:51.322 P00   INFO: restore global/pg_control (performed last to ensure aborted restores cannot be started) 2025-10-25 16:38:51.322 P00   INFO: restore size = 372MB, file total = 2298 2025-10-25 16:38:51.322 P00   INFO: restore command end: completed successfully (1886ms) | 
3) Starting the database service again.
| 1 | shell> brew services start postgresql@17 | 
Note: The required WAL segments for consistent recovery will be fetched from the pgBackRest repository, which is kept in MinIO.
| 1 | restore_command = '/opt/homebrew/bin/pgbackrest --config=/opt/homebrew/etc/pgbackrest/pgbackrest.conf --stanza=pg0app archive-get %f "%p"' | 
Please also ensure the WAL archiving is in place after restoring the instance.
| 1 | archive_command = '/opt/homebrew/bin/pgbackrest --config=/opt/homebrew/etc/pgbackrest/pgbackrest.conf --stanza=pg0app --pg1-path=/opt/homebrew/var/postgresql@17 archive-push %p' | 
Restoring pgBackRest in K8s/Percona postgres operator environment
1) Create the restore yaml file and apply it.
| 1 | shell> cat restore.yaml  | 
| 1 2 3 4 5 6 7 | piVersion: pgv2.percona.com/v2 kind: PerconaPGRestore metadata:   name: restore1 spec:   pgCluster: cluster1   repoName: repo1 | 
| 1 2 | shell> kubectl apply -f restore.yaml -n postgres-operator perconapgrestore.pgv2.percona.com/restore1 created | 
2) The restore job ran fine as demonstrated below.
| 1 2 3 | shell> kubectl get pg-restore -n postgres-operator NAME       CLUSTER    STATUS      COMPLETED   AGE restore1   cluster1   Succeeded   76s         93s | 
For more details about the restore job, refer to the command below.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | shell> kubectl describe pg-restore restore1 -n postgres-operator Name:         restore1 Namespace:    postgres-operator Labels:       <none> Annotations:  <none> API Version:  pgv2.percona.com/v2 Kind:         PerconaPGRestore Metadata:   Creation Timestamp:  2025-10-26T05:28:22Z   Finalizers:     internal.percona.com/delete-restore   Generation:        1   Resource Version:  2097013   UID:               43ee4fb5-14c0-4776-92c1-7d52a67b8934 Spec:   Pg Cluster:  cluster1   Repo Name:   repo1 Status:   Completed:  2025-10-26T05:28:39Z   State:      Succeeded Events:       <none> | 
3) The postgres pod will be reinitialized with the new restoration and ready to be used.
| 1 2 3 4 | shell> kubectl get pods -n postgres-operator ... cluster1-instance1-g68c-0                      4/4     Running     0             10h ... | 
We can verify the data by simplifying the login inside the respective pod and service.
| 1 2 3 | shell> kubectl exec -it cluster1-instance1-g68c-0 -n postgres-operator -- sh bash> psql ... | 
Note: The restore described above for the PostgreSQL operator will automatically overwrite the existing cluster data. Therefore, we need to be extra cautious, or, if needed, take existing backups before performing any restorations.
Conclusion
MinIO buckets provide an excellent way to test and manage S3-style repositories without having any vendor-specific (AWS/GCP) setup. It supports both traditional and Kubernetes/operator-based environments efficiently. Combined with pgBackRest, PostgreSQL backups/recovery are made quite easy and accessible, almost instant when having a centralized backup repository.
 
 

 
 
						 
						 
						 
						