What Is a Docker Compose Pattern?
A Docker Compose pattern is a reusable docker-compose.yml template that solves one specific hosting problem: running a CMS with its database and cache, putting a reverse proxy in front of several apps, scheduling automated backups, or capping a container's memory before it takes down the host. Instead of writing service definitions from scratch every time, you copy the pattern, adjust the environment variables and volumes, and deploy.
This post is a recipe collection, not a tutorial. If you are still learning Compose syntax, service dependencies, networking, or volumes, read our Docker Compose tutorial first. That post teaches the concepts. This one assumes you already know them and gives you fifteen ready-to-copy stacks: real images, real environment variables, and the operational notes that separate a demo from something you would actually run on a production server.
Every example below uses the current Compose Specification syntax. There is no version: key at the top of the file — that field was deprecated years ago and modern docker compose ignores it. If you see a version: "3.8" line in an old tutorial, delete it.
1. WordPress + MariaDB + Redis Object Cache
The most common stack on the internet, done properly: PHP-FPM behind its own web server, a dedicated database, and a Redis object cache so every page load does not hammer MySQL with the same queries.
services:
wordpress:
image: wordpress:6-php8.3-fpm-alpine
restart: unless-stopped
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_password
WORDPRESS_DB_NAME: wordpress
WORDPRESS_CONFIG_EXTRA: |
define('WP_REDIS_HOST', 'cache');
define('WP_CACHE', true);
volumes:
- wp_data:/var/www/html
secrets:
- db_password
networks:
- wp_net
db:
image: mariadb:11
restart: unless-stopped
environment:
MARIADB_DATABASE: wordpress
MARIADB_USER: wordpress
MARIADB_PASSWORD_FILE: /run/secrets/db_password
MARIADB_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
volumes:
- db_data:/var/lib/mysql
secrets:
- db_password
- db_root_password
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 5
networks:
- wp_net
cache:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru
networks:
- wp_net
volumes:
wp_data:
db_data:
secrets:
db_password:
file: ./secrets/db_password.txt
db_root_password:
file: ./secrets/db_root_password.txt
networks:
wp_net:
Operational note: pair this with a WordPress-aware caching plugin (Redis Object Cache) so the WP_REDIS_HOST define is actually used — installing Redis alone does nothing without the plugin telling WordPress to talk to it.
2. PostgreSQL + pgAdmin for a Managed Database Endpoint
Use this when an application needs Postgres but you also want a web UI to inspect tables without SSH-ing in and running psql by hand.
services:
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: appdb
POSTGRES_USER: appuser
POSTGRES_PASSWORD_FILE: /run/secrets/pg_password
volumes:
- pg_data:/var/lib/postgresql/data
secrets:
- pg_password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
interval: 10s
timeout: 5s
retries: 5
networks:
- db_net
pgadmin:
image: dpage/pgadmin4:8
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
PGADMIN_DEFAULT_EMAIL: [email protected]
PGADMIN_DEFAULT_PASSWORD_FILE: /run/secrets/pgadmin_password
volumes:
- pgadmin_data:/var/lib/pgadmin
secrets:
- pgadmin_password
ports:
- "127.0.0.1:5050:80"
networks:
- db_net
volumes:
pg_data:
pgadmin_data:
secrets:
pg_password:
file: ./secrets/pg_password.txt
pgadmin_password:
file: ./secrets/pgadmin_password.txt
networks:
db_net:
Operational note: bind pgAdmin to 127.0.0.1 and reach it over an SSH tunnel or your reverse proxy — do not publish port 5050 to the public internet directly.
3. Node.js API + PostgreSQL With Startup Ordering
The classic mistake with a Node app and a database in the same stack is the app crashing on boot because Postgres was not ready yet. A healthcheck-gated depends_on fixes it without a custom wait script.
services:
api:
build: ./api
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgres://appuser:${DB_PASSWORD}@db:5432/appdb
NODE_ENV: production
ports:
- "3000:3000"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 15s
timeout: 3s
retries: 3
start_period: 20s
networks:
- api_net
db:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: appdb
POSTGRES_USER: appuser
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- api_db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
interval: 10s
timeout: 5s
retries: 5
networks:
- api_net
volumes:
api_db_data:
networks:
api_net:
Operational note: DB_PASSWORD comes from a .env file in the same directory as the compose file — Compose loads it automatically, so it never has to be typed into the YAML itself.
4. Nginx Reverse Proxy in Front of Multiple Apps
Use this when several services need to share one public IP and port 80/443, routed by hostname. This is the pattern behind almost every multi-app VPS.
services:
proxy:
image: nginx:1.27-alpine
restart: unless-stopped
ports:
- "80:80"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
depends_on:
- app_one
- app_two
networks:
- proxy_net
app_one:
image: traefik/whoami
restart: unless-stopped
networks:
- proxy_net
app_two:
image: traefik/whoami
restart: unless-stopped
networks:
- proxy_net
networks:
proxy_net:
The routing lives in nginx/conf.d/default.conf, mounted read-only into the proxy container:
server {
listen 80;
server_name one.example.com;
location / {
proxy_pass http://app_one:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
server {
listen 80;
server_name two.example.com;
location / {
proxy_pass http://app_two:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Operational note: service names (app_one, app_two) resolve through Docker's built-in DNS as long as every container shares the same network — see our Docker networking guide for how bridge networks and DNS resolution actually work under the hood.
5. Static Site With an Edge Cache Layer
When an origin (WordPress, a CMS, or a slow API) sits behind Nginx, adding a proxy cache in front of it cuts origin requests dramatically for content that does not change every request.
services:
cache:
image: nginx:1.27-alpine
restart: unless-stopped
ports:
- "80:80"
volumes:
- ./nginx/cache.conf:/etc/nginx/conf.d/default.conf:ro
- nginx_cache:/var/cache/nginx
depends_on:
- origin
networks:
- site_net
origin:
image: nginx:1.27-alpine
restart: unless-stopped
volumes:
- ./site:/usr/share/nginx/html:ro
networks:
- site_net
volumes:
nginx_cache:
networks:
site_net:
cache.conf defines the cache zone and forwards to the origin container:
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=edge:10m max_size=1g inactive=60m;
server {
listen 80;
location / {
proxy_cache edge;
proxy_cache_valid 200 10m;
proxy_pass http://origin:80;
add_header X-Cache-Status $upstream_cache_status;
}
}
Operational note: the X-Cache-Status header (HIT, MISS, EXPIRED) is the fastest way to confirm the cache is actually working — check it with curl -I before assuming anything is cached.
6. Nextcloud + PostgreSQL + Redis
Self-hosted file sync and collaboration. Postgres backs the metadata, Redis handles file locking and caching so multiple simultaneous uploads do not corrupt the lock table.
services:
nextcloud:
image: nextcloud:29-apache
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
POSTGRES_HOST: db
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD_FILE: /run/secrets/nc_db_password
REDIS_HOST: cache
NEXTCLOUD_TRUSTED_DOMAINS: files.example.com
volumes:
- nc_data:/var/www/html
secrets:
- nc_db_password
networks:
- nc_net
db:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD_FILE: /run/secrets/nc_db_password
volumes:
- nc_db_data:/var/lib/postgresql/data
secrets:
- nc_db_password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U nextcloud"]
interval: 10s
timeout: 5s
retries: 5
networks:
- nc_net
cache:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
networks:
- nc_net
volumes:
nc_data:
nc_db_data:
secrets:
nc_db_password:
file: ./secrets/nc_db_password.txt
networks:
nc_net:
Operational note: set NEXTCLOUD_TRUSTED_DOMAINS to the real hostname before first login — Nextcloud rejects requests from hostnames it does not recognize, which is the single most common "why can't I log in" support question.
7. Gitea + PostgreSQL
A self-hosted Git server for a team or personal projects. Lighter than running your own GitLab instance, with the same core Git-over-HTTP/SSH workflow.
services:
gitea:
image: gitea/gitea:1.23
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
GITEA__database__DB_TYPE: postgres
GITEA__database__HOST: db:5432
GITEA__database__NAME: gitea
GITEA__database__USER: gitea
GITEA__database__PASSWD: ${GITEA_DB_PASSWORD}
volumes:
- gitea_data:/data
ports:
- "3080:3000"
- "2222:22"
networks:
- gitea_net
db:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: gitea
POSTGRES_USER: gitea
POSTGRES_PASSWORD: ${GITEA_DB_PASSWORD}
volumes:
- gitea_db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U gitea"]
interval: 10s
timeout: 5s
retries: 5
networks:
- gitea_net
volumes:
gitea_data:
gitea_db_data:
networks:
gitea_net:
Operational note: mapping port 2222 to container port 22 keeps Gitea's internal SSH server off the host's own port 22, so your regular server SSH access is untouched.
8. n8n Workflow Automation + PostgreSQL
An open-source alternative to Zapier-style automation, running entirely on your own server with your own data.
services:
n8n:
image: n8nio/n8n:latest
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: db
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: ${N8N_DB_PASSWORD}
N8N_BASIC_AUTH_ACTIVE: "true"
N8N_BASIC_AUTH_USER: admin
N8N_BASIC_AUTH_PASSWORD: ${N8N_ADMIN_PASSWORD}
GENERIC_TIMEZONE: UTC
volumes:
- n8n_data:/home/node/.n8n
ports:
- "5678:5678"
networks:
- n8n_net
db:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: n8n
POSTGRES_USER: n8n
POSTGRES_PASSWORD: ${N8N_DB_PASSWORD}
volumes:
- n8n_db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n"]
interval: 10s
timeout: 5s
retries: 5
networks:
- n8n_net
volumes:
n8n_data:
n8n_db_data:
networks:
n8n_net:
Operational note: the default setup uses SQLite if you skip the DB_TYPE variables entirely — fine for testing a handful of workflows, but switch to Postgres before you have anything that matters running on a schedule.
9. Uptime Kuma, Standalone
A single-container uptime monitor with a clean dashboard — no database service needed, it ships with its own SQLite store.
services:
uptime-kuma:
image: louislam/uptime-kuma:1
restart: unless-stopped
volumes:
- kuma_data:/app/data
ports:
- "3001:3001"
volumes:
kuma_data:
Operational note: this is the one pattern in this list that genuinely needs nothing else — no database, no cache, no reverse proxy required for a single-user setup. Resist the urge to over-engineer it.
10. Immich Photo Library (App + Database + Cache + Machine Learning)
A self-hosted Google Photos alternative. The full stack has more moving parts than most self-hosted apps because it includes a machine learning container for face and object recognition.
services:
immich-server:
image: ghcr.io/immich-app/immich-server:release
restart: unless-stopped
depends_on:
- db
- redis
environment:
DB_HOSTNAME: db
DB_USERNAME: immich
DB_PASSWORD: ${IMMICH_DB_PASSWORD}
DB_DATABASE_NAME: immich
REDIS_HOSTNAME: redis
volumes:
- immich_uploads:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
ports:
- "2283:2283"
networks:
- immich_net
immich-machine-learning:
image: ghcr.io/immich-app/immich-machine-learning:release
restart: unless-stopped
volumes:
- immich_ml_cache:/cache
networks:
- immich_net
redis:
image: redis:7-alpine
restart: unless-stopped
networks:
- immich_net
db:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: immich
POSTGRES_USER: immich
POSTGRES_PASSWORD: ${IMMICH_DB_PASSWORD}
volumes:
- immich_db_data:/var/lib/postgresql/data
networks:
- immich_net
volumes:
immich_uploads:
immich_ml_cache:
immich_db_data:
networks:
immich_net:
Operational note: immich_uploads grows fast once photo backup starts — put this volume on a disk with real headroom, not the same partition as your OS.
11. Redis + Background Worker Queue
Any app that needs to offload slow work (sending email, resizing images, generating reports) out of the request/response cycle needs a queue. This is the minimal shape of that pattern.
services:
queue:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- queue_data:/data
networks:
- worker_net
worker:
build: ./worker
restart: unless-stopped
depends_on:
- queue
environment:
REDIS_URL: redis://queue:6379
WORKER_CONCURRENCY: "4"
deploy:
replicas: 2
networks:
- worker_net
volumes:
queue_data:
networks:
worker_net:
Operational note: redis-server --appendonly yes persists the queue to disk so an unfinished job is not silently lost if the container restarts mid-task. Without it, an in-flight job in Redis's memory disappears on restart.
12. Healthcheck and Restart Policy Pattern
This is not a standalone stack — it is the snippet you should be adding to every service above. A container that "runs" but never answers requests is worse than one that crashes cleanly, because docker ps shows it as healthy when it is not.
services:
app:
image: your-app:latest
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
Operational note: restart: unless-stopped is almost always the right choice over always — it respects a deliberate docker compose stop instead of fighting you by restarting a container you intentionally shut down. Naive depends_on without a healthcheck condition is a common source of race-condition crashes on container startup.
13. Resource Limit Pattern
One runaway container should never be able to take down every other service on the same host. Cap memory and CPU on every service, not just the ones you suspect will misbehave.
services:
app:
image: your-app:latest
restart: unless-stopped
mem_limit: 512m
mem_reservation: 256m
cpus: "0.50"
pids_limit: 200
Operational note: mem_limit/cpus work directly with plain docker compose up — you do not need Swarm mode for basic resource caps, that is a common misconception carried over from older Compose documentation.
14. Secrets and Environment Management Pattern
Passwords typed directly into environment: end up in docker inspect output, in your shell history if you ever ran the equivalent command manually, and potentially in a Git repository if the compose file gets committed. File-based secrets avoid all three.
services:
app:
image: your-app:latest
restart: unless-stopped
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Operational note: add secrets/ to .gitignore immediately, before the first commit — not after. See our Docker security guide for the wider picture on least-privilege containers, and our rootless Docker guide if you want to remove the root daemon from the equation entirely.
15. Backup Sidecar Pattern
A database with no automated backup is a ticking clock, not a database. This pattern adds a lightweight scheduler container that runs pg_dump on a cron schedule without installing cron on the host or inside the database image itself.
services:
db:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: appdb
POSTGRES_USER: appuser
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
networks:
- backup_net
backup:
image: mcuadros/ofelia:latest
restart: unless-stopped
depends_on:
- db
command: daemon --docker
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./backups:/backups
labels:
ofelia.job-exec.pg-dump.schedule: "0 0 3 * * *"
ofelia.job-exec.pg-dump.command: >
sh -c "PGPASSWORD=${DB_PASSWORD} pg_dump -h db -U appuser appdb
> /backups/appdb-$(date +%Y%m%d).sql"
networks:
- backup_net
volumes:
db_data:
networks:
backup_net:
Operational note: a backup you have never restored is a theory, not a backup. Test the restore path (psql < appdb-20260101.sql against a throwaway container) on a schedule, not just the dump job itself.
Copying These Into Production
A few things apply across every pattern above:
- Replace every plaintext
${VARIABLE}with a real value in a.envfile next to the compose file, or switch to the file-basedsecrets:pattern from #14 for anything sensitive. - Named volumes (
db_data:,wp_data:, and so on) survivedocker compose down. Onlydocker compose down -vdeletes them — know the difference before you run either command against a production stack. - Every stack here uses its own dedicated network so service name resolution stays predictable and containers from unrelated stacks cannot see each other by default.
- Pin image tags to a major version (
postgres:17-alpine, notpostgres:latest) so an unrelated upstream update does not change your database version out from under you on a routinedocker compose pull.
Running These Stacks on Panelica
Most of the applications covered here — WordPress, Nextcloud, Gitea, n8n, Uptime Kuma, Immich, PostgreSQL, pgAdmin, Redis, and more — are already available as one-click templates in Panelica's Docker Manager, which currently ships 176 pre-configured app templates covering CMS platforms, databases, monitoring tools, automation, and self-hosted alternatives to commercial SaaS products. A template handles the image pull, volume creation, and port mapping for you.
If you would rather write and manage your own docker-compose.yml files directly — which is exactly what this post assumes — Panelica's Docker Manager also supports importing and managing Compose stacks from the panel itself, alongside the same per-container resource controls (cgroups-based CPU and memory limits) shown in pattern #13. You get the copy-paste recipes from this post and a UI to watch logs, restart services, and adjust limits without touching the command line.
Frequently Asked Questions
Do these patterns require Docker Swarm?
No. Every example in this post runs with plain docker compose up -d on a single host. Swarm is a separate orchestration mode for running the same stack across multiple machines, and none of these patterns depend on it — including the resource limit pattern in #13, which is a common point of confusion carried over from older documentation that only described deploy.resources in a Swarm context.
Can I combine multiple patterns into one compose file?
Yes. Merge the services:, volumes:, and networks: blocks from two or more patterns into a single file, making sure every service name and volume name stays unique across the merged file. A common combination is the reverse proxy pattern (#4) sitting in front of several app stacks defined elsewhere in the same file.
What is the difference between this post and your Docker Compose tutorial?
Our Docker Compose tutorial teaches Compose from the ground up: syntax, networking, volumes, healthchecks, and multi-stage builds, using one WordPress example to illustrate each concept as it is introduced. This post assumes you already know that material and instead gives you fifteen separate, ready-to-copy production stacks for real applications.
How often should I update the image tags in these files?
Pin to a major version and update deliberately, not automatically. Run docker compose pull and review the changelog for that major version before applying it to a production stack — especially for anything with a database, where an unreviewed minor version bump can occasionally include a breaking migration.