DevOps

Docker Compose in Production: A Practical Guide for Small Teams in 2026

Ruslan Ismailov Published 10 min read
D

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 version field is no longer required and is effectively ignored by newer versions of Docker Compose v2+. You can remove it from your files.
  • The depends_on directive 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-compose command (hyphenated, Python version) is deprecated. Use docker 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\n

Key Architectural Elements

  • Networks: separate the internal network (service-to-service) from the external one (proxying). The internal: true flag 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_on only checks that the container has started, not that the application is ready.
  • Restart policies: unless-stopped is 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

  1. Docker Secrets (native): files are mounted into /run/secrets/ inside the container. Supported in regular Compose (not just Swarm). Shown in the example above.
  2. External vaults: HashiCorp Vault, AWS Secrets Manager, Doppler. Secrets are fetched at deploy time and passed as files or via environment variables.
  3. 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\n

Updating 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\n

The --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"\n

CI/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\n

Note: 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"\n

For 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:

  1. Use docker compose (v2 plugin) without the version field in your file
  2. Separate networks into internal and external
  3. Never pass secrets through .env files committed to the repository
  4. Health checks are mandatory, not optional
  5. Pin image versions — never use latest in production
  6. Automate deployment via CI/CD from day one
  7. 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 →