Getting a Docker container running locally is easy. Getting it running safely in production โ with proper configuration, health checks, and zero-downtime updates โ takes a few more steps. This guide walks through deploying a containerized application to production the right way in 2026, from a production-ready image to a reverse proxy and updates that do not drop traffic.
๐ Table of Contents
- Step 1: Write a Production-Ready Dockerfile
- Step 2: Handle Configuration with Environment Variables
- Step 3: Add a Health Check
- Step 4: Use Docker Compose for a Clean Setup
- Step 5: Put a Reverse Proxy in Front
- Step 6: Deploy Updates Without Downtime
- Step 7: Logging and Monitoring
- Frequently Asked Questions
- Conclusion
Step 1: Write a Production-Ready Dockerfile
Start with a multi-stage build so your final image contains only what it needs to run โ not build tools and dev dependencies. Smaller images pull faster, start faster, and have a smaller attack surface.
# Build stage
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Runtime stage
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
Note the USER node line โ never run production containers as root. Running as a non-privileged user limits damage if the app is compromised.
Step 2: Handle Configuration with Environment Variables
Never bake secrets into your image. Pass configuration and secrets at runtime through environment variables or a secrets manager. Your image should be identical across staging and production; only the environment differs.
docker run -d \
--name myapp \
--env-file /etc/myapp/production.env \
--restart unless-stopped \
-p 3000:3000 \
myapp:1.4.0
Keep the .env file off the image and out of version control. On a single host, an env file with locked-down permissions works; at scale, use Docker secrets, AWS Secrets Manager, or your platform’s secret store.
Step 3: Add a Health Check
A health check lets Docker (and your orchestrator or reverse proxy) know whether the container is actually ready to serve traffic, not just running. Add an endpoint like /health to your app, then declare the check.
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
With this in place, docker ps shows a healthy or unhealthy status, and orchestrators can avoid routing traffic to containers that are still warming up or have gone bad.
Step 4: Use Docker Compose for a Clean Setup
Even on a single server, Compose makes your deployment reproducible and self-documenting. It captures the image, ports, environment, restart policy, and dependencies in one file.
services:
app:
image: myapp:1.4.0
restart: unless-stopped
env_file: /etc/myapp/production.env
expose:
- "3000"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 30s
timeout: 3s
retries: 3
depends_on:
- db
db:
image: postgres:17-alpine
restart: unless-stopped
env_file: /etc/myapp/db.env
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Note the named volume for Postgres โ container storage is ephemeral, so any data you must keep belongs in a volume, never inside the container’s writable layer.
Step 5: Put a Reverse Proxy in Front
Do not expose your app’s port directly to the internet. Put a reverse proxy (Nginx, Caddy, or Traefik) in front to terminate TLS, add security headers, and route traffic. Caddy is popular because it obtains and renews HTTPS certificates automatically.
# Caddyfile
myapp.example.com {
reverse_proxy app:3000
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options nosniff
}
}
The proxy handles HTTPS and headers while your app focuses on serving requests over plain HTTP inside the private Docker network.
Step 6: Deploy Updates Without Downtime
The naive update โ stop the old container, start the new one โ drops traffic during the gap. For zero-downtime updates, start the new container, wait for it to pass health checks, then switch the proxy and remove the old one. Traefik and orchestrators automate this; on a simple setup you can run two versions briefly and cut over.
# Pull the new version and recreate with Compose
docker compose pull app
docker compose up -d --no-deps app
# Compose starts the new container; verify health before removing old
Always tag images with a specific version (myapp:1.4.0), never rely on latest in production. Explicit tags make rollbacks trivial โ you know exactly which version to redeploy.
Step 7: Logging and Monitoring
Configure a log driver so logs go somewhere durable, and cap log size so a container cannot fill the disk. Ship logs to a central place (Loki, CloudWatch, or an ELK stack) for anything beyond a hobby project.
docker run -d \
--log-driver json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
myapp:1.4.0
Frequently Asked Questions
Q: Should I run my database in a container in production?
A: For small projects, yes โ with a named volume for persistence and regular backups. For anything critical or high-scale, a managed database (RDS, Cloud SQL) reduces operational risk considerably.
Q: Why not just expose the app port directly?
A: You would lose TLS termination, security headers, and a single point to manage routing. A reverse proxy also lets you run multiple apps on one host and handle certificates centrally.
Q: How do I keep secrets out of the image?
A: Pass them at runtime via environment variables, an env file with locked permissions, or a secrets manager. Never COPY a secrets file into the image or hardcode credentials in the Dockerfile.
Q: What restart policy should I use?
A: unless-stopped for most services โ it restarts the container on failure and after a host reboot, but respects a manual stop.
Q: How do I roll back a bad deploy?
A: Because you tagged images by version, redeploy the previous tag (docker compose up -d with the old version). This is why latest is dangerous in production โ it makes rollbacks ambiguous.
Conclusion
Deploying Docker to production safely comes down to a handful of disciplines: build a lean multi-stage image running as a non-root user, inject config and secrets at runtime, add health checks, front the app with a reverse proxy for TLS, tag images by version for clean rollbacks, and cap your logs. Capture it all in a Compose file so the setup is reproducible. Get these fundamentals right and your containerized app will run reliably, update without dropping traffic, and be easy to roll back when something goes wrong.
๐ You might also like
๐ Share this article




โ๏ธ Leave a Comment