Deploying With Docker Compose
Disclosure: This post contains one or more links to providers we have a real, registered affiliate/referral relationship with. We may earn a commission at no extra cost to you if you sign up through them.
Deploying with Docker Compose is one of the most reliable ways to run multi-container applications on a single host without pulling in the operational overhead of a full orchestrator. Whether you’re standing up a small SaaS backend, a self-hosted automation stack, or a staging environment that mirrors production, Docker Compose gives you a declarative, version-controlled way to define services, networks, and volumes in one file. This article walks through the practical mechanics of deploying with Docker Compose, from initial file structure to production-grade patterns like environment separation, health checks, and rolling updates.
Why Teams Choose Docker Compose for Deployment
Docker Compose sits in a comfortable middle ground between running raw docker run commands and adopting a full orchestration platform like Kubernetes. For a single VPS or a small fleet of servers, deploying with Docker Compose gives you most of the benefits of container orchestration — service definitions, networking, dependency ordering — without the operational burden of managing a control plane, etcd, or a CNI.
A few reasons this approach remains popular for small-to-medium production workloads:
docker-compose.yml file that can be reviewed in a pull request.The tradeoff is that Compose is fundamentally single-host. If you eventually need multi-node scheduling, automatic failover across machines, or horizontal autoscaling based on load, you’ll outgrow it — at which point comparing Kubernetes vs Docker Compose becomes a worthwhile exercise. But for the large majority of self-hosted tools, internal APIs, and small production services, deploying with Docker Compose is the pragmatic default.
Compose vs. a Bare Dockerfile
It’s worth being precise about scope: a Dockerfile builds a single image, while Compose orchestrates multiple containers built from one or more Dockerfiles or pulled from a registry. If you’re unclear on where one tool’s responsibility ends and the other’s begins, the breakdown in Dockerfile vs Docker Compose covers the distinction in more depth than this guide will.
Structuring Your Compose File for Production
A production-ready docker-compose.yml looks different from a quick local prototype. The core differences are pinned image versions, explicit resource limits, named volumes instead of bind mounts for stateful data, and a defined restart policy.
version: "3.9"
services:
app:
image: myregistry.example.com/myapp:1.4.2
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://app:app@db:5432/appdb
ports:
- "3000:3000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=app
- POSTGRES_DB=appdb
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db_data:
Pinning postgres:16-alpine instead of postgres:latest matters more than it looks: an unpinned tag means your next docker compose pull could silently bring in a breaking major-version upgrade. The same logic applies to your own application image — tag it with a version or a Git SHA, never latest, when deploying with Docker Compose to a server you care about.
If your stack includes a database, it’s worth reading a dedicated setup guide rather than guessing at healthcheck and volume conventions — see Postgres Docker Compose or, if you’re on the newer image family, PostgreSQL Docker Compose. Similarly, if you’re caching sessions or queueing jobs, the Redis Docker Compose guide covers the equivalent pattern for that service.
Environment Variables and Secrets
Never hardcode credentials directly into docker-compose.yml if that file is committed to version control. Use an .env file (excluded from Git) or a secrets manager, and reference variables with ${VARIABLE_NAME} syntax. For a deeper walkthrough of variable precedence, interpolation, and multi-environment .env layering, see Docker Compose Env and Docker Compose Environment Variables. For anything more sensitive than a database password — API keys, TLS private keys, third-party tokens — Compose’s native secrets: block (backed by files, not environment variables) is a better fit; the pattern is covered in Docker Compose Secrets.
The Deployment Workflow Step by Step
Deploying with Docker Compose to a remote server generally follows the same sequence regardless of what’s inside the stack:
1. Provision the host (a VPS is sufficient for most workloads — see the unmanaged VPS hosting guide if you’re new to self-managing a server).
2. Install Docker Engine and the Compose plugin.
3. Copy or git clone your docker-compose.yml, any override files, and your .env file onto the host.
4. Pull images and start the stack.
5. Verify health checks and logs before considering the deploy complete.
# On the remote host, after cloning the repo
docker compose pull
docker compose up -d
docker compose ps
docker compose up -d starts everything in detached mode. It’s declarative — running it again after editing the YAML only recreates the containers whose configuration actually changed, leaving unaffected services untouched. That idempotency is a big part of why deploying with Docker Compose fits naturally into repeatable deployment scripts and CI pipelines.
Rebuilding and Updating a Running Stack
When you change application code rather than just configuration, you need to rebuild the image before Compose will pick up the change:
docker compose build app
docker compose up -d --no-deps app
The --no-deps flag avoids restarting dependent services (like the database) that don’t need to move. If you’re troubleshooting a rebuild that doesn’t seem to reflect your latest code — usually a stale layer cache — the Docker Compose Rebuild guide and the flag reference in Docker Compose Up Build cover the common causes and fixes in detail.
Zero-Downtime Considerations
Docker Compose alone doesn’t give you true rolling updates the way an orchestrator does — docker compose up -d will briefly stop and recreate a changed container. For most internal tools and low-traffic services this brief interruption is acceptable. If it isn’t, common mitigations include running two instances behind a reverse proxy and swapping traffic manually, or using docker compose up -d --scale app=2 temporarily during a deploy so at least one instance stays available while the other restarts. This is also the point at which many teams start evaluating whether they’ve genuinely outgrown a single-host deployment model.
Networking and Reverse Proxy Setup
By default, Compose creates a private bridge network for each project, and services can reach each other by their service name as a hostname (db, app, etc.) — no manual linking required. For exposing the stack to the public internet, most production deployments put a reverse proxy in front rather than binding application ports directly:
services:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
volumes:
caddy_data:
A reverse proxy handles TLS termination and lets you route multiple services on one host through a single pair of exposed ports. If you’re also using Cloudflare in front of the server for DNS, caching, or WAF rules, the Cloudflare Page Rules guide is a useful companion for tuning cache behavior once your Compose stack is reachable.
Logging, Debugging, and Observability
Once a stack is deployed, the operational work shifts to keeping it healthy. docker compose logs is the first tool you’ll reach for:
docker compose logs -f --tail=100 app
For a full breakdown of filtering, timestamps, and log driver configuration, see Docker Compose Logs and the command-focused Docker Compose Log Command reference. If containers are logging excessively and filling disk, configuring log rotation at the driver level (via logging.options.max-size) is covered in Docker Compose Logging.
Beyond logs, monitoring metrics over time is worth setting up early rather than after an incident — a lightweight Prometheus stack alongside your application containers is a common starting point, described in the Prometheus Docker Compose guide.
Tearing Down Safely
Stopping a stack cleanly matters as much as starting one, especially when volumes hold production data:
docker compose down
docker compose down removes containers and networks but leaves named volumes intact by default — you’d need -v to also remove volumes, which you should do deliberately, never as a habit. For the full set of flags and what each one actually deletes, see Docker Compose Down.
Automating Deployments in CI/CD
Manually SSHing into a server to run docker compose up -d works for small projects but doesn’t scale to a team. A minimal CI/CD deployment step typically looks like this:
ssh [email protected] "cd /opt/myapp &&
git pull origin main &&
docker compose pull &&
docker compose up -d --remove-orphans"
The --remove-orphans flag cleans up containers for services that were removed from the compose file since the last deploy — a small detail that prevents old, orphaned containers from lingering silently on the host. Wiring this into GitHub Actions, GitLab CI, or a similarly simple pipeline is often all that’s needed for a small team deploying with Docker Compose to one or two servers; you don’t need a heavyweight deployment platform to get repeatable, auditable releases.
Choosing Where to Host Your Compose Stack
The hosting layer underneath your Compose stack matters more than it might seem. You want predictable CPU and memory, fast local disk for database volumes, and a provider that makes snapshotting or resizing straightforward. A well-specified VPS from a provider like DigitalOcean or Hetzner is generally sufficient for a Compose-based deployment handling moderate traffic — you don’t need managed Kubernetes for a stack that fits comfortably on one machine.
Recommended: Ready to put this into practice? DigitalOcean is a tool we use for exactly this, and we have a real, disclosed affiliate relationship with them.
FAQ
Does Docker Compose support automatic scaling across multiple servers?
No. Compose is designed for single-host deployments. You can run multiple replicas of a service on one host with --scale, but distributing containers across several physical or virtual machines requires an orchestrator such as Docker Swarm or Kubernetes.
How do I run different configurations for staging and production?
Use Compose’s override file mechanism. Keep a base docker-compose.yml with shared settings, then layer docker-compose.prod.yml or docker-compose.staging.yml on top with docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d. This avoids duplicating the entire file per environment.
What’s the safest way to update a production stack without losing data?
Never run docker compose down -v on a live environment — it removes volumes along with containers. Instead, back up named volumes (or your database) before any significant change, use docker compose pull plus docker compose up -d for routine updates, and test the update path in staging first.
Can I use Docker Compose alongside a Dockerfile I already have?
Yes, and this is the normal setup. Compose typically references a build: context pointing at your Dockerfile for services you build yourself, while pulling prebuilt images (databases, proxies, caches) directly from a registry.
Conclusion
Deploying with Docker Compose remains a solid choice for teams running single-host or small multi-service applications where the operational simplicity of one YAML file outweighs the need for cluster-wide scheduling. Getting it right in production comes down to a handful of disciplined habits: pin your image versions, separate secrets from configuration, add health checks so dependent services start in the right order, and automate the deploy step so it’s repeatable rather than manual. Once those fundamentals are in place, deploying with Docker Compose scales comfortably from a single side project to a production service handling real traffic — and if you eventually outgrow a single host, the same YAML you wrote here becomes a useful reference point for evaluating what a heavier orchestrator would actually buy you. For the official reference on every option covered above, see the Docker Compose documentation and the broader Docker Engine documentation.
Related articles:
Leave a Reply