Docker
DockerIntermediate

Docker Best Practices: Building Efficient and Secure Containers

3 min read
dockercontainerssecuritydevops

TL;DR

Learn essential Docker best practices for building efficient, secure, and maintainable container images, including multi-stage builds, security scanning, and optimization techniques.

Docker Best Practices: Building Efficient and Secure Containers

Docker containers have revolutionized how we package and deploy applications. However, building container images that are both efficient and secure requires following established best practices. This guide covers essential techniques for creating production-ready Docker containers.

$1

Creating efficient Docker images is crucial for faster deployments, reduced storage costs, and improved security. Multi-stage builds and proper layer caching are key techniques for achieving these goals.

``mermaid

graph TD

A[Source Code] --> B[Build Stage]

B --> C[Dependencies]

C --> D[Compile]

D --> E[Final Stage]

E --> F[Runtime Deps]

F --> G[Application]

style A fill:#f96,stroke:#333

style D fill:#9cf,stroke:#333

style G fill:#9f9,stroke:#333

`

$1

`dockerfile

Build stage

FROM node:18-alpine AS builder

WORKDIR /app

COPY package*.json ./

RUN npm ci

COPY . .

RUN npm run build

Production stage

FROM node:18-alpine

WORKDIR /app

COPY --from=builder /app/dist ./dist

COPY --from=builder /app/package*.json ./

RUN npm ci --only=production

EXPOSE 3000

CMD ["npm", "start"]

`

$1

Security should be a primary concern when building Docker images. Following these practices helps minimize vulnerabilities and protect your applications.

| Practice | Description | Impact |

|----------|-------------|---------|

| Minimal Base Image | Use slim/alpine variants | Reduced attack surface |

| Non-root User | Run as non-privileged user | Enhanced security |

| Security Scanning | Regular vulnerability scans | Early detection |

| Update Dependencies | Keep packages current | Patch vulnerabilities |

$1

`dockerfile

Use specific version for reproducibility

FROM node:18.19.0-alpine3.18

Add non-root user

RUN addgroup -S appgroup && adduser -S appuser -G appgroup

Set working directory and permissions

WORKDIR /app

COPY --chown=appuser:appgroup . .

Install dependencies

RUN npm ci --only=production

Use non-root user

USER appuser

Define health check

HEALTHCHECK --interval=30s --timeout=3s \

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

EXPOSE 3000

CMD ["npm", "start"]

`

$1

Understanding Docker layer caching and optimizing layer order can significantly improve build times and reduce image size.

`mermaid

flowchart TB

subgraph "Layer Optimization"

A[Base Image] --> B[System Dependencies]

B --> C[Application Dependencies]

C --> D[Application Code]

D --> E[Configuration]

end

style A fill:#f96,stroke:#333

style C fill:#9cf,stroke:#333

style E fill:#9f9,stroke:#333

`

$1

1. Place infrequently changing layers first

2. Combine related commands in single layers

3. Use .dockerignore effectively

4. Clean up in the same layer as installation

Example .dockerignore:

`text

.git

node_modules

npm-debug.log

Dockerfile

.dockerignore

.env

*.md

`

$1

Proper container runtime configuration is essential for production deployments. This includes resource limits, logging, and monitoring setup.

`yaml

version: '3.8'

services:

app:

image: myapp:latest

deploy:

resources:

limits:

cpus: '0.50'

memory: 512M

reservations:

cpus: '0.25'

memory: 256M

logging:

driver: "json-file"

options:

max-size: "10m"

max-file: "3"

healthcheck:

test: ["CMD", "curl", "-f", "http://localhost:3000/health"]

interval: 30s

timeout: 10s

retries: 3

`

$1

Establishing a consistent development workflow helps teams work efficiently with Docker.

`mermaid

graph LR

A[Local Development] -->|Docker Compose| B[Testing]

B -->|CI/CD| C[Staging]

C -->|Promotion| D[Production]

style A fill:#f96,stroke:#333

style B fill:#9cf,stroke:#333

style D fill:#9f9,stroke:#333

`

$1

Example docker-compose.yml for development:

`yaml

version: '3.8'

services:

app:

build:

context: .

target: development

volumes:

- .:/app

- /app/node_modules

ports:

- "3000:3000"

environment:

- NODE_ENV=development

command: npm run dev

``

Why This Matters

Understanding the business and technical context helps you make informed decisions rather than blindly following patterns.

Trade-offs to Consider

Every architectural decision involves trade-offs. Consider your specific requirements, team expertise, and scale when evaluating options.

When NOT to Use This

Knowing when a solution doesn't apply is as valuable as knowing when it does. Consider alternatives for your specific situation.

Decision Framework

Use this framework to evaluate whether this approach is right for your use case based on your specific constraints and requirements.