August 26, 2026
High-Availability Databases on Docker Swarm: PostgreSQL, MySQL and Redis (2026)
Databases run well on Docker Swarm when the data is on a pinned local volume and failover is the database’s own: streaming replicas and Patroni for PostgreSQL, Galera for MySQL, Sentinel for Redis, plus the backups that make it real.

"Swarm is not for databases" is repeated often enough to sound like a rule. It is not one. A database on Swarm is a database on a specific node with a specific disk, and Swarm's job is to keep it there, restart it, put a replica somewhere else, and route the application to whichever one is primary. Get the storage decision right and the rest is the same replication engineering you would do on bare metal. This guide covers the patterns that hold up for PostgreSQL, MySQL and Redis.
The one rule: local volumes, pinned nodes
A named volume in Swarm is created on the node where the task first runs and does not follow the task anywhere. That is a feature for a database: a primary's data directory belongs on one machine's fast local disk, and the service that owns it must be pinned to that machine with a placement constraint.
services:
postgres-primary:
image: postgres:16
deploy:
replicas: 1
placement:
constraints:
- node.labels.db-primary == true
resources:
limits: { cpus: '2.0', memory: 4G }
reservations: { cpus: '1.0', memory: 2G }
volumes:
- pg_primary:/var/lib/postgresql/data
networks:
- db-net
secrets:
- postgres_password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
volumes:
pg_primary:
networks:
db-net:
driver: overlay
attachable: true
secrets:
postgres_password:
external: true
Label the node once, docker node update --label-add db-primary=true <node>, and the scheduler will never put the primary anywhere else. If that node dies, the primary is down until you promote a replica, which is what the replication design below is for. What you must not do is put the data directory on NFS or a basic distributed filesystem to make the task "movable": database engines assume local POSIX semantics and write ordering, and the failure mode is silent corruption.
| Storage | Use it for | Risk |
|---|---|---|
| Local volume, pinned node | Every primary and every replica | Low |
| Ceph RBD or similar block | Replicas, when you already run it | Medium |
| NFS or SMB | Backups and non-critical data only | High |
Everything else in this guide follows from that rule: separate the database on its own overlay network, give it reservations so a noisy neighbour cannot starve it, and put the failover logic in the database's own replication rather than in Swarm rescheduling.
PostgreSQL
[!NOTE] The environment variables in the stack files below are the ones the Bitnami and Percona images document. Image tags change what they accept, so read the README for the tag you deploy before you trust a file from any blog, this one included, with production data.
Primary plus streaming replicas is the pattern most teams need. The primary runs pinned as above; each replica runs on another labelled node with its own local volume and streams the WAL from the primary. The official postgres image leaves replication setup to you; the Bitnami PostgreSQL image wires it from environment variables, which is why it is the common choice on Swarm:
services:
pg-primary:
image: bitnami/postgresql:16
environment:
POSTGRESQL_REPLICATION_MODE: master
POSTGRESQL_REPLICATION_USER: repl
POSTGRESQL_REPLICATION_PASSWORD_FILE: /run/secrets/pg_repl_password
POSTGRESQL_PASSWORD_FILE: /run/secrets/pg_password
deploy:
placement:
constraints: [node.labels.db-primary == true]
volumes: [pg_primary:/bitnami/postgresql]
secrets: [pg_password, pg_repl_password]
networks: [db-net]
pg-replica:
image: bitnami/postgresql:16
environment:
POSTGRESQL_REPLICATION_MODE: slave
POSTGRESQL_MASTER_HOST: pg-primary
POSTGRESQL_REPLICATION_USER: repl
POSTGRESQL_REPLICATION_PASSWORD_FILE: /run/secrets/pg_repl_password
POSTGRESQL_PASSWORD_FILE: /run/secrets/pg_password
deploy:
placement:
constraints: [node.labels.db-replica == true]
volumes: [pg_replica:/bitnami/postgresql]
secrets: [pg_password, pg_repl_password]
networks: [db-net]
Swarm's DNS resolves pg-primary on the overlay network, so the replica finds it by service name. Reads can go to the replica; writes go to the primary; failover is a manual promotion, which for many teams is the right trade because it is a decision a person should make.
Automatic failover means Patroni: a small agent beside each PostgreSQL that uses a distributed store (etcd is the usual one) to elect a leader, promote a replica when the leader disappears, and expose which node is primary. It is the standard answer to "Postgres HA without Kubernetes" and it runs on Swarm as three pinned Patroni services plus an etcd cluster. Put PgBouncer or HAProxy in front so the application connects to one name and follows the leader. The moving parts are real, so adopt Patroni when you have measured that manual promotion is too slow for you, not before.
For a single-node database with backups, which is where most homelabs and many small teams should start, the postgres chart in the SwarmCLI charts repository deploys a pinned, secret-authenticated instance in one command.
MySQL and MariaDB
Percona XtraDB Cluster (Galera replication) gives you a multi-primary cluster where every node accepts writes and the cluster survives the loss of a node while it keeps quorum, which needs three members:
services:
pxc1:
image: percona/percona-xtradb-cluster:8.0
environment:
CLUSTER_NAME: swarm-pxc
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root
XTRABACKUP_PASSWORD_FILE: /run/secrets/xtrabackup
deploy:
placement:
constraints: [node.labels.pxc == 1]
volumes: [pxc1:/var/lib/mysql]
networks: [db-net]
secrets: [mysql_root, xtrabackup]
pxc2:
image: percona/percona-xtradb-cluster:8.0
environment:
CLUSTER_NAME: swarm-pxc
CLUSTER_JOIN: pxc1
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root
XTRABACKUP_PASSWORD_FILE: /run/secrets/xtrabackup
deploy:
placement:
constraints: [node.labels.pxc == 2]
volumes: [pxc2:/var/lib/mysql]
networks: [db-net]
secrets: [mysql_root, xtrabackup]
# pxc3 identical to pxc2, on node.labels.pxc == 3
One service per member rather than replicas: 3, because each member needs its own pinned volume and its own name. Bootstrap the first member alone, then start the others with CLUSTER_JOIN pointing at it. Put ProxySQL or HAProxy in front for a single application endpoint. If Galera is more than you need, MariaDB or MySQL with a pinned primary and a read replica follows the PostgreSQL pattern above, and the mariadb chart covers the single-node case.
Redis
Redis has two answers, and they solve different problems.
Sentinel gives one primary, replicas, and automatic failover. Run the Redis instances as pinned services and the sentinels as a separate service whose configuration is a Swarm config, since Sentinel wants a writable file it can update:
services:
redis-primary:
image: redis:7
command: ['redis-server', '--appendonly', 'yes']
deploy:
placement:
constraints: [node.labels.redis == primary]
volumes: [redis_primary:/data]
networks: [db-net]
redis-replica:
image: redis:7
command: ['redis-server', '--appendonly', 'yes', '--replicaof', 'redis-primary', '6379']
deploy:
placement:
constraints: [node.labels.redis == replica]
volumes: [redis_replica:/data]
networks: [db-net]
sentinel:
image: redis:7
command: ['sh', '-c', 'cp /sentinel.conf /tmp/sentinel.conf && redis-sentinel /tmp/sentinel.conf']
configs:
- source: sentinel_conf
target: /sentinel.conf
deploy:
replicas: 3
placement:
max_replicas_per_node: 1
networks: [db-net]
configs:
sentinel_conf:
external: true
With a sentinel.conf of sentinel monitor mymaster redis-primary 6379 2 plus the down-after and failover timeouts. Clients that speak Sentinel ask it for the current primary and follow a failover on their own.
Redis Cluster shards the keyspace across primaries and is the answer when a single instance is too small, not when you want failover for a small dataset. It needs six instances for a minimal safe layout and a cluster-aware client. For the single-instance case, the redis chart in the SwarmCLI repository is the one-command start.
Backups are the actual high availability
Replication protects you from a dead node. It replicates a bad DELETE just as faithfully. Every database above needs a backup that runs on a schedule, lands somewhere that is not the cluster, and has been restored at least once on purpose:
- PostgreSQL:
pg_dumpnightly for small databases; WAL archiving with pgBackRest or Barman for point-in-time recovery. - MySQL: Percona XtraBackup for hot, consistent backups;
mysqldumpfor small ones. - Redis: RDB snapshots plus AOF; copy the files out on a schedule.
A one-replica Swarm service running restic or a shell script on a cron image, with the backup volume mounted read-only and an off-site target, is enough. The swarm-cronjob chart schedules it.
Monitoring, and what to watch during a failover
Run postgres_exporter, mysqld_exporter and redis_exporter beside the databases and scrape them with the Prometheus and Grafana stack. Alert on replication lag, not just on up or down.
[!TIP] SwarmCLI tip: during a failover, the tasks view in SwarmCLI shows which node each database task is running on and its last state change, which answers "did the replica come up and where" faster than a dashboard.
Common problems
| Problem | Cause and fix |
|---|---|
| Task Pending after a node change | The constraint names a label no healthy node carries; relabel, or accept that the primary is down until restored |
| Empty database after a reschedule | The volume was not pinned and the task started on another node with a fresh volume; pin it, and restore from backup |
| Split brain | Two writers believing they are primary; use Patroni, Galera or Sentinel rather than hand-rolled promotion, and keep an odd quorum |
| Corruption on shared storage | A data directory on NFS; move it to a local volume and restore |
| Connection storms after failover | Put PgBouncer or ProxySQL in front so clients reconnect to one name |
Conclusion
Databases run well on Swarm when the data lives on a pinned local volume, replication is the database's own, and backups are real. Start with a single pinned instance and a tested backup, add a streaming replica when you need reads or a warm standby, and adopt Patroni, Galera or Sentinel when a person can no longer be the failover. The Definitive Docker Swarm Guide covers the placement and update mechanics these stacks rely on.