12 Factor Docker Containers

Containerization has become increasingly popular in the tech industry, offering an efficient way to package, distribute, and manage applications. Technologies like Docker & Kubernetes, at the forefront of this revolution, provide the tools necessary to build & run containerized applications. However, creating containers can vary greatly in efficiency, security, and maintainability. Inspired by the 12-factor application methodology principles, I crafted this guide to writing Dockerfiles & building containers that stand the test of scalability and robustness. Here’s how you can apply these principles.

mindmap ("12Factor Docker Containers") ("1 Single Responsibility") ("2 Version Control") ("3 Minimal Base Images") ("4 Explicit Dependencies") ("5 Environment Abstraction") ("6 Layer Efficiency") ("7 Security Practices") ("8 Non-Privileged User") ("9 Health Checks") ("10 Build Context Minimization") ("11 Documentation and Comments") ("12 One-Time Commands")

Factor 1: Single Responsibility

Modular Containers Each Container should focus on a single concern. Avoid the temptation to bundle multiple services or applications into one container. This not only makes your container lightweight but also simplifies management and scaling.


# Good: Focuses on one application (Node.js app)
FROM node:14
COPY . /app
RUN npm install
CMD ["node", "app.js"]

Factor 2: Version Control

Track Your Dockerfiles Treat Dockerfiles as part of your source code. Store them in your version control system alongside the application they deploy. This practice ensures synchronization between your application’s evolution and its deployment environment.


# Example of version control commands
git add Dockerfile
git commit -m "Add Dockerfile for our app"
git push origin main

Factor 3: Minimal Base Images

Opt for Slim Start with the smallest base image possible that still provides the functionality your application needs. Images like Alpine or slim variants of popular distributions keep your containers lean and fast.


# Good: Using a minimal base image
FROM python:3.9-slim

Factor 4: Explicit Dependencies

Declare with Care Clearly specify all dependencies within your Dockerfile. Avoid relying on packages present in the base image; instead, explicitly install what you need. This not only documents your application’s requirements but also prevents surprises when base images update.


# Good: Installing only necessary packages
RUN apt-get update && apt-get install -y \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

Factor 5: Environment Abstraction

Configurable Containers Externalize configuration that varies between environments (staging, production, etc.) using environment variables. This strategy avoids hard-coding settings and makes your container portable and environment-agnostic.


# Use environment variables for configuration
ENV DATABASE_URL postgres://db_user:db_pass@hostname:5432/db_name

Factor 6: Layer Efficiency

Optimize for the Cache Order Dockerfile instructions to take advantage of Docker’s layer caching. Group changes to reduce layers and rebuild times. Remember, Docker only re-builds from the first changed instruction!


# Good: Combining RUN commands
RUN apt-get update && apt-get install -y \
    curl \
    git \
    && rm -rf /var/lib/apt/lists/*

Factor 7: Security Practices

Secure by Design Employ multi-stage builds to reduce the attack surface by limiting what’s included in the final image. Regularly update and scan your images for vulnerabilities, and ensure that only necessary components are present in production containers.

Example - Regularly update the base image:

FROM ubuntu:20.04

Example - Use multi-stage builds:

FROM golang:1.15 as builder
COPY . .
RUN go build -o myapp

FROM alpine:latest  
COPY --from=builder /app/myapp .
CMD ["./myapp"]

Factor 8: Non-Privileged User

Least Privilege Run containers as a non-root user whenever possible. This minimizes risks if your container becomes compromised and is a best practice for security in containerized environments.

RUN adduser -D myuser
USER myuser

Factor 9: Health Checks

Self-Healing Systems Implement health checks in your container. This enables the orchestrator to respond to issues, maintaining the reliability and availability of your services.


HEALTHCHECK CMD curl --fail http://localhost:8080/health || exit 1

Factor 10: Build Context Minimization

Selective Copying Use a .dockerignore file to keep the build context as small as possible. Excluding unnecessary files reduces build time and minimizes potential for unintended inclusion of sensitive files.

Example .dockerignore:


Factor 11: Documentation and Comments

Clear as Day Comment your Dockerfiles thoroughly. Like good code documentation, this helps future maintainers understand why certain decisions were made. A well-documented Dockerfile is a sign of a mature development process.


# Install dependencies
# Note: libpq-dev is required for PostgreSQL adapter
RUN apt-get update && apt-get install -y libpq-dev

Factor 12: One-Time Commands

Build-time Actions Use Dockerfile instructions to run setup commands that need to only be run once, like compiling assets or code. This is more efficient than running these commands every time the container starts.


RUN chmod +x ./script.sh
RUN ./script.sh

Applying these 12 factors to your Dockerfiles & containers will not only improve the quality of your containers but will also align your development practices with modern, cloud-native principles. The result is a fleet of containers that are secure, scalable, and maintainable, ready to be deployed across diverse environments with confidence.

In conclusion, as you embark on the journey of containerization, let these 12 factors be the guiding stars that steer your Dockerfiles towards excellence.

Happy containerizing!