Docker Compose in Production: A Practical Guide for Small Teams in 2026
Introduction: Docker Compose in 2026 — Alive and Relevant
For years, people have been saying Docker Compose is "outdated" and only good for local development. Yet in 2026, thousands of startups and small teams are successfully using it for production deployments — not out of ignorance, but by conscious choice.
Docker Compose remains a relevant tool wherever simplicity, iteration speed, and minimal operational overhead matter. Kubernetes is more powerful, but that power comes with complexity: team training, cluster maintenance, RBAC configuration, Helm charts. For a team of two to five people, that's often overkill.
In this article, we'll walk through how to build a production-ready infrastructure with Docker Compose — from file architecture to CI/CD and monitoring — and honestly discuss when it's time to look at Kubernetes.
Docker Compose v2 vs v3: Key Differences and What to Use Now
For a long time, there was confusion between the Compose file schema versions (v2, v3) and the versions of the tool itself. By 2026, the situation has clarified: Docker officially moved to a unified Compose Specification that combines the best of both worlds.
What Changed in Practice
- The
versionfield is no longer required and is effectively ignored by newer versions of Docker Compose v2+. You can remove it from your files. - The
depends_ondirective now supports conditions (condition: service_healthy), making it a fully capable tool for controlling startup order. - Secrets and configs received native support — not just for Swarm, but for regular Compose as well.
- The
docker-composecommand (hyphenated, Python version) is deprecated. Usedocker compose(the Go-based plugin) — it's faster and actively maintained.
The bottom line: in 2026, write your files using the Compose Specification without the version field, and use docker compose instead of docker-compose.
Architecture of a Production-Ready Compose File
A good production file is more than just a list of services. It's a declaration of how your application should behave under load, during failures, and during updates.
Example Base Structure
services:\n app:\n image: registry.example.com/myapp:${APP_VERSION:-latest}\n restart: unless-stopped\n networks:\n - internal\n - proxy\n environment:\n - DATABASE_URL=${DATABASE_URL}\n secrets:\n - db_password\n healthcheck:\n test: ["CMD", "curl", "-f", "http://localhost:8080/health"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 40s\n depends_on:\n db:\n condition: service_healthy\n deploy:\n resources:\n limits:\n cpus: "1.0"\n memory: 512M\n\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n networks:\n - internal\n volumes:\n - pg_data:/var/lib/postgresql/data\n environment:\n POSTGRES_PASSWORD_FILE: /run/secrets/db_password\n secrets:\n - db_password\n healthcheck:\n test: ["CMD-SHELL", "pg_isready -U postgres"]\n interval: 10s\n timeout: 5s\n retries: 5\n\n redis:\n image: redis:7-alpine\n restart: unless-stopped\n networks:\n - internal\n volumes:\n - redis_data:/data\n command: redis-server --appendonly yes\n\n nginx:\n image: nginx:alpine\n restart: unless-stopped\n ports:\n - "80:80"\n - "443:443"\n networks:\n - proxy\n volumes:\n - ./nginx/conf.d:/etc/nginx/conf.d:ro\n - certbot_data:/etc/letsencrypt\n\nnetworks:\n internal:\n driver: bridge\n internal: true\n proxy:\n driver: bridge\n\nvolumes:\n pg_data:\n redis_data:\n certbot_data:\n\nsecrets:\n db_password:\n file: ./secrets/db_password.txt\nKey Architectural Elements
- Networks: separate the internal network (service-to-service) from the external one (proxying). The
internal: trueflag prevents containers on that network from accessing the internet — a good practice for databases. - Volumes: always use named volumes for data that requires persistence. Never store production data in bind mounts.
- Health checks: mandatory for any service that others depend on. Without them,
depends_ononly checks that the container has started, not that the application is ready. - Restart policies:
unless-stoppedis a sensible default for production. It restarts the container on failure but leaves it alone when stopped manually. - Resource limits: constrain CPU and memory. Without limits, a single service can consume all host resources.
Secrets and Configs: Securely Managing Environment Variables
Passing secrets via environment variables in a .env file is the most common mistake. Such files end up in the repository, in CI logs, and in command history. Production requires a different approach.
Levels of Secret Management
- Docker Secrets (native): files are mounted into
/run/secrets/inside the container. Supported in regular Compose (not just Swarm). Shown in the example above. - External vaults: HashiCorp Vault, AWS Secrets Manager, Doppler. Secrets are fetched at deploy time and passed as files or via environment variables.
- CI/CD environment variables: GitLab CI, GitHub Actions, and other systems allow storing secrets at the project level. They are injected at deploy time wherever needed.
A Practical Approach for Small Teams
The minimum secure setup: secrets are stored as GitLab/GitHub variables, written to files on the server during deployment, and Compose reads them via the secrets mechanism. The .env file contains only non-sensitive settings (image names, ports, database names) and can safely live in the repository.
# .env (safe to commit)\nAPP_VERSION=1.4.2\nDB_NAME=myapp_prod\nREDIS_MAX_MEMORY=256mb\n\n# secrets/db_password.txt (DO NOT commit, add to .gitignore)\n# Created by the deploy script from CI/CD variables\nUpdating Services Without Downtime
Docker Compose doesn't have a built-in rolling update like Kubernetes. But zero downtime is achievable with the right architecture.
Blue-Green Strategy via Nginx
The simplest approach: keep a reverse proxy (nginx or Traefik) in front, and update the application with a brief switch:
# Zero-downtime update\ndocker compose pull app\ndocker compose up -d --no-deps --build app\nThe --no-deps flag updates only the specified service without restarting its dependencies. Docker starts the new container, waits for the health check to pass, then stops the old one. With a health check in place, downtime is reduced to seconds.
Traefik as a Smart Proxy
For smoother updates, many teams use Traefik instead of Nginx. Traefik reads Docker container labels and automatically routes traffic to healthy instances:
app:\n image: registry.example.com/myapp:${APP_VERSION}\n labels:\n - "traefik.enable=true"\n - "traefik.http.routers.app.rule=Host(`example.com`)"\n - "traefik.http.services.app.loadbalancer.healthcheck.path=/health"\nCI/CD Pipeline Integration
Automated deployment with Docker Compose in CI/CD is no rocket science. The typical flow: build image → push to registry → SSH to server → docker compose up.
Example GitLab CI/CD Pipeline
stages:\n - build\n - deploy\n\nbuild:\n stage: build\n script:\n - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .\n - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA\n only:\n - main\n\ndeploy:\n stage: deploy\n script:\n - echo "$DB_PASSWORD" > secrets/db_password.txt\n - export APP_VERSION=$CI_COMMIT_SHORT_SHA\n - docker compose pull\n - docker compose up -d --no-deps app\n - docker compose exec app php artisan migrate --force\n environment:\n name: production\n only:\n - main\nNote: database migrations run after the container starts, but before traffic is switched — this order matters. If you're using Laravel or another framework with a migration system, make sure your migrations are idempotent.
Rollback
Rolling back in the Compose world is trivial: simply change the image tag to the previous one and re-run docker compose up. Pin image tags to variables and never use latest in production.
Monitoring and Logging Containers
"Monitoring is not optional in production" — a cliché, but true. Docker Compose doesn't provide monitoring out of the box, but it integrates with popular solutions without much hassle.
Logging
Configure the logging driver at the service level or globally in /etc/docker/daemon.json:
app:\n logging:\n driver: "json-file"\n options:\n max-size: "50m"\n max-file: "5"\nFor centralized logging, Loki + Grafana is an excellent choice for small teams. Promtail collects logs from Docker containers and ships them to Loki. The entire stack can be brought up with the same Compose setup.
Metrics
The Prometheus + Grafana stack has become the de facto standard. Add the following to your Compose file:
- cAdvisor — container metrics (CPU, RAM, network)
- Node Exporter — host metrics
- Prometheus — metrics collection and storage
- Grafana — visualization
The entire monitoring stack can be extracted into a separate docker-compose.monitoring.yml and launched with docker compose -f docker-compose.yml -f docker-compose.monitoring.yml up -d.
Alerts
Alertmanager (part of the Prometheus ecosystem) or a simple uptimerobot.com for basic availability checks is enough for most small projects. Set up notifications in Slack or Telegram.
When to Move to Kubernetes: Honest Criteria
Docker Compose is a great tool, but it has real limitations. Here's an honest list of signals that it's time to consider Kubernetes:
Technical Triggers
- You need horizontal scaling: you want to run 5, 10, or 50 replicas of a service across multiple machines. Compose runs on a single host. Docker Swarm is a middle ground, but its development has slowed in 2026.
- Autoscaling is required: the HPA (Horizontal Pod Autoscaler) in Kubernetes responds to load automatically. In Compose, you have to adjust replicas manually.
- Complex orchestration: dozens of services with different versions, intricate dependencies, and independent deploy cycles — Kubernetes with Helm is significantly more manageable.
- Multi-region or multi-node: as soon as you need multiple servers for a single application, Compose is no longer sufficient.
- Strict SLAs: if you have a 99.99% SLA, you need automatic recovery mechanisms, rolling updates without downtime, and circuit breakers — all of which are native to Kubernetes.
Organizational Triggers
- The team has grown to 10+ people across several independent squads
- You have a dedicated DevOps/Platform engineer
- The number of microservices has exceeded 15–20
- You need environment isolation (dev/staging/prod) with different resources at the cluster level
When You Don't Need Kubernetes
If your monolith or set of 3–5 services handles 10,000 users per day on a single server, Docker Compose works perfectly well. Moving to Kubernetes adds at least 2–4 weeks of setup time and ongoing operational overhead. That time is better spent on your product.
Conclusion
Docker Compose in 2026 is a mature, reliable tool for production deployments by small teams. With the right architecture — separated networks, named volumes, health checks, secret management, and CI/CD integration — it delivers stable application performance without the operational complexity of Kubernetes.
Key principles worth remembering:
- Use
docker compose(v2 plugin) without theversionfield in your file - Separate networks into internal and external
- Never pass secrets through
.envfiles committed to the repository - Health checks are mandatory, not optional
- Pin image versions — never use
latestin production - Automate deployment via CI/CD from day one
- Add monitoring before the first incident, not after
Kubernetes is a powerful tool, but its time comes with scale. Don't over-engineer prematurely. Compose delivers 80% of the capabilities at 20% of the complexity — and for most small teams, that's the ideal trade-off.
Technologies
Tags
Ruslan Ismailov
Senior Web / Backend Developer. Senior web/backend developer with 9 years of experience. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservices, CI/CD. More about me →