Containers & Orchestration·5 min read·

Hardening Docker Compose for Production: Secrets, Healthchecks, and Graceful Restarts

Transform basic development Docker Compose files into hardened, production-grade stacks featuring Docker secrets, robust healthchecks, and rolling restarts.

NB

Netbay Engineering

Netbay Engineering

On this page

Docker Compose is ubiquitous for local development, but deploying compose files directly to a production VPS without hardening invites reliability and security risks. Plaintext environment variables expose database credentials to process inspection tools, containers without healthchecks receive web traffic before initialization completes, and unmanaged restarts can trigger cascading failure loops during transient database outages.

By implementing native Docker secrets, explicit healthcheck probes, read-only root filesystems, and controlled restart policies, Docker Compose transforms into a reliable production deployment orchestrator on single-node VPS environments.

Most developers begin with a simple docker-compose.yml file designed for rapid iteration on a laptop. In that environment, setting environment: - POSTGRES_PASSWORD=mysecret is convenient. But on a production Linux server, environment variables are visible to any process with read access to /proc/pid/environ, appear in plain text inside docker inspect output, and are frequently captured in crash dumps or third-party APM error traces. Hardening your stack requires treating production Compose files with the same security rigor as Kubernetes manifests.

Expand
Docker Compose Production Stack
Docker Compose production stack security and lifecycle orchestration

Eliminating Plaintext Credentials with Docker Secrets

Placing sensitive API keys and database passwords in .env files or docker-compose environment stanzas exposes them across the operating system. Docker Compose natively supports file-based secrets that mount sensitive data as unprivileged tmpfs files inside the container:

Production Docker Compose Lifecycle & Secret Isolation App Container (Read-Only Root) read_only: true | tmpfs: /tmp Secrets mounted under /run/secrets/db_pass Active Healthcheck Loop test: ["CMD-SHELL", "curl -f http://localhost:8080/health"] interval: 10s | timeout: 3s | retries: 3 Zero-Downtime Rolling Update 1. Pull new container image tag 2. Instantiate replica on temporary port 3. Await healthcheck status: healthy 4. Atomically swap reverse proxy upstream 5. Send SIGTERM and graceful teardown
yaml
version: "3.8"

services:
  api:
    image: myorg/backend-service:2026.1
    restart: unless-stopped
    read_only: true
    tmpfs:
      - /tmp:rw,noexec,nosuid,size=64m
    secrets:
      - db_password
      - api_jwt_secret
    environment:
      - DB_USER=appuser
      - DB_NAME=production_db
      - DB_PASSWORD_FILE=/run/secrets/db_password
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://127.0.0.1:8080/health || exit 1"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 20s
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 2048M

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_jwt_secret:
    file: ./secrets/jwt_secret.key

Notice the read_only: true directive on the application container. This locks down the container root filesystem into an immutable, read-only state. If an attacker discovers an arbitrary file upload or remote code execution vulnerability in your application runtime, they cannot write malicious scripts to /tmp, alter system binaries in /bin, or inject backdoors. The tmpfs mount provides a volatile, memory-backed temporary scratchpad with noexec and nosuid flags set, ensuring that temporary files can be created by application processes without permitting binary execution.

Ensure that the secrets directory on the host VPS is strictly locked down:

bash
# Lock down secrets directory permissions on host
chmod 700 ./secrets
chmod 600 ./secrets/*

# Verify that file ownership belongs exclusively to root or deploy user
ls -la ./secrets

Orchestrating Zero-Downtime Restarts

When deploying a newer version of a service, a standard docker compose up -d can cause brief service interruptions. To achieve seamless zero-downtime rolling upgrades on a single server:

bash
# Pull new image without disrupting active containers
docker compose pull api

# Recreate and start the service with no dependencies downtime
docker compose up -d --no-deps --build api

# Monitor the healthcheck transition until status is healthy
docker inspect --format '{{json .State.Health.Status}}' $(docker compose ps -q api)

The flag --no-deps ensures that dependent services (such as a PostgreSQL database container or Redis cache) are not restarted simultaneously with the application service. The Docker engine starts the replacement container and verifies the healthcheck before disconnecting the previous container, ensuring active requests are fulfilled without dropping connections.

Healthchecks and Upstream Reverse Proxy Coordination

Configuring the start_period parameter prevents Docker from prematurely marking slow-starting Java or Node.js applications as unhealthy. When paired with reverse proxies like Caddy or Nginx, traffic is routed only after the healthcheck probe returns HTTP 200, eliminating 502 Bad Gateway errors during deploys.

Furthermore, defining explicit deploy.resources.limits prevents memory leaks in one service from triggering the Linux kernel Out-Of-Memory (OOM) killer against vital host services such as sshd or the reverse proxy.

Production Docker Compose provides enterprise-grade reliability without the operational complexity of distributed cluster schedulers.

Docker Compose in Production Best Practices
YouTube

You can spin up an Ubuntu 24.04 instance on Netbay in under 60 seconds with Intel Xeon Platinum compute and high-speed SSD storage in Lucknow — netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance