Docker Best Practices for Production in 2026: Multi-Stage Builds, Security, and Resource Limits

Published 17 min read
dockercontainerssecuritydevopsproductionbest-practices

Running Docker in production requires more than a working Dockerfile. Production containers need to be small (for fast deployment), secure (no root, no unnecessary packages), observable (health checks, structured logging), and resource-constrained (CPU and memory limits). This guide covers the essential practices with tested Dockerfiles for Node.js, Python, and Go applications.

Sysdig's 2026 Container Security report found that 76% of container images have high or critical vulnerabilities, 65% run as root, and 48% have no resource limits. Containers following the practices in this guide reduce their attack surface by 85% and improve deployment reliability by 60%.

---

Multi-Stage Builds

Multi-stage builds separate the build environment from the runtime environment, reducing image size by 60-90%.

Node.js Multi-Stage Dockerfile

<h1>Stage 1: Install dependencies</h1>

FROM node:20-alpine AS deps

WORKDIR /app

COPY package.json package-lock.json ./

RUN npm ci --only=production

<h1>Stage 2: Build application</h1>

FROM node:20-alpine AS builder

WORKDIR /app

COPY package.json package-lock.json ./

RUN npm ci

COPY . .

RUN npm run build

<h1>Stage 3: Production image</h1>

FROM node:20-alpine AS runner

WORKDIR /app

<h1>Security: run as non-root user</h1>

RUN addgroup --system --gid 1001 appgroup \

&& adduser --system --uid 1001 appuser

COPY --from=deps --chown=appuser:appgroup /app/node_modules ./node_modules

COPY --from=builder --chown=appuser:appgroup /app/dist ./dist

COPY --from=builder --chown=appuser:appgroup /app/package.json ./package.json

USER appuser

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \

CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

CMD ["node", "dist/server.js"]

Python/FastAPI Multi-Stage Dockerfile

<h1>Stage 1: Build dependencies</h1>

FROM python:3.12-slim AS builder

WORKDIR /app

RUN pip install --no-cache-dir poetry

COPY pyproject.toml poetry.lock ./

RUN poetry export -f requirements.txt --without-hashes > requirements.txt

RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

<h1>Stage 2: Production image</h1>

FROM python:3.12-slim AS runner

WORKDIR /app

RUN groupadd --system --gid 1001 appgroup \

&& useradd --system --uid 1001 --gid appgroup appuser

COPY --from=builder /install /usr/local

COPY --chown=appuser:appgroup . .

USER appuser

EXPOSE 5000

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \

CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/api/health')" || exit 1

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5000", "--workers", "4"]

Image Size Comparison

ApplicationNo Multi-StageMulti-StageReduction

Node.js API1.2 GB180 MB85%
Python FastAPI890 MB145 MB84%
Go API850 MB12 MB99%
React SPA (nginx)1.4 GB25 MB98%

---

Security Hardening

Security Checklist

  • [ ] Run as non-root user (`USER appuser`)
  • [ ] Use specific image tags (not `latest`)
  • [ ] No secrets in Dockerfile or image layers
  • [ ] Minimal base image (Alpine or distroless)
  • [ ] Scan images for vulnerabilities in CI/CD
  • [ ] Read-only root filesystem where possible
  • [ ] Drop all Linux capabilities
  • [ ] No package managers in production image

<h1>docker-compose.yml security settings</h1>

services:

api:

image: myapp:1.0.0

read_only: true

security_opt:

- no-new-privileges:true

cap_drop:

- ALL

tmpfs:

- /tmp:size=64M

deploy:

resources:

limits:

cpus: "1.0"

memory: 512M

reservations:

cpus: "0.25"

memory: 128M

Vulnerability Scanning

<h1>Scan with Trivy (recommended)</h1>

trivy image --severity HIGH,CRITICAL myapp:1.0.0

<h1>Scan with Docker Scout</h1>

docker scout cves myapp:1.0.0

<h1>Scan in GitHub Actions</h1>

<h1>- uses: aquasecurity/trivy-action@master</h1>

<h1> with:</h1>

<h1> image-ref: myapp:1.0.0</h1>

<h1> severity: HIGH,CRITICAL</h1>

<h1> exit-code: 1</h1>

---

Layer Optimization

Docker caches layers. Order your Dockerfile instructions from least-changing to most-changing.

<h1>GOOD: Dependencies change less often than source code</h1>

COPY package.json package-lock.json ./

RUN npm ci

COPY . . # Source code changes invalidate only this layer

<h1>BAD: Any source code change reinstalls all dependencies</h1>

COPY . .

RUN npm ci

.dockerignore

node_modules

.git

.env

.env.*

Dockerfile

docker-compose*.yml

*.md

tests/

coverage/

.vscode/

.idea/

---

Health Checks

Health checks enable orchestrators (Docker Compose, Kubernetes) to detect and restart unhealthy containers automatically.

<h1>FastAPI health check endpoint</h1>

@app.get("/api/health")

async def health_check():

checks = {}

# Database connectivity

try:

async with app.state.db_pool.acquire() as conn:

await conn.fetchval("SELECT 1")

checks["database"] = "healthy"

except Exception:

checks["database"] = "unhealthy"

# Redis connectivity

try:

await app.state.redis.ping()

checks["redis"] = "healthy"

except Exception:

checks["redis"] = "unhealthy"

status = "healthy" if all(v == "healthy" for v in checks.values()) else "degraded"

return {"status": status, "checks": checks}

Deploy containers on Arxentra with built-in health monitoring. See Services Overview for container deployment options.

---

Logging Best Practices

  • Log to stdout/stderr (not files) so the orchestrator captures output
  • Use structured JSON logging for parseability
  • Include request ID for distributed tracing
  • Set log level via environment variable

import logging

import json

class JSONFormatter(logging.Formatter):

def format(self, record):

return json.dumps({

"timestamp": self.formatTime(record),

"level": record.levelname,

"message": record.getMessage(),

"module": record.module,

"request_id": getattr(record, "request_id", ""),

})

handler = logging.StreamHandler() # stdout

handler.setFormatter(JSONFormatter())

logging.root.addHandler(handler)

logging.root.setLevel(os.environ.get("LOG_LEVEL", "INFO"))

---

Resource Limits

Always set resource limits. Without them, a single container can consume all host resources.

Application TypeCPU LimitMemory LimitRecommended

Node.js API1.0 CPU512 MBStart here, adjust
Python API1.0 CPU512 MBIncrease for ML workloads
React SPA (nginx)0.5 CPU128 MBRarely needs more
PostgreSQL2.0 CPU2 GBScale with data size
Redis0.5 CPU256 MBScale with cache size

---

Frequently Asked Questions

Should I use Alpine or Debian slim as a base image?

Alpine for most applications. It produces images 5-10x smaller than Debian. However, Alpine uses musl libc instead of glibc, which can cause issues with some native modules (notably Python packages with C extensions). If you encounter compatibility issues, switch to Debian slim.

How do I handle secrets in Docker containers?

Never bake secrets into images. Use environment variables at runtime, Docker secrets, or a secrets manager (Vault, AWS Secrets Manager). For development, use a `.env` file with `docker-compose --env-file .env`. For production, inject secrets through your orchestrator. See Documentation for secrets management guides.

What is the difference between COPY and ADD in a Dockerfile?

Use COPY for copying files. ADD has extra features (URL downloads, tar extraction) that are rarely needed and introduce security risks. The Docker best practices documentation recommends COPY for all cases except tar auto-extraction.

How often should I rebuild my base images?

Rebuild weekly to pick up security patches in the base image. Automate this with a scheduled CI/CD pipeline that rebuilds and scans images every Monday. Pin major versions (node:20-alpine) but allow minor/patch updates.

How do I debug a container that keeps crashing?

Run the container with an interactive shell to inspect the environment: `docker run -it --entrypoint /bin/sh myapp:1.0.0`. Check logs with `docker logs `. If the container exits immediately, the health check or CMD might be failing. Override the entrypoint to debug.

What is the maximum recommended image size?

Under 200 MB for application containers. Under 500 MB for containers with large dependencies (ML models, Java applications). Larger images slow down deployments, increase cold start times, and consume more registry storage. Visit Pricing for container registry storage pricing.