Introduction
Docker has revolutionized how we build, ship, and run applications by providing a standardized way to package software with all its dependencies. Understanding the Docker build process is fundamental to modern DevOps practices.
This guide visualizes the complete journey from writing a Dockerfile to running a container:
- Dockerfile Creation: Writing instructions for your application
- Image Building: Creating a reusable image from the Dockerfile
- Container Execution: Running instances of your image
- Multi-Stage Builds: Optimizing image size and security
Part 1: The Docker Build Process Flow
Complete Build to Run Lifecycle
Define base image,
dependencies, commands] CreateDockerfile --> BuildContext[Prepare Build Context
Gather files needed
for build] BuildContext --> DockerBuild[Run: docker build -t app:v1 .] DockerBuild --> LayerProcess{Process Each
Instruction} LayerProcess --> FROM[FROM instruction
Pull base image] FROM --> COPY[COPY/ADD instructions
Copy application files] COPY --> RUN[RUN instructions
Execute commands
Install dependencies] RUN --> ENV[ENV/EXPOSE/WORKDIR
Set environment] ENV --> CMD[CMD/ENTRYPOINT
Define startup command] CMD --> CacheCheck{Layer exists
in cache?} CacheCheck -->|Yes| UseCache[Use cached layer
β‘ Fast build] CacheCheck -->|No| BuildLayer[Build new layer
π¨ Execute instruction] UseCache --> NextLayer{More
instructions?} BuildLayer --> NextLayer NextLayer -->|Yes| LayerProcess NextLayer -->|No| CreateImage[Create Docker Image
Tagged as app:v1
Stored in local registry] CreateImage --> DockerRun[Run: docker run -p 8080:8080 app:v1] DockerRun --> CreateContainer[Create Container
- Writable layer on top
- Network configuration
- Volume mounts] CreateContainer --> StartProcess[Start Container Process
Execute CMD/ENTRYPOINT] StartProcess --> Running[Container Running β
Application accessible
on port 8080] Running --> Stop{Container
stopped?} Stop -->|No| Running Stop -->|Yes| Cleanup[Container stopped
Can be restarted
or removed] style CreateDockerfile fill:#064e3b,stroke:#3b82f6 style CreateImage fill:#1e3a8a,stroke:#3b82f6 style Running fill:#064e3b,stroke:#3b82f6
Image Layer Structure
Docker images are built in layers, with each instruction in the Dockerfile creating a new layer:
Application logs, temp files
Lost when container removed] end subgraph Image["π¦ Docker Image (Read-Only Layers)"] Layer5[Layer 5: CMD node app.js
Size: 0 B metadata only] Layer4[Layer 4: COPY . /app
Size: 2.3 MB application code] Layer3[Layer 3: RUN npm install
Size: 45 MB node_modules] Layer2[Layer 2: WORKDIR /app
Size: 0 B metadata only] Layer1[Layer 1: FROM node:18-alpine
Size: 167 MB base image] end WritableLayer --> Layer5 Layer5 --> Layer4 Layer4 --> Layer3 Layer3 --> Layer2 Layer2 --> Layer1 style WritableLayer fill:#7f1d1d,stroke:#ef4444 style Layer5 fill:#1e3a8a,stroke:#3b82f6 style Layer4 fill:#1e3a8a,stroke:#3b82f6 style Layer3 fill:#1e3a8a,stroke:#3b82f6 style Layer2 fill:#1e3a8a,stroke:#3b82f6 style Layer1 fill:#064e3b,stroke:#10b981
Part 2: Dockerfile to Image Build Process
How Docker Processes Each Instruction
(Dockerfile + files) Note over Daemon: Parse Dockerfile
Validate syntax Daemon->>Registry: FROM node:18-alpine
Pull base image if not cached Registry-->>Daemon: Base image layers Note over Daemon: Layer 1: Base image
SHA: abc123... Daemon->>Daemon: WORKDIR /app
Create metadata layer Note over Daemon: Layer 2: Working directory
SHA: def456... Daemon->>FS: COPY package*.json /app/ FS-->>Daemon: Files copied Note over Daemon: Layer 3: Package files
SHA: ghi789... Daemon->>Daemon: RUN npm install
Execute in temporary container Note over Daemon: Installing dependencies...
Creating node_modules Daemon->>Daemon: Commit changes to new layer Note over Daemon: Layer 4: Dependencies
SHA: jkl012...
Size: 45 MB Daemon->>FS: COPY . /app/ FS-->>Daemon: Application code copied Note over Daemon: Layer 5: Application code
SHA: mno345... Daemon->>Daemon: CMD ["node", "app.js"]
Set default command metadata Note over Daemon: Layer 6: Metadata
SHA: pqr678... Daemon->>Daemon: Create final image
Tag: myapp:v1.0
Total size: 214 MB Daemon-->>CLI: Build successful
Image ID: sha256:pqr678... CLI-->>Dev: β Successfully built myapp:v1.0
Basic Dockerfile Example
# Layer 1: Base image
FROM node:18-alpine
# Layer 2: Set working directory
WORKDIR /app
# Layer 3: Copy package files
COPY package*.json ./
# Layer 4: Install dependencies
RUN npm install --production
# Layer 5: Copy application code
COPY . .
# Layer 6: Expose port (metadata only)
EXPOSE 3000
# Layer 7: Set environment variable
ENV NODE_ENV=production
# Layer 8: Define startup command (metadata only)
CMD ["node", "app.js"]
Part 3: Image to Container Execution
Container Creation and Startup Flow
- Image name
- Port mappings
- Environment vars
- Volume mounts] ParseArgs --> CheckImage{Image exists
locally?} CheckImage -->|No| PullImage[Pull from registry
docker pull myapp:v1.0] CheckImage -->|Yes| CreateContainer[Create Container] PullImage --> CreateContainer CreateContainer --> AllocateResources[Allocate Resources
- Container ID
- Writable layer
- Network namespace
- Process namespace] AllocateResources --> ConfigNetwork[Configure Networking
- Assign IP address
- Setup port mapping
- Connect to bridge] ConfigNetwork --> MountVolumes{Volumes
specified?} MountVolumes -->|Yes| SetupVolumes[Mount Volumes
Bind host paths
or named volumes] MountVolumes -->|No| SetEnv[Set Environment
Variables] SetupVolumes --> SetEnv SetEnv --> CreateProcess[Create Container
Process
Run CMD/ENTRYPOINT] CreateProcess --> InitNamespace[Initialize
Namespaces
- PID namespace
- Network namespace
- Mount namespace
- User namespace] InitNamespace --> ApplyCgroups[Apply cgroups
Limits
- CPU limits
- Memory limits
- I/O limits] ApplyCgroups --> StartApp[Start Application
Process] StartApp --> HealthCheck{Health check
configured?} HealthCheck -->|Yes| RunHealthCheck[Execute health
check command] HealthCheck -->|No| Running RunHealthCheck --> CheckResult{Health check
passed?} CheckResult -->|Yes| Running[Container Running β
Status: healthy] CheckResult -->|No| Unhealthy[Container Running
Status: unhealthy] Running --> Monitor[Monitor Container
- Logs
- Resource usage
- Exit code] Unhealthy --> Monitor style CreateContainer fill:#1e3a8a,stroke:#3b82f6 style Running fill:#064e3b,stroke:#10b981 style Unhealthy fill:#7f1d1d,stroke:#ef4444
Container Runtime Lifecycle
Process exits
Health check fails Stopped --> Running: docker start
Restart policy Running --> Restarting: Auto-restart
triggered Restarting --> Running: Restart
successful Restarting --> Stopped: Restart
failed Stopped --> Removed: docker rm Created --> Removed: docker rm Removed --> [*] note right of Created Container configured but not started - Layers allocated - Config set - No process running end note note right of Running Container executing - Process active - Network accessible - Resources allocated - Logs streaming end note note right of Paused Process suspended - State frozen - No CPU used - Memory retained - Quick resume end note note right of Stopped Process terminated - Exit code recorded - Writable layer kept - Can be restarted - Logs preserved end note
Part 4: Multi-Stage Builds
Multi-stage builds allow you to optimize image size by separating build-time dependencies from runtime dependencies:
Multi-Stage Build Flow
Size: 8 MB] S1Start --> S1Copy --> S1Build --> S1Binary end subgraph Stage2["π Runtime Stage"] S2Start[FROM alpine:latest] S2Copy[COPY --from=builder
/app/main /app/] S2Cmd[CMD /app/main] S2Final[Final Image
Size: 13 MB] S2Start --> S2Copy --> S2Cmd --> S2Final end S1Binary -.->|Extract only binary| S2Copy Note1["Build stage: 850 MB
golang image + dependencies
DISCARDED after build"] Note2["Final image: 13 MB
alpine + binary only
97% size reduction!"] style Stage1 fill:#1e3a8a,stroke:#3b82f6 style Stage2 fill:#064e3b,stroke:#10b981 style S1Binary fill:#78350f,stroke:#f59e0b style S2Final fill:#064e3b,stroke:#10b981
Multi-Stage Dockerfile Example
# ========== Build Stage ==========
FROM golang:1.21-alpine AS builder
WORKDIR /build
# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
# ========== Runtime Stage ==========
FROM alpine:latest
# Install ca-certificates for HTTPS
RUN apk --no-cache add ca-certificates
WORKDIR /app
# Copy only the binary from build stage
COPY --from=builder /build/main .
# Create non-root user
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser && \
chown -R appuser:appuser /app
USER appuser
EXPOSE 8080
CMD ["./main"]
Part 5: Docker Build Optimization Techniques
Layer Caching Strategy
changed?} Check1 -->|No| Cache1[β Use cached layer
Base image: 167 MB] Check1 -->|Yes| Rebuild1[β Rebuild from here
All following layers invalidated] Cache1 --> Check2{WORKDIR
changed?} Check2 -->|No| Cache2[β Use cached layer
Metadata only] Check2 -->|Yes| Rebuild2[β Rebuild from here] Cache2 --> Check3{package.json
changed?} Check3 -->|No| Cache3[β Use cached layer
Package files: 1 KB] Check3 -->|Yes| Rebuild3[β Rebuild from here
Dependencies changed!] Cache3 --> Check4{npm install
needed?} Check4 -->|No| Cache4[β Use cached layer
node_modules: 45 MB
β‘ Saves 2 minutes!] Check4 -->|Yes| Rebuild4[β Run npm install
Takes ~2 minutes] Cache4 --> Check5{Source code
changed?} Check5 -->|Yes| Rebuild5[β Rebuild from here
Copy new code: 2.3 MB
β‘ Only this layer rebuilt!] Check5 -->|No| Cache5[β Use cached layer] Rebuild5 --> Complete[Build complete
Total time: ~10 seconds] Cache5 --> Complete Rebuild1 --> RebuildAll[Rebuild everything
Total time: ~3 minutes] Rebuild2 --> RebuildAll Rebuild3 --> RebuildAll Rebuild4 --> RebuildAll style Cache1 fill:#064e3b,stroke:#10b981 style Cache2 fill:#064e3b,stroke:#10b981 style Cache3 fill:#064e3b,stroke:#10b981 style Cache4 fill:#064e3b,stroke:#10b981 style Rebuild5 fill:#78350f,stroke:#f59e0b style Complete fill:#064e3b,stroke:#10b981 style RebuildAll fill:#7f1d1d,stroke:#ef4444
Optimized vs Non-Optimized Dockerfile
β Non-Optimized (Slow Rebuilds)
FROM node:18-alpine
WORKDIR /app
# Copies everything, invalidates cache on any change
COPY . .
RUN npm install
CMD ["node", "app.js"]
β Optimized (Fast Rebuilds)
FROM node:18-alpine
WORKDIR /app
# Copy dependency files first (changes rarely)
COPY package*.json ./
RUN npm install --production
# Copy source code last (changes frequently)
COPY . .
CMD ["node", "app.js"]
Part 6: Common Docker Commands
Essential Docker Workflow
# Build an image
docker build -t myapp:v1.0 .
docker build -t myapp:v1.0 -f Dockerfile.prod .
# List images
docker images
docker image ls
# Run a container
docker run -d -p 8080:3000 --name myapp-container myapp:v1.0
docker run -it --rm myapp:v1.0 /bin/sh # Interactive shell
# List containers
docker ps # Running containers
docker ps -a # All containers
# View logs
docker logs myapp-container
docker logs -f myapp-container # Follow logs
# Execute command in running container
docker exec -it myapp-container /bin/sh
# Stop and remove
docker stop myapp-container
docker rm myapp-container
# Remove image
docker rmi myapp:v1.0
# Clean up
docker system prune -a # Remove unused images, containers, networks
docker volume prune # Remove unused volumes
Part 7: Best Practices
Security and Optimization
- Use Specific Base Image Tags
# β Bad: Version can change
FROM node:latest
# β
Good: Specific version
FROM node:18.17.0-alpine3.18
- Run as Non-Root User
# Create and use non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
USER nodejs
- Use .dockerignore
node_modules
npm-debug.log
.git
.env
*.md
.vscode
- Minimize Layers
# β Bad: Multiple RUN commands
RUN apk add --no-cache git
RUN apk add --no-cache curl
RUN apk add --no-cache vim
# β
Good: Single RUN command
RUN apk add --no-cache \
git \
curl \
vim
- Use Multi-Stage Builds
- Separate build and runtime dependencies
- Significantly reduce final image size
- Improve security by excluding build tools
Comparison: Docker vs Traditional Deployment
| Aspect | Traditional Deployment | Docker Containers |
|---|---|---|
| Consistency | “Works on my machine” issues | Identical across environments |
| Startup Time | Minutes to hours | Seconds |
| Resource Usage | Full OS per application | Shared kernel, minimal overhead |
| Isolation | Limited, shared dependencies | Process and filesystem isolation |
| Portability | Environment-specific | Run anywhere Docker runs |
| Scalability | Manual, slow | Rapid horizontal scaling |
| Versioning | Complex rollbacks | Tag-based, easy rollbacks |
Conclusion
Understanding the Docker build process from Dockerfile to container is essential for modern DevOps:
- Dockerfiles define your application environment as code
- Images are immutable, layered snapshots built from Dockerfiles
- Containers are running instances of images with writable layers
- Layer caching dramatically speeds up builds when used correctly
- Multi-stage builds optimize size and security
The visual diagrams in this guide illustrate how each component interacts, making the abstract concept of containerization more concrete and easier to understand.
Further Reading
- Docker Official Documentation
- Dockerfile Best Practices
- Docker Multi-Stage Builds
- Docker Security Best Practices
Ready to containerize your applications? Start with a simple Dockerfile and iterate!