DockerDevOpsInfrastructure

Docker in Production: Best Practices We Follow on Every Project

developersEra Team|2026-03-18|8 min read

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-alpine instead of nginx:latest
  • postgres:16.2-alpine instead of postgres
  • 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 →