Managing a web app, a database, and a cache using individual docker run commands requires manual network creation, volume flags, environment variables, and teardown scripts. One missed flag breaks the stack in ways that eat your afternoon.
Docker Compose removes that friction. You define your entire multi-container application in a single file written in YAML Ain't Markup Language (YAML), a human-readable configuration format, and manage it with a handful of commands.
In brief:
- Docker Compose defines multi-container applications declaratively in a
compose.yamlfile. It manages services and networks and provisions volumes through commands such asdocker compose upanddocker compose down. - The current release line is Compose v5, a Docker command-line interface (CLI) plugin invoked as
docker compose(with a space). The Pythondocker-composev1 binary is end of life. depends_ononly waits for containers to start; pair its long form with health checks to wait for actual readiness.- Strapi publishes Dockerfile and Compose examples, including examples in the official Strapi 5 Docker guide for running Strapi with a database locally or on a single host. Its catalog has no official container images.
What is Docker Compose?
If you're newer to containers themselves, this Docker essentials guide covers the fundamentals first.
Services define the containers that make up your application, including a Node.js API or a PostgreSQL database. A Redis cache can run as another service. Networks control how those containers communicate, with automatic Domain Name System (DNS)-based service discovery. Volumes store persistent data so it survives container restarts and rebuilds.
The current release line is Compose v5 (the latest tag is Compose v5.3.1). Per the official Compose history, v5 is functionally identical to v2; the version number jumped to v5 to avoid confusion with the legacy Compose file formats labeled v2 and v3, and the release added an official Go software development kit (SDK). The command is still docker compose with a space. The old Python docker-compose binary (v1) is no longer maintained, so treat any hyphenated command in older tutorials as legacy syntax.
The Compose file reference labels the top-level version: key an obsolete version key: Compose ignores it, validates against the latest schema regardless, and prints a warning if you include it. You can leave it out of new files.
Docker Compose vs. docker run
| Dimension | docker run | Docker Compose |
|---|---|---|
| Method | Imperative: step-by-step commands | Declarative: desired state in YAML |
| Scope | Single container per command | Multi-container application |
| Networking | Manual: create and connect yourself | Automatic: DNS by service name |
| Reproducibility | Shell scripts or tribal knowledge | YAML committed to version control |
| Teardown | Stop and remove each container individually | docker compose down handles everything |
You can use docker run when you need a quick, one-off container: testing an image interactively or running a standalone utility. Once your app depends on more than one service, Compose is usually easier to manage. Docker's own guidance is blunt: while docker run is convenient for launching containers, it becomes difficult to manage a growing application stack with it, per the multi-container apps docs. A teammate can clone your repo and run docker compose up.
How Docker Compose works
- Write a
compose.yaml: define your services, their images or build contexts, networking, volumes, and environment variables. - Run
docker compose up: Compose pulls or builds images, creates a default network, resolves dependencies, and starts containers in order. - Develop and iterate: re-running
updetects configuration or image changes and only recreates the containers that changed, per the Compose FAQ. - Tear down with
docker compose down: stops and removes containers and networks. Add-vto also wipe named volumes.
Short-form depends_on (depends_on: [db]) starts the dependency's container without checking whether the service inside it is ready. For databases with initialization delays, consider using the long form with condition: service_healthy and pairing it with a healthcheck, as documented in the startup order guide. The long form also supports service_completed_successfully for one-shot jobs like migrations, and a restart: true attribute that restarts the dependent service when its dependency is updated, per the services reference.
Anatomy of a compose.yaml file
The following compose.yaml defines a Node.js app backed by PostgreSQL:
services:
api:
build: ./api
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:secret@db:5432/mydb
depends_on:
db:
condition: service_healthy
db:
image: postgres:17.10
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: mydb
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d mydb"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
volumes:
db-data:Here is what each section does:
services: each child key defines a container.apibuilds from a local Dockerfile;dbpulls a pre-built image.environment: sets variables inside the container. Noticeapireferencesdbby service name in its connection string. Compose's built-in DNS resolves service names automatically, so you don't need to hardcode container IPs, which Docker assigns dynamically.volumes(top-level): declares a named volume so PostgreSQL data persists across restarts and rebuilds.healthcheckplus thedepends_oncondition:apistarts only after PostgreSQL actually accepts connections.
Essential Docker Compose commands
Start and stop services:
docker compose up -d # Start all services in background
docker compose up -d --build # Rebuild images, then start
docker compose up -d --wait # Wait for healthy status before returning
docker compose down # Stop and remove containers + networks
docker compose down -v # Also wipe named volumes (careful, deletes DB data)Monitor and debug:
docker compose ps # List running containers
docker compose logs -f # Follow logs from all services
docker compose logs -f api # Follow logs from one service
docker compose config # Print the fully resolved configurationInteract with running containers:
docker compose exec api sh # Open a shell in a running container
docker compose exec -T api npm test # Non-interactive (use -T in CI scripts)
docker compose run --rm api npm run migrate # One-off command, auto-remove container--rm is optional for docker compose run; use it explicitly, per the run reference.
Hot-reload during development:
docker compose watchThe watch command monitors your source files and reacts based on your develop.watch configuration. The develop specification defines five actions: sync copies changed files into the container (ideal for frameworks with hot module replacement like Vite), rebuild rebuilds the image and recreates the container, sync+restart syncs then restarts, restart restarts without syncing, and sync+exec syncs then runs a command inside the container. Watch requires services to have a build attribute; image-only services are unsupported.
A production-oriented compose.yaml, section by section
This compose.yaml defines a web application with PostgreSQL and Redis. It is a complete compose.yaml demonstrating features used in real projects. You will need the referenced build and environment files, along with the secret files and project-specific values:
services:
web:
build:
context: ./app
target: production
ports:
- "${APP_PORT:-3000}:3000"
env_file:
- path: ./default.env
required: true
- path: ./override.env
required: false
secrets:
- api_key
networks:
- frontend
- backend
depends_on:
db:
condition: service_healthy
restart: true
cache:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped
db:
image: postgres:17.10
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
volumes:
- db-data:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myapp"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: always
cache:
image: redis:7.2.15-alpine
volumes:
- cache-data:/data
networks:
- backend
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped
volumes:
db-data:
cache-data:
networks:
frontend:
driver: bridge
backend:
driver: bridge
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
file: ./secrets/api_key.txtHere is what each section does:
- Build target:
webtargets theproductionstage of a multi-stage Dockerfile, which keeps the final image free of build tools and dev dependencies. - Port mapping with a default:
${APP_PORT:-3000}:3000lets each developer setAPP_PORTin their.envfile to dodge port conflicts without editing shared YAML. env_filewithrequiredattributes:default.envmust exist;override.envis silently ignored if missing. Values set under a service'senvironmentkey overrideenv_filevalues when both define the same variable, per the environment variables docs.- Docker secrets: the database password is mounted at
/run/secrets/db_passwordinstead of passed as a plain environment variable. The_FILEsuffix convention (likePOSTGRES_PASSWORD_FILE) is used by Docker Official Images includingpostgresandmysql, per the secrets guide. - Custom networks:
frontendandbackendsegment traffic.dbandcacheonly exist onbackend, so the database is never reachable from the public-facing network. For stricter isolation,internal: trueon a network removes external connectivity entirely. - Restart policies:
alwaysfor the database,unless-stoppedfor app services. The default isno, meaning containers never restart automatically unless you set a policy.
Where Docker Compose fits: development, CI, and self-hosting
Docker officially supports Compose for single-host production deployments, alongside local development and continuous integration (CI). Multi-node scheduling may call for an orchestrator such as Kubernetes; Docker's Compose Bridge can convert a Compose file into Kubernetes manifests if that need arises. For a broader look at deployment strategies, our overview of Strapi deployment options covers several paths.
Local development stacks
A single docker compose up -d brings up a repeatable application stack on any machine with Docker installed, reducing onboarding setup to one command. Compose variables and .env files handle per-developer differences (credentials, ports) without touching the shared configuration. Use the environment or env_file service attribute to populate container environments; the project-level .env file supplies variable interpolation in the YAML itself, per the precedence docs.
This pattern extends naturally to full-stack work: you can run your CMS and database in Compose while connecting a Next.js and Strapi frontend from your host machine.
Continuous integration and continuous delivery pipelines
CI can start the services and run tests, then remove the containers with these commands:
docker compose up -d --wait
./run_tests
docker compose down -vYour integration tests run against a real PostgreSQL and a real Redis. When teardown succeeds with -v, each run can start with fresh containers and named volumes, reducing state carryover between runs.
On GitHub Actions, current Ubuntu runner images ship with Compose preinstalled, so no separate Compose installation is usually needed on GitHub-hosted Ubuntu runners. The legacy docker-compose v1 binary was removed in August 2024; scripts still calling it will fail.
On GitLab CI, Docker-in-Docker (DinD) needs DOCKER_TLS_CERTDIR: "/certs" (plus DOCKER_HOST: tcp://docker:2376 on the Kubernetes executor), and Compose is not included in DinD, so you need to install it as a pipeline step.
The advantage over platform-specific service containers is portability: the same compose.yaml runs on your laptop and in CI. You can extend this into multi-environment pipelines covering dev, pre-production, and production.
Self-hosting Strapi with Docker Compose
Compose is one way to self-host applications that ship a container image and need a backing database, and a headless CMS is a textbook case. Strapi, built on Node.js, stores content in a relational database, so a Compose file pairing the two gives you the whole stack in one managed setup. The pattern is covered end to end in headless CMS self-hosting. For context on when self-hosting makes sense versus a managed platform, see our hosting comparison.
- The official Docker guide provides Dockerfile and Compose examples: a development Dockerfile based on
node:22-alpinerunningnpm run develop, and a multi-stage production Dockerfile. Strapi publishes no official container images. One build gotcha from those docs: avoid settingNODE_ENV=productionbeforenpm ci, or npm skips the devDependencies Strapi needs to compile the Admin Panel. - Set
DATABASE_HOSTto the Compose service name (for example,strapiDB). Containers talk over the Docker network by service name. - For production, Strapi supports several database options. Strapi 5's database configuration docs list PostgreSQL (17.0 recommended, 14.0 minimum), MySQL configuration, MariaDB, and SQLite. SQLite is the default for quick starts, but it has production limitations. The PostgreSQL connection setup walks through the
database.jsconfiguration. For a broader look at how these databases compare, see our relational database comparison. - Consider setting the connection pool
minto0in Docker. The database configuration docs recommend it because Docker kills idle connections. - Persist production secrets explicitly. Strapi auto-generates defaults for
APP_KEYS,API_TOKEN_SALT,ADMIN_JWT_SECRET,JWT_SECRET, and related environment variables, but production deployments should explicitly configure and persist stable values where appropriate. You can pass.envvalues through Strapi's environment configuration. Docker secrets are mounted as files and require explicit configuration or a startup script to read those files and expose their values to Strapi; Strapi does not automatically apply the_FILEconvention used by some Docker Official Images. For a broader checklist on hardening your instance, the Strapi security checklist is a good companion reference.
Platforms like Coolify can manage Compose-based deployments; see this Coolify deployment guide. And if you'd rather skip server management entirely, Strapi Cloud provides fully managed hosting. For a full walkthrough of deploying Strapi in general, the Strapi deployment guide covers multiple approaches.
Docker Compose best practices
- Consider naming the file
compose.yaml. It is the canonical filename per the Compose application model;docker-compose.ymlstill works for backward compatibility, but if both exist, Compose preferscompose.yaml. - Keep secrets out of inline environment variables. Docker secrets work well for passwords and API keys, while
.gitignored.envfiles suit non-sensitive configuration. - Consider pinning image versions. Docker's build best practices state that image tags are mutable. A publisher can update a tag to point to a new image. You can pin at least to a specific version (
postgres:17.10); for supply-chain integrity, consider pinning to an immutable image digest (postgres:17.10@sha256:...). Digest pinning does opt you out of automatic security patches, so it helps to pair it with tooling that opens update pull requests. Check end-of-life dates when you pick versions: PostgreSQL 14, for instance, reaches end of life in November 2026, per the PostgreSQL support schedule. - Multi-stage builds help keep runtime images focused. You can reference the production stage via
build.targetso runtime images exclude compilers and dev dependencies. - Health checks and explicit restart policies help with resilience. Health checks make
condition: service_healthywork;unless-stoppedkeeps app services running through daemon restarts while respecting intentional stops. - Avoid
container_nameon services you might scale. Compose cannot scale a service with a fixed container name beyond one instance. - You can toggle optional services with Compose profiles. Debug tools and admin UIs can live in the same file without starting by default; activate them with
docker compose --profile debug upor theCOMPOSE_PROFILESvariable. - Run
docker compose configbefore you deploy. It prints the fully resolved YAML, including interpolated variables and merged overrides. Run it whenever a service is not picking up the configuration you expect.
Start building with Docker Compose
Docker Compose reduces multi-container management to one version-controlled YAML file and a few commands. You can follow the Strapi Compose tutorial to build a working CMS stack, or work through the Compose documentation for the full reference.
Ready to see how Strapi fits into your container workflow? Start building with Strapi Cloud or spin up a local instance with npx create-strapi@latest and pair it with the compose.yaml examples above.







