← Back to DevBytes

Docker Stack Deployments: Production Guide

What is Docker Stack Deploy?

Docker Stack Deploy is the command-line interface that allows you to deploy and manage multi-service applications on a Docker Swarm cluster using a declarative Compose file. It extends the familiar docker-compose.yml format with Swarm-specific production features such as replicated services, rolling updates, secrets management, and overlay networking.

When you run docker stack deploy, Docker reads your Compose file, creates the necessary services, networks, and secrets, and then distributes the workload across the nodes in your Swarm according to the placement constraints and replication settings you've defined. This transforms a simple container definition into a resilient, scalable, production-grade deployment.

At its core, a stack is a group of interrelated services that share the same lifecycle. Think of it as a namespace for your application's components. For example, a web application stack might include a frontend service, a backend API service, a database service, a Redis cache, and a background worker—all deployed together, sharing networks, and managed as a single unit.

# Deploy a stack from a Compose file
docker stack deploy -c docker-compose.yml myapp

# Deploy with a custom stack name
docker stack deploy -c production-stack.yml prod-environment

Why Docker Stack Deployments Matter for Production

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

In development, docker-compose up works perfectly for running multi-container applications on a single host. But production environments demand more: high availability, zero-downtime deployments, load balancing, secret management, and the ability to span workloads across multiple machines. Docker Stack Deployments address all of these concerns natively.

High Availability and Fault Tolerance

When you deploy a stack to a Swarm, Docker schedules service replicas across multiple nodes. If a node fails, the orchestrator automatically reschedules the affected containers to healthy nodes. This self-healing behavior ensures your application stays online even during infrastructure failures.

Declarative Desired State

Stacks operate on a desired state model. You declare what you want in the Compose file, and Docker continuously works to make reality match that declaration. If someone manually stops a container, Docker restarts it. If a node goes down, containers are rescheduled elsewhere. This declarative approach eliminates the need for external process supervisors.

Zero-Downtime Rolling Updates

Updating a service in production traditionally required downtime windows. With Docker Stack Deploy, rolling updates replace old containers with new ones incrementally, maintaining service capacity throughout the process. You can control the parallelism, delay between updates, and failure handling behavior.

Built-In Secrets Management

Passing sensitive configuration as environment variables or baking them into images is dangerous. Docker Swarm provides encrypted secrets that are mounted as files inside containers at runtime, accessible only to authorized services. Stack deployments integrate seamlessly with this secrets system.

Prerequisites: Setting Up a Swarm

Before deploying stacks, you need a functioning Docker Swarm. A Swarm can range from a single-node development cluster to a multi-node production setup with manager redundancy.

Initializing a Swarm

On your first manager node, initialize the Swarm:

# Initialize the swarm on the first manager
docker swarm init --advertise-addr eth0

# The output provides a join token for worker nodes
# Swarm initialized: current node (node1) is now a manager.
# To add a worker to this swarm, run:
# docker swarm join --token SWMTKN-1-xxxxx 192.168.1.10:2377

Adding Worker Nodes

On each worker machine, use the join command provided during initialization:

# Join as a worker
docker swarm join --token SWMTKN-1-xxxxx 192.168.1.10:2377

# Verify the swarm from a manager
docker node ls

# Output:
# ID        HOSTNAME    STATUS    AVAILABILITY   MANAGER STATUS
# abc123 *  manager1    Ready     Active         Leader
# def456    worker1     Ready     Active
# ghi789    worker2     Ready     Active

For Production: Multiple Managers

For high availability of the control plane, deploy three or five manager nodes (odd numbers prevent split-brain scenarios):

# On the initial manager, get the manager join token
docker swarm join-token manager

# Join additional manager nodes with the provided command
docker swarm join --token SWMTKN-1-manager-token 192.168.1.10:2377

Anatomy of a Production Compose File

A production Compose file for stacks uses version 3.x or higher and includes Swarm-specific deployment directives. Let's examine a complete example for a web application stack with PostgreSQL and Redis.

version: '3.8'

# Define named volumes that can persist across service updates
volumes:
  postgres_data:
    driver: local
  redis_data:
    driver: local

# Define secrets (encrypted at rest, mounted as files in containers)
secrets:
  db_password:
    external: true
    external_name: myapp_db_password_v2
  api_key:
    external: true
    external_name: myapp_api_key_2025

# Overlay networks for service isolation
networks:
  frontend:
    driver: overlay
    attachable: true
  backend:
    driver: overlay
    internal: true  # Prevents containers on this network from accessing the internet

services:
  # Frontend web server (public-facing)
  frontend:
    image: nginx:1.25-alpine
    ports:
      - target: 80
        published: 80
        protocol: tcp
        mode: ingress  # Load balanced across all swarm nodes
    networks:
      - frontend
    configs:
      - source: nginx_config
        target: /etc/nginx/nginx.conf
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
        order: start-first
        failure_action: rollback
        max_failure_ratio: 0.2
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
      placement:
        constraints:
          - node.role == worker
      resources:
        limits:
          cpus: '0.50'
          memory: 256M
        reservations:
          cpus: '0.25'
          memory: 128M
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

  # Application backend API
  api:
    image: myapp/api:3.2.1
    networks:
      - frontend
      - backend
    secrets:
      - db_password
      - api_key
    environment:
      - DATABASE_URL=postgresql://appuser@postgres/appdb
      - REDIS_URL=redis://redis:6379
      - SECRETS_DIR=/run/secrets
    deploy:
      replicas: 4
      update_config:
        parallelism: 2
        delay: 15s
        order: start-first
        failure_action: rollback
      restart_policy:
        condition: on-failure
        window: 30s
      placement:
        constraints:
          - node.role == worker
        preferences:
          - spread: node.labels.datacenter
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.50'
          memory: 256M
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 15s
      timeout: 3s
      retries: 5
      start_period: 20s

  # PostgreSQL database
  postgres:
    image: postgres:16-alpine
    networks:
      - backend
    secrets:
      - db_password
    environment:
      POSTGRES_USER: appuser
      POSTGRES_DB: appdb
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    deploy:
      replicas: 1  # Single replica for stateful database
      update_config:
        order: stop-first  # Stop old before starting new for stateful services
      restart_policy:
        condition: on-failure
        delay: 10s
        max_attempts: 5
      placement:
        constraints:
          - node.labels.database == true  # Pin to specific node with local storage
      resources:
        limits:
          cpus: '2.0'
          memory: 2048M
        reservations:
          cpus: '1.0'
          memory: 1024M
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Redis cache
  redis:
    image: redis:7-alpine
    networks:
      - backend
    volumes:
      - redis_data:/data
    command: ["redis-server", "--appendonly", "yes"]
    deploy:
      replicas: 1
      update_config:
        order: stop-first
      restart_policy:
        condition: on-failure
      placement:
        constraints:
          - node.labels.cache == true
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3

  # Background worker
  worker:
    image: myapp/worker:3.2.1
    networks:
      - backend
    secrets:
      - db_password
      - api_key
    environment:
      - DATABASE_URL=postgresql://appuser@postgres/appdb
      - REDIS_URL=redis://redis:6379
      - QUEUE_NAME=default
    deploy:
      replicas: 2
      update_config:
        parallelism: 1
        delay: 30s
        order: start-first
      restart_policy:
        condition: on-failure
      placement:
        constraints:
          - node.role == worker
      resources:
        limits:
          cpus: '0.75'
          memory: 384M
        reservations:
          cpus: '0.25'
          memory: 192M

configs:
  nginx_config:
    file: ./nginx/nginx.conf

Key Deployment Directives Explained

replicas: The number of container instances to run. Stateless services (frontend, API, workers) can scale horizontally. Stateful services (databases) typically run a single replica unless clustered.

update_config: Controls how rolling updates proceed. parallelism sets how many containers are updated simultaneously. delay introduces a waiting period between update batches. order: start-first starts the new container before stopping the old one (minimizes downtime), while stop-first is safer for stateful services. failure_action: rollback automatically reverts if the update fails.

restart_policy: Configures automatic restarts. The condition can be none, on-failure, or any. The window option defines a time window during which restart attempts are counted.

placement constraints: Control which nodes can run the service. Use node labels, roles, or hostnames. For example, node.labels.database == true pins databases to specific hardware with fast storage.

resources: Set CPU and memory limits and reservations. Limits prevent containers from consuming excessive resources. Reservations guarantee minimum resources for scheduling decisions.

Deploying a Stack to Production

With your Compose file ready and Swarm initialized, deployment is a single command. But production deployments require careful sequencing.

Step 1: Create External Secrets First

Secrets referenced as external: true must exist before stack deployment:

# Create secrets from files or stdin
echo "s3cur3_p4ssw0rd_v2" | docker secret create myapp_db_password_v2 -
echo "sk-abc123def456" | docker secret create myapp_api_key_2025 -

# Verify secrets exist
docker secret ls

# Output:
# ID                          NAME                       CREATED             UPDATED
# abc123def456                myapp_db_password_v2       5 minutes ago       5 minutes ago
# def789ghi012                myapp_api_key_2025         3 minutes ago       3 minutes ago

Step 2: Apply Node Labels

If your Compose file uses placement constraints with custom labels, apply them before deployment:

# Label nodes for service placement
docker node update --label-add database=true node-db1
docker node update --label-add cache=true node-cache1
docker node update --label-add datacenter=us-east worker1
docker node update --label-add datacenter=us-east worker2
docker node update --label-add datacenter=us-west worker3

# Verify labels
docker node inspect worker1 --format '{{json .Spec.Labels}}'

Step 3: Deploy the Stack

# Deploy the stack
docker stack deploy -c docker-compose.yml myapp

# Expected output:
# Creating network myapp_frontend
# Creating network myapp_backend
# Creating secret myapp_db_password (from external)
# Creating config myapp_nginx_config
# Creating service myapp_frontend
# Creating service myapp_api
# Creating service myapp_postgres
# Creating service myapp_redis
# Creating service myapp_worker

Notice how Docker prefixes resources with the stack name (myapp_). This namespace isolation allows multiple stacks to coexist on the same Swarm without naming conflicts.

Step 4: Verify Deployment Status

# Check stack services
docker stack services myapp

# Output:
# ID            NAME             MODE         REPLICAS   IMAGE
# abc123        myapp_frontend   replicated   3/3        nginx:1.25-alpine
# def456        myapp_api        replicated   4/4        myapp/api:3.2.1
# ghi789        myapp_postgres   replicated   1/1        postgres:16-alpine
# jkl012        myapp_redis      replicated   1/1        redis:7-alpine
# mno345        myapp_worker     replicated   2/2        myapp/worker:3.2.1

# Detailed service inspection
docker service ps myapp_api

# Check container distribution across nodes
docker stack ps myapp --filter "desired-state=running"

Managing Stacks in Production

Listing and Inspecting Stacks

# List all stacks
docker stack ls

# Get detailed stack information
docker stack services myapp
docker stack ps myapp

# Inspect individual services
docker service inspect myapp_api --pretty

Scaling Services

You can scale services without redeploying the entire stack:

# Scale the API service to 8 replicas
docker service scale myapp_api=8

# Scale multiple services at once
docker service scale myapp_frontend=5 myapp_worker=4

# Verify the new replica count
docker service ls --filter "name=myapp_api"

Viewing Logs

Aggregate logs from all replicas of a service:

# Tail logs from the API service
docker service logs -f --tail=100 myapp_api

# Get logs from the last hour with timestamps
docker service logs --since=1h --timestamps myapp_worker

# Get detailed logs including timestamps and container IDs
docker service logs -f --details --timestamps myapp_api

Removing a Stack

# Remove the entire stack and all its resources
docker stack rm myapp

# This removes services, networks, and configs
# Note: External secrets and volumes are NOT removed automatically
# To clean up secrets:
docker secret rm myapp_db_password_v2 myapp_api_key_2025

# To clean up volumes (caution: this destroys data):
docker volume rm myapp_postgres_data myapp_redis_data

Rolling Updates and Rollbacks

One of the most powerful production features of Docker Stack is the ability to perform zero-downtime rolling updates and instantaneous rollbacks.

Performing a Rolling Update

To update a service, simply update the image tag or configuration in your Compose file and redeploy:

# Update the API image version in docker-compose.yml
# Change: image: myapp/api:3.2.1  →  image: myapp/api:3.3.0

# Redeploy the stack (only changed services are updated)
docker stack deploy -c docker-compose.yml myapp

# Watch the rolling update progress
watch docker service ps myapp_api

# You'll see old containers being replaced one by one:
# myapp_api.1  myapp/api:3.2.1  node1  Shutdown   Shutdown
# myapp_api.1  myapp/api:3.3.0  node1  Running    Running   15 seconds ago
# myapp_api.2  myapp/api:3.2.1  node2  Shutdown   Shutdown
# myapp_api.2  myapp/api:3.3.0  node2  Running    Running   5 seconds ago

Monitoring Update Progress

# Check update status
docker service inspect myapp_api --format '{{.UpdateStatus}}'

# Possible states:
# - "updating": update in progress
# - "completed": update finished successfully
# - "paused": update paused (manual intervention needed)
# - "rollback_started": failure detected, rollback initiated
# - "rollback_completed": rollback finished

Instant Rollback

If an update causes issues, you can roll back to the previous version instantly:

# Rollback the API service to its previous specification
docker service rollback myapp_api

# This uses the service's stored previous spec
# Verify rollback completed
docker service inspect myapp_api --format '{{.UpdateStatus}}'

# Force rollback even if update status shows "completed"
docker service rollback --rollback myapp_api

Configuring Update Behavior Per Service

Fine-tune update parameters for each service:

deploy:
  update_config:
    parallelism: 2        # Update 2 containers at a time
    delay: 30s            # Wait 30s between batches
    order: start-first    # Start new before stopping old
    failure_action: rollback  # Auto-rollback on failure
    monitor: 60s          # Wait 60s after update to verify health
    max_failure_ratio: 0.3  # Rollback if >30% of containers fail

Secrets Management in Stacks

Docker Swarm secrets are encrypted during transit and at rest. Only services explicitly granted access can read them, and secrets are mounted as files at /run/secrets/ inside containers.

Creating and Managing Secrets

# Create a secret from a file
docker secret create db_password ./db_password.txt

# Create a secret from stdin (avoids shell history leaks)
printf "my-secret-password" | docker secret create db_password -

# Create a secret with labels for organization
docker secret create \
  --label env=production \
  --label app=myapp \
  api_token ./api_token.txt

# List secrets
docker secret ls

# Inspect secret metadata (content is not shown)
docker secret inspect db_password

# Remove a secret (only if no running services use it)
docker secret rm db_password

Using Secrets in Services

Secrets are mounted as files in /run/secrets/<secret_name>. Your application reads the secret from this file:

# In docker-compose.yml
services:
  api:
    secrets:
      - db_password
      - api_token
    environment:
      - DB_PASSWORD_FILE=/run/secrets/db_password
      - API_TOKEN_FILE=/run/secrets/api_token

# In your application code (Node.js example)
const fs = require('fs');
const dbPassword = fs.readFileSync('/run/secrets/db_password', 'utf8').trim();
const apiToken = fs.readFileSync('/run/secrets/api_token', 'utf8').trim();

Secret Rotation Strategy

Secrets are immutable once created. To rotate, create a new secret version and update the service:

# Create new secret version
echo "new-password-v3" | docker secret create db_password_v3 -

# Update service to use new secret
docker service update \
  --secret-rm db_password_v2 \
  --secret-add db_password_v3 \
  myapp_api

# After all services migrate, remove old secret
docker secret rm db_password_v2

Networking in Stack Deployments

Docker Stack automatically creates overlay networks that span all Swarm nodes, enabling containers to communicate across physical machines.

Network Design Patterns

A common pattern is the three-tier network architecture:

networks:
  ingress:
    driver: overlay
    # Public-facing, connects to external load balancer
  application:
    driver: overlay
    internal: false
    # Service-to-service communication within the app
  data:
    driver: overlay
    internal: true  # No internet access for security
    # Database and cache communication only

The internal: true option prevents containers on that network from accessing the internet, adding a security boundary for sensitive data services.

Connecting Services Across Stacks

By default, networks are scoped to a stack. To allow cross-stack communication, create an externally managed network:

# Create a shared network before deploying stacks
docker network create \
  --driver overlay \
  --attachable \
  shared_monitoring

# In stack A's Compose file
networks:
  monitoring:
    external: true
    external_name: shared_monitoring

# In stack B's Compose file
networks:
  monitoring:
    external: true
    external_name: shared_monitoring

# Now services in both stacks can communicate over 'monitoring'

Ingress Routing Mesh

When you publish a port with mode: ingress, Docker's routing mesh distributes incoming requests across all service replicas, regardless of which node receives the request. This provides built-in load balancing without external configuration:

# Port published in ingress mode (default)
ports:
  - target: 80
    published: 80
    mode: ingress

# Every node in the swarm listens on port 80
# Requests are routed to healthy replicas transparently

Health Checks and Service Resilience

Health checks are critical for production. They tell Docker whether a container is actually functional, not just running. Swarm uses health status for routing decisions and update monitoring.

# HTTP health check for web services
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
  interval: 30s
  timeout: 5s
  retries: 3
  start_period: 30s  # Grace period before health checks begin

# Database health check
healthcheck:
  test: ["CMD-SHELL", "pg_isready -U appuser -d appdb || exit 1"]
  interval: 10s
  timeout: 5s
  retries: 5

# Custom script health check
healthcheck:
  test: ["CMD", "/usr/local/bin/health-check.sh"]
  interval: 60s
  timeout: 10s
  retries: 3
  start_period: 120s  # Long startup for JVM applications

During rolling updates, Docker waits for the health check to pass on new containers before considering them healthy and proceeding with the next batch. If health checks fail within the monitor window, the update is marked as failed, triggering a rollback if configured.

Best Practices for Production Stack Deployments

1. Use Specific Image Tags

Never use latest tags in production. Always pin to specific versions or digests to ensure reproducible deployments:

# Bad - unpredictable
image: myapp/api:latest

# Good - specific version
image: myapp/api:3.2.1

# Better - immutable digest
image: myapp/api@sha256:abc123def456...

2. Implement Resource Constraints Everywhere

Every service should have CPU and memory limits to prevent noisy neighbors:

deploy:
  resources:
    limits:
      cpus: '1.0'
      memory: 512M
    reservations:
      cpus: '0.25'
      memory: 128M

3. Use Node Labels for Workload Placement

Tag nodes by capability (compute-optimized, memory-optimized, GPU, database-storage) and constrain services accordingly:

# Label nodes
docker node update --label-add tier=compute node1
docker node update --label-add tier=storage node2

# Constrain services
deploy:
  placement:
    constraints:
      - node.labels.tier == compute

4. Design for Statelessness

Stateless services scale horizontally and update gracefully. Keep state in dedicated services (databases, caches, object stores) and mount volumes only where absolutely necessary. For stateful services that need volumes, use placement constraints to pin them to specific nodes with local storage.

5. Use External Secrets with Versioning

Create secrets externally and reference them by versioned names. This enables rotation without service disruption:

secrets:
  db_password:
    external: true
    external_name: myapp_db_password_v3

6. Configure Comprehensive Health Checks

Health checks should validate actual application functionality, not just process existence. Include database connectivity checks, cache availability checks, and endpoint responses:

healthcheck:
  test: ["CMD-SHELL", "curl -f http://localhost:3000/health/database && curl -f http://localhost:3000/health/cache"]
  interval: 30s
  timeout: 10s
  retries: 3

7. Test Updates in a Staging Swarm

Before deploying to production, validate your Compose file changes in a staging Swarm that mirrors production topology. Test rolling updates, rollbacks, and failure scenarios:

# Deploy to staging
docker stack deploy -c docker-compose.yml myapp-staging \
  --with-registry-auth

# Simulate update failure by using a non-existent image
# Verify rollback behavior works correctly

8. Use Configs for Non-Secret Configuration

Separate secrets from configuration. Use Docker configs for nginx configs, application config files, and other non-sensitive configuration:

configs:
  nginx_config:
    file: ./nginx/nginx.conf

services:
  frontend:
    configs:
      - source: nginx_config
        target: /etc/nginx/nginx.conf
        mode: 0444

9. Monitor Stack State Continuously

Integrate stack status monitoring into your observability pipeline:

# Script for monitoring stack health
docker stack services myapp --format "{{.Name}}: {{.Replicas}}" \
  | grep -v "1/1\|3/3\|4/4"  # Alert if any service isn't fully replicated

# Check for failed containers
docker stack ps myapp --filter "desired-state=running" \
  --filter "status=failed" --format "{{.Name}} {{.Error}}"

10. Keep Compose Files Version Controlled

Treat Compose files like infrastructure-as-code. Store them in Git, review changes via pull requests, and tag versions that correspond to deployed states. Include the Compose file version in your deployment pipeline metadata.

Common Troubleshooting Scenarios

Service Stuck in "Pending" State

If a service replica shows Pending indefinitely, check resource constraints and node availability:

# Inspect the service for scheduling issues
docker service ps myapp_api --filter "desired-state=running" --no-trunc

# Check node resources
docker node inspect worker1 --format '{{.Status.State}} {{.Description.Resources}}'

# Common causes:
# - No node meets placement constraints
# - Insufficient CPU/memory reservations on available nodes
# - Node in "drain" or "pause" availability mode

Rolling Update Not Progressing

If an update hangs, check the health status of new containers:

# Check container health
docker inspect $(docker service ps myapp_api --filter "desired-state=running" \
  -q --format "{{.Name}}.{{.ID}}" | head -1) \
  --format '{{.State.Health.Status}}'

# If health checks fail, the update pauses
# You can manually rollback or fix the issue
docker service rollback myapp_api

Network Connectivity Issues

Verify overlay network health and DNS resolution:

# Test DNS resolution within a container
docker exec $(docker ps --filter "name=myapp_api" -q | head -1) \
  nslookup myapp_postgres

# Check network list
docker network ls --filter "driver=overlay"

# Inspect network for connected containers
docker network inspect myapp_backend

Conclusion

Docker Stack Deployments transform your Compose-defined applications into production-grade, self-healing distributed systems running on a Swarm cluster. By mastering the deployment directives—replication, rolling updates, resource constraints, placement rules, secrets, and health checks—you gain precise control over how your services behave at scale. The declarative model ensures your infrastructure continuously converges toward your desired state, while features like automatic rollbacks and the ingress routing mesh provide resilience without complex external tooling. Start with a well-structured Compose file, apply the best practices outlined here, test thoroughly in staging, and you'll have a repeatable, version-controlled deployment process that scales from a single node to a multi-datacenter Swarm with confidence.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles