Deploy Basics
The simple deployment pattern
Section titled “The simple deployment pattern”The most straightforward deployment: SSH to your server, pull the new image, and start a container.
# On the deployment serverdocker pull acme/api-server:2.1.0docker stop api-server || truedocker rm api-server || truedocker run -d \ --name api-server \ --restart unless-stopped \ -p 3000:3000 \ -e DATABASE_URL="$DATABASE_URL" \ acme/api-server:2.1.0The || true prevents the pipeline from failing if the container does not yet exist on a first deploy.
Restart policies
Section titled “Restart policies”The --restart flag tells Docker what to do if the container exits:
| Policy | Behaviour |
|---|---|
no (default) | Never restart |
on-failure | Restart only on non-zero exit |
always | Always restart, including after a Docker daemon restart (a manual docker stop stays stopped until then) |
unless-stopped | Like always but does not restart if you explicitly docker stop it |
For production services, use unless-stopped. It survives host reboots and daemon restarts but respects intentional stops.
Rolling updates with Docker Compose
Section titled “Rolling updates with Docker Compose”If your server already runs services with Compose v2, updates are a two-command operation:
# Pull all updated images defined in compose.ymldocker compose pull
# Recreate containers that have a newer imagedocker compose up -dCompose recreates only the services whose image digest has changed, leaving unchanged services running. If you use a compose.yml with pinned image tags:
services: api: image: acme/api-server:2.1.0 restart: unless-stopped ports: - "3000:3000" env_file: - .envChange the tag in compose.yml and run docker compose up -d — Compose detects the tag change and recreates the service.
Environment variables and secrets on the host
Section titled “Environment variables and secrets on the host”Never bake secrets into your image. Instead, pass them at runtime.
Three patterns, from simplest to most robust:
1. Environment variables via -e
Section titled “1. Environment variables via -e”docker run -d \ -e DATABASE_URL="postgres://user:pass@db:5432/app" \ -e SECRET_KEY="abc123" \ acme/api-server:2.1.0Simple, but the values appear in docker inspect output and shell history.
2. Environment file via --env-file
Section titled “2. Environment file via --env-file”# .env (not committed to git)DATABASE_URL=postgres://user:pass@db:5432/appSECRET_KEY=abc123docker run -d --env-file .env acme/api-server:2.1.0Cleaner than -e flags. Keep the .env file in a secrets manager or vault on the server.
3. Docker secrets (Swarm / Compose)
Section titled “3. Docker secrets (Swarm / Compose)”For teams running Docker Swarm or Compose with the secrets: block, secrets are mounted as files at /run/secrets/<name> inside the container. This is the most secure option — secrets are never exposed as environment variables.
services: api: image: acme/api-server:2.1.0 secrets: - db_password
secrets: db_password: file: ./db_password.txtVerifying the deployment
Section titled “Verifying the deployment”After pulling and restarting, confirm the new version is running:
docker psdocker logs api-server --tail 20docker inspect api-server | grep ImageHands-on: pull and run a public image with a restart policy
Section titled “Hands-on: pull and run a public image with a restart policy”The snippet below demonstrates docker pull, docker run -d --restart unless-stopped, and log inspection — using a public image so no credentials are needed.
# Pull a versioned nginx image
docker pull nginx:1.31-alpine
# Run it detached with a restart policy
docker run -d \
--name web \
--restart unless-stopped \
-p 8080:80 \
nginx:1.31-alpine
# Verify it is running
docker ps
# Check logs
docker logs web --tail 5
# Simulate an update: pull a different tag and recreate
docker pull nginx:1.30-alpine
docker stop web && docker rm web
docker run -d \
--name web \
--restart unless-stopped \
-p 8080:80 \
nginx:1.30-alpine
docker psTrade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
Deploy with docker run directly | Easy to understand, full control over every step | No automatic rollback — you have to script checks and rollback yourself |
Deploy with docker compose up -d | Declarative, handles several services at once, recreates only what changed | Still not full orchestration — no health-based rollback like Kubernetes has |
Common mistakes
Section titled “Common mistakes”- Deploying a new image version to production without ever running a vulnerability scan on it first.
- Pulling and restarting immediately without verifying the container actually comes up healthy before promoting to the next environment.
- Pushing and deploying only the
latesttag, leaving no pinned version to roll back to when a deploy goes wrong.
💡 Real-world example
Many teams use Trivy (an open-source scanner from Aqua Security, github.com/aquasecurity/trivy) as a gate in the pipeline before the deploy step — if a critical CVE is found in the image, it blocks the deploy automatically, instead of letting a vulnerable image reach production.