← All Articles
DevOps9 min read

Docker in Production: 12 Best Practices We Apply on Every Project

April 5, 20259 min read

Docker is deceptively easy to get started with. A Dockerfile with five lines and you have a running container. But production environments expose every shortcut, every assumption, every security gap. Here's what we actually do.

1. Multi-stage builds — always

A Node.js application doesn't need its build toolchain in production. Use a build stage with all dev dependencies, then copy only the compiled output into a minimal runtime image. This reduces image size by 60–80% and eliminates an entire category of vulnerabilities from build tools that never needed to be in production.

2. Pin base image versions

FROM node:latest is a liability. Pin to a specific digest: FROM node:20.11.0-alpine3.19. Use Dependabot to automate updates with controlled testing.

3. Run as non-root

By default, containers run as root. If an attacker escapes the container, they have root on the host. Add USER node to your Dockerfile and ensure your application doesn't require root privileges to run.

4. Read-only filesystem

Mount the container filesystem as read-only. If your application doesn't need to write to the filesystem (it shouldn't — use object storage or volumes), make it impossible. Add --read-only to your run command or set readOnlyRootFilesystem: true in your Kubernetes pod spec.

5. Drop all capabilities

Linux capabilities give containers access to privileged kernel features. Drop all capabilities and add back only what you need: cap_drop: ALL, then cap_add: [NET_BIND_SERVICE] only if you need to bind to ports below 1024 (you probably don't — use port mapping instead).

6. Scan images for vulnerabilities

Integrate Trivy or Grype into your CI pipeline. Fail builds on critical vulnerabilities. Scan both your base image and your final image. Keep a policy — we fail on CRITICAL, warn on HIGH, track MEDIUM.

7. Layer caching strategy

Order Dockerfile instructions from least-changed to most-changed. Copy package.json and run npm install before copying application code. This ensures dependency layers are cached and only rebuild when dependencies change.

8. Health checks

Define a HEALTHCHECK in your Dockerfile. Orchestrators use this to determine container health and route traffic only to healthy instances. A simple HTTP check on your health endpoint is sufficient.

9. Resource limits

Always set memory and CPU limits. Without limits, a single misbehaving container can starve all other containers on the host. Set limits at 80% of what you've observed the container actually needs under load.

10. Use .dockerignore aggressively

Exclude node_modules, .git, .env files, and test directories from the build context. A large build context slows builds and risks leaking secrets into the image.

11. Immutable image tags

Never overwrite an image tag in production. Tag with the git commit SHA. myapp:a3f9bc2 is immutable and traceable. myapp:latest is not.

12. Log to stdout

Containers should not manage their own log files. Write all logs to stdout/stderr and let the container runtime and your logging infrastructure handle collection. This enables centralised log aggregation without any application-level configuration.

GET STARTED

Ready to build
something exceptional?

From idea to launch in weeks, not months. Let's talk about your project.