Dockerfile Guide - Building Custom Container Images

Status: Active
Last Updated: 2026-01-30
Category: Containers - Image Building
Prerequisites: docker-basics, docker-concepts
Time: 4-5 hours
Tags: docker, dockerfile, images, build, best-practices

Summary

Master Dockerfile creation to build custom container images. Learn every instruction, layer optimization, multi-stage builds, and production best practices. Transform your applications into portable, reproducible container images.

๐ŸŽฏ What You'll Learn

By the end of this article, you'll be able to:

๐Ÿ“ What is a Dockerfile?

Dockerfile: Text file with instructions to build a Docker image.

Analogy: Recipe for creating a container image.

Basic Structure:

# Base image
FROM ubuntu:22.04

# Set working directory
WORKDIR /app

# Copy files
COPY . .

# Install dependencies
RUN apt-get update && apt-get install -y python3

# Define startup command
CMD ["python3", "app.py"]

๐Ÿ—๏ธ Your First Dockerfile

Simple Python App

1. Create App File (app.py):

print("Hello from Docker!")

2. Create Dockerfile:

FROM python:3.11-slim

WORKDIR /app

COPY app.py .

CMD ["python", "app.py"]

3. Build Image:

docker build -t my-python-app:v1 .

4. Run Container:

docker run my-python-app:v1
# Output: Hello from Docker!

๐Ÿ“š Dockerfile Instructions

FROM - Base Image

Sets the base image for your build.

# Use official image
FROM python:3.11-slim

# Specific version
FROM node:20.10.0

# Alpine (smaller size)
FROM python:3.11-alpine

# Multi-stage build (later)
FROM golang:1.21 AS builder
FROM scratch

Best Practices:


WORKDIR - Set Working Directory

Sets the working directory for subsequent instructions.

FROM python:3.11-slim

# Set working directory
WORKDIR /app

# All commands now run in /app
COPY . .
RUN pip install -r requirements.txt

Without WORKDIR:

# Bad - repetitive and error-prone
COPY . /app
RUN cd /app && pip install -r requirements.txt

COPY - Copy Files

Copies files from build context to image.

# Copy single file
COPY app.py /app/

# Copy directory
COPY ./src /app/src

# Copy with wildcards
COPY *.py /app/

# Copy and rename
COPY app.py /app/main.py

Best Practice: Copy only what you need.

# Bad - copies everything
COPY . .

# Good - specific files
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY ./src ./src

ADD - Copy Files (Advanced)

Like COPY but with extra features:

# Extract tar file
ADD archive.tar.gz /app/

# Download from URL (not recommended)
ADD https://example.com/file.tar.gz /tmp/

Best Practice: Use COPY unless you specifically need ADD features.


RUN - Execute Commands

Runs commands during image build.

# Single command
RUN apt-get update

# Multiple commands (bad - creates multiple layers)
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim

# Multiple commands (good - single layer)
RUN apt-get update && \
    apt-get install -y \
        curl \
        vim \
    && rm -rf /var/lib/apt/lists/*

Common Patterns:

Python:

RUN pip install --no-cache-dir -r requirements.txt

Node.js:

RUN npm ci --only=production

System Packages:

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        package1 \
        package2 \
    && rm -rf /var/lib/apt/lists/*

CMD - Default Command

Sets the default command to run when container starts.

# Exec form (preferred)
CMD ["python", "app.py"]

# Shell form
CMD python app.py

# With arguments
CMD ["nginx", "-g", "daemon off;"]

Note: Only last CMD takes effect. Can be overridden at runtime.

# Uses CMD from Dockerfile
docker run my-app

# Overrides CMD
docker run my-app python debug.py

ENTRYPOINT - Container Executable

Makes container run as an executable.

# With ENTRYPOINT
ENTRYPOINT ["python"]
CMD ["app.py"]

# Now you can:
docker run my-app            # Runs: python app.py
docker run my-app debug.py   # Runs: python debug.py

ENTRYPOINT + CMD Pattern:

# ENTRYPOINT = executable
ENTRYPOINT ["python"]

# CMD = default arguments
CMD ["app.py"]

ENV - Environment Variables

Sets environment variables.

# Single variable
ENV NODE_ENV=production

# Multiple variables
ENV APP_HOME=/app \
    APP_USER=appuser \
    APP_PORT=8080

# Use in subsequent commands
RUN echo "Port: $APP_PORT"

Usage at Runtime:

# Override env vars
docker run -e APP_PORT=9000 my-app

ARG - Build Arguments

Variables available during build (not in final image).

# Define argument
ARG PYTHON_VERSION=3.11

# Use in FROM
FROM python:${PYTHON_VERSION}-slim

# Can have defaults
ARG APP_ENV=development
RUN echo "Building for $APP_ENV"

Build with Arguments:

docker build --build-arg PYTHON_VERSION=3.10 -t my-app .
docker build --build-arg APP_ENV=production -t my-app:prod .

ARG vs ENV:


EXPOSE - Document Ports

Documents which ports the container listens on.

# Expose single port
EXPOSE 8080

# Multiple ports
EXPOSE 80 443

# With protocol
EXPOSE 8080/tcp
EXPOSE 53/udp

Note: This is documentation only. Still need -p to publish ports.

docker run -p 8080:8080 my-app

VOLUME - Define Mount Points

Creates mount point for external volumes.

# Define volume
VOLUME /app/data

# Multiple volumes
VOLUME ["/app/data", "/app/logs"]

Usage:

# Docker creates anonymous volume
docker run my-app

# Use named volume
docker run -v my-data:/app/data my-app

USER - Set User Context

Run commands as specific user (security!).

# Create user
RUN adduser --disabled-password --gecos '' appuser

# Switch to user
USER appuser

# All subsequent commands run as appuser
CMD ["python", "app.py"]

Security Best Practice: Don't run as root!


LABEL - Add Metadata

Add metadata to image.

LABEL maintainer="admin@example.com"
LABEL version="1.0"
LABEL description="My awesome application"

# Multi-line
LABEL org.opencontainers.image.title="My App" \
      org.opencontainers.image.version="1.0.0" \
      org.opencontainers.image.vendor="My Company"

View Labels:

docker inspect --format='{{json .Config.Labels}}' my-image

๐ŸŽจ Real-World Examples

Example 1: Python Flask App

Project Structure:

my-flask-app/
โ”œโ”€โ”€ Dockerfile
โ”œโ”€โ”€ requirements.txt
โ”œโ”€โ”€ app.py
โ””โ”€โ”€ static/
    โ””โ”€โ”€ style.css

requirements.txt:

Flask==3.0.0
gunicorn==21.2.0

app.py:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return "Hello from Docker Flask!"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Dockerfile:

FROM python:3.11-slim

# Set working directory
WORKDIR /app

# Copy requirements first (layer caching!)
COPY requirements.txt .

# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Copy application code
COPY . .

# Create non-root user
RUN adduser --disabled-password --gecos '' appuser && \
    chown -R appuser:appuser /app
USER appuser

# Expose port
EXPOSE 5000

# Use gunicorn for production
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

Build and Run:

docker build -t flask-app:v1 .
docker run -d -p 5000:5000 --name my-flask flask-app:v1
curl http://localhost:5000

Example 2: Node.js Express App

Project Structure:

my-node-app/
โ”œโ”€โ”€ Dockerfile
โ”œโ”€โ”€ package.json
โ”œโ”€โ”€ package-lock.json
โ””โ”€โ”€ server.js

package.json:

{
  "name": "my-node-app",
  "version": "1.0.0",
  "main": "server.js",
  "dependencies": {
    "express": "^4.18.2"
  }
}

server.js:

const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('Hello from Docker Node!');
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Dockerfile:

FROM node:20-alpine

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy application
COPY server.js .

# Non-root user (node user exists in node image)
USER node

EXPOSE 3000

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

Build and Run:

docker build -t node-app:v1 .
docker run -d -p 3000:3000 --name my-node node-app:v1
curl http://localhost:3000

Example 3: Static Website (Nginx)

Project Structure:

my-website/
โ”œโ”€โ”€ Dockerfile
โ””โ”€โ”€ html/
    โ”œโ”€โ”€ index.html
    โ””โ”€โ”€ style.css

html/index.html:

<!DOCTYPE html>
<html>
<head><title>My Site</title></head>
<body><h1>Hello from Docker Nginx!</h1></body>
</html>

Dockerfile:

FROM nginx:1.25-alpine

# Remove default nginx website
RUN rm -rf /usr/share/nginx/html/*

# Copy our website
COPY html/ /usr/share/nginx/html/

# nginx runs as nginx user by default

EXPOSE 80

# nginx already has CMD defined

Build and Run:

docker build -t my-website:v1 .
docker run -d -p 8080:80 --name my-web my-website:v1
curl http://localhost:8080

๐Ÿš€ Multi-Stage Builds

Problem: Build tools bloat final image.

Solution: Multi-stage builds!

Example: Go Application

Single-Stage (Bad):

FROM golang:1.21

WORKDIR /app
COPY . .
RUN go build -o myapp

CMD ["./myapp"]

# Final image: ~1GB (includes Go compiler!)

Multi-Stage (Good):

# Stage 1: Build
FROM golang:1.21 AS builder

WORKDIR /app
COPY . .
RUN go build -o myapp

# Stage 2: Runtime
FROM alpine:latest

WORKDIR /app
COPY --from=builder /app/myapp .

CMD ["./myapp"]

# Final image: ~15MB!

Multi-Stage: Node.js with Build Step

# Stage 1: Build dependencies
FROM node:20 AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Stage 2: Build application
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 3: Production
FROM node:20-alpine
WORKDIR /app

# Copy production dependencies
COPY --from=dependencies /app/node_modules ./node_modules

# Copy built application
COPY --from=builder /app/dist ./dist
COPY package.json ./

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

โšก Layer Optimization

Understanding Layers

Each instruction = New layer.

FROM ubuntu:22.04           # Layer 1
RUN apt-get update          # Layer 2
RUN apt-get install curl    # Layer 3
RUN apt-get install vim     # Layer 4
COPY app.py .               # Layer 5

Better:

FROM ubuntu:22.04                                   # Layer 1
RUN apt-get update && apt-get install -y curl vim  # Layer 2
COPY app.py .                                       # Layer 3

Optimize Build Cache

Order Matters! Put changing layers last.

Bad:

FROM python:3.11-slim
COPY . .                        # โ† Changes often, invalidates cache
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

Good:

FROM python:3.11-slim
COPY requirements.txt .         # โ† Changes rarely
RUN pip install -r requirements.txt  # โ† Cached if requirements.txt unchanged
COPY . .                        # โ† Changes often, but cache above preserved
CMD ["python", "app.py"]

Minimize Layers

Combine Related Commands:

Bad:

RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN apt-get clean

Good:

RUN apt-get update && \
    apt-get install -y \
        curl \
        vim \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

๐Ÿ”’ Security Best Practices

1. Don't Run as Root

# Create user
RUN addgroup --gid 1000 appgroup && \
    adduser --uid 1000 --gid 1000 --disabled-password appuser

# Switch to user
USER appuser

# Now container runs as non-root

2. Use Specific Tags

# Bad
FROM python:latest

# Good
FROM python:3.11.7-slim

3. Scan for Vulnerabilities

# Use docker scan or trivy
docker scan my-image:v1

# Or trivy
trivy image my-image:v1

4. Don't Store Secrets in Images

Bad:

# DON'T DO THIS!
ENV API_KEY=secret123

Good:

# Pass at runtime
docker run -e API_KEY=secret123 my-app

# Or use secrets management
docker run --env-file .env my-app

5. Minimize Attack Surface

# Use minimal base images
FROM python:3.11-alpine  # Smaller = fewer vulnerabilities

# Remove unnecessary packages
RUN apk add --no-cache curl && \
    # Use curl
    apk del curl  # Remove when done

๐Ÿท๏ธ Building and Tagging

Build with Tag

# Basic build
docker build -t my-app:v1 .

# Multiple tags
docker build -t my-app:v1 -t my-app:latest .

# Specify Dockerfile
docker build -f Dockerfile.prod -t my-app:prod .

# Build with build args
docker build --build-arg VERSION=1.0 -t my-app:1.0 .

Tagging Strategies

# Version tags
my-app:1.0.0
my-app:1.0
my-app:1
my-app:latest

# Environment tags
my-app:dev
my-app:staging
my-app:prod

# Git commit tags
my-app:abc123
my-app:main-abc123

.dockerignore File

Exclude files from build context (like .gitignore).

.dockerignore:

# Git
.git
.gitignore

# Dependencies
node_modules/
__pycache__/
*.pyc

# Development
.env
.env.local
*.log

# Documentation
README.md
docs/

# CI/CD
.github/
.gitlab-ci.yml

Benefits:


๐Ÿšจ Troubleshooting

Build Fails

View Build Output:

docker build -t my-app:v1 .

Check Specific Layer:

# Build fails at layer X
# Run up to that point
docker build --target builder -t debug .
docker run -it debug bash

Image Too Large

Check Layer Sizes:

docker history my-app:v1

Common Issues:

Solutions:


Cache Not Working

Force rebuild without cache:

docker build --no-cache -t my-app:v1 .

Rebuild from specific layer:

docker build --no-cache --target builder -t my-app:v1 .

๐Ÿ’ก Pro Tips

1. Use build Healthchecks:

HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost/ || exit 1

2. Set Shell for RUN:

# Use bash instead of sh
SHELL ["/bin/bash", "-c"]

3. Use ONBUILD for Base Images:

# In base image
ONBUILD COPY . /app
ONBUILD RUN pip install -r requirements.txt

# Child images automatically run these

๐Ÿ”— What's Next?

Now that you can build images:

Volumes:

Networking:

Multi-Container:


๐Ÿ“š Resources

Official Docs:

Tools:


๐Ÿ“ Change Log

2026-01-30


Next Article: docker-volumes - Master persistent storage!

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse