Docker in Production: Best Practices We Follow on Every Project
Docker has become the standard for packaging and deploying applications. But there is a significant gap between running a container locally and running one reliably in production. At developersEra, every application we build ships in a Docker container, and over dozens of deployments we have developed a set of practices that we follow without exception.
Always Use Multi-Stage Builds
A common mistake is shipping build tools, source code, and development dependencies inside production images. This bloats the image, increases the attack surface, and slows down deployments.
Multi-stage builds solve this cleanly. The first stage compiles or builds the application. The second stage copies only the compiled output into a minimal base image.
# Build stage
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM nginx:1.27-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
The result is a production image that contains only what is needed to run -- nothing more.
Never Run as Root
By default, containers run as root. This means if an attacker exploits a vulnerability in your application, they have root access inside the container. While container isolation limits the blast radius, running as a non-root user adds a critical layer of defense.
RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup
USER appuser
We apply this to every Dockerfile we write, without exception.
Always Include Health Checks
Without a health check, your orchestrator has no way to know if your application is actually working. A container can be "running" while the application inside has crashed, is deadlocked, or is returning errors.
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget -q --spider http://localhost:8080/actuator/health || exit 1
For Spring Boot applications, we use the Actuator health endpoint. For other stacks, a simple HTTP check against a known endpoint works.
Set Resource Limits
Containers without resource limits can consume all available memory or CPU on the host, affecting other containers and potentially crashing the entire server.
services:
app:
image: myapp:latest
deploy:
resources:
limits:
memory: 512M
cpus: "1.0"
reservations:
memory: 256M
cpus: "0.5"
We set both limits and reservations on every service. Limits prevent runaway consumption. Reservations ensure the container has guaranteed minimum resources.
Use Specific Image Tags
Using latest or untagged images in production is a recipe for unpredictable deployments. The same docker-compose up command can produce different results on different days.
We always pin to specific versions:
nginx:1.27-alpineinstead ofnginx:latestpostgres:16.2-alpineinstead ofpostgres- Our own images are tagged with the git commit SHA
Restart Policies and Logging
Every production container gets a restart policy. For most services, unless-stopped is the right choice -- it restarts automatically after crashes or host reboots, but respects manual stops.
For logging, we configure structured JSON output and set log rotation to prevent disk exhaustion:
services:
app:
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
Environment Variables for Configuration
We never bake configuration into Docker images. Everything that varies between environments -- database URLs, API keys, feature flags -- comes from environment variables or mounted config files.
services:
app:
env_file:
- .env
environment:
- SPRING_PROFILES_ACTIVE=production
This makes the same image deployable to staging and production without rebuilding.
The Compound Effect
None of these practices are individually complex. But applied consistently across every container in a system, they compound into infrastructure that is secure, predictable, and easy to operate. When something goes wrong at 2 AM, these practices are the difference between a quick recovery and a long night.
At developersEra, these are not aspirational guidelines. They are the baseline for every project we ship. Docker practices are half the picture -- the other half is monitoring and observability from day one.
Need help building something like this?
We build production-grade systems. Let's talk about your project.
Start a Conversation →