60 Days DevOps Challenge: Day 11

Making Docker Containers Production-Ready

๐Ÿš€ Initial Tasks

โœ… Task 1: Learn about Multi-Stage Builds

  • Understand how multi-stage builds help create smaller, secure, and production-ready Docker images.

  • Focus on:

    • Separating build and runtime environments.

    • Minimizing attack surface and image size.

    • Removing unnecessary build dependencies from the final image.


โœ… Task 2: Write a Basic Dockerfile

  • Create a Dockerfile for a simple Python or Node.js app.

  • Build and analyze the image size using:

docker images
  • Explore optimization opportunities like .dockerignore, base image choice, and caching layers.

โœ… Task 3: Check Container Logs Using Docker Logging

docker logs <container_id>
  • Default Docker log path:
    /var/lib/docker/containers/<container_id>/

  • Default logging driver: json-file

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
  • Useful for inspecting logs and managing disk usage.

โœ… Challenge 1: Convert an existing Dockerfile into a multi-stage build and compare image sizes.

e.g. Dockerfile

# Original Dockerfile (Single-Stage)
FROM python:3.9
# Set working directory
WORKDIR /app
# Copy all files
COPY . .
# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Expose port 5000
EXPOSE 5000
# Run the Flask app
CMD ["python", "app.py"]

Answer:

Step 1: Build & Check Image Size of the Single-Stage Build

docker build -t flask-single-stage .
docker images | grep flask-single-stage

Example Output:

flask-single-stage   latest   850MB

Step 2: Convert to a Multi-Stage Build

Optimized Multi-Stage Dockerfile

# First Stage: Build Dependencies
FROM python:3.9 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

# Second Stage: Final Image
FROM python:3.9-slim
WORKDIR /app
COPY --from=builder /app /app
EXPOSE 5000
CMD ["python", "app.py"]

Step 3: Build & Compare Image Sizes

docker build -t flask-multi-stage .
docker images | grep flask

Example Output:

flask-single-stage   latest   850MB
flask-multi-stage    latest   120MB

โœ… Reduced image size from 850MB โ†’ 120MB!

Step 4: Test the Optimized Image

docker run --rm -p 5000:5000 flask-multi-stage
curl http://localhost:5000

Expected Output:

Multi-Stage Flask App Running!

โœ… Challenge 2: Run a lightweight Alpine-based container (python:3.9-alpine or node:14-alpine).

Answer:
To further reduce the image size, change the final stage to python:3.9-alpine:

Final Dockerfile:

# First Stage: Build Dependencies
FROM python:3.9 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

# Second Stage: Final Image
FROM python:3.9-alpine
WORKDIR /app
COPY --from=builder /app /app
EXPOSE 5000
CMD ["python", "app.py"]

Rebuild & compare:

docker build -t flask-alpine .
docker images | grep flask

Example Output:

flask-multi-stage latest 120MB
flask-alpine      latest  50MB

โœ… Challenge 3: Add a HEALTHCHECK in a Dockerfile for a web application (curl or wget).

Answer:

Step 1: Update the Dockerfile with HEALTHCHECK

# First Stage: Build Dependencies
FROM python:3.9 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

# Second Stage: Final Image
FROM python:3.9-alpine
WORKDIR /app
COPY --from=builder /app /app
EXPOSE 5000
# Add a health check
HEALTHCHECK --interval=10s --timeout=3s --retries=3 CMD curl -f http://localhost:5000/ || exit 1
CMD ["python", "app.py"]

Step 2: Build the Image

docker build -t flask-app-healthcheck .

Step 3: Run the Container

docker run -d --name flask-container -p 5000:5000 flask-app-healthcheck

Step 4: Verify Container Health Status

docker ps

Example Output:

CONTAINER ID  IMAGE                   STATUS                   NAMES
abc123456789  flask-app-healthcheck   Up 30s (healthy)         flask-container

Step 5: Inspect Health Check Details

docker inspect --format='{{json .State.Health}}' flask-container | jq

โœ… Challenge 4: Run a container without root privileges and ensure the app runs correctly.

Answer:

Step 1: Modify the Dockerfile to Run as a Non-Root User

# First Stage: Build Dependencies
FROM python:3.9 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

# Second Stage: Final Image
FROM python:3.9-slim
WORKDIR /app
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
COPY --from=builder /app /app
RUN chown -R appuser:appgroup /app
USER appuser
EXPOSE 5000
CMD ["python", "app.py"]

Step 2: Build the Image

docker build -t flask-nonroot-app .

Step 3: Run the Container as a Non-Root User

docker run -d --name flask-nonroot-container -p 5000:5000 flask-nonroot-app

Step 4: Verify the Running User in the Container

docker exec -it flask-nonroot-container whoami

Expected Output:

appuser

Step 5: Test the Application

curl http://localhost:5000

Expected Output:

Flask App Running as Non-Root User!

Step 6: Check the Running Processes

docker exec -it flask-nonroot-container ps aux

Expected Output (Shows appuser instead of root):

appuser       1  0.0  0.1  18236  3456 ?        Ss   10:30   0:00 python app.py

โœ… Challenge 5: Scan your Docker image using docker scan or Trivy, and fix vulnerabilities.

Answer:

Step 1: Build the Docker Image

docker build -t flask-app-secure .

Step 2: Scan the Image Using docker scan (Docker Hub Scanner)

docker scan flask-app-secure

Example Output:

โœ— High severity vulnerability found in glibc
Description: Heap-based buffer overflow
Fix: Upgrade to glibc 2.35-0ubuntu3.2

Step 3: Scan the Image Using Trivy (More Detailed)

brew install trivy    # macOS
sudo apt install trivy  # Ubuntu/Debian
trivy image flask-app-secure

Example Output:

Total: 5 vulnerabilities
Critical: 1
High: 2
Medium: 2
Low: 0

Step 4: Fix Vulnerabilities

  • Replace python:3.9 with python:3.9-slim or python:3.9-alpine

  • Update system packages:

RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends curl
  • Use non-root user:
RUN useradd -m appuser
USER appuser

Step 5: Rebuild the Image and Re-Scan

docker build -t flask-app-secure-fixed .
trivy image flask-app-secure-fixed

โœ… If vulnerabilities are reduced, the security improvements were successful!


โœ… Challenge 6: Implement log management by redirecting container logs to a file.

Answer:

Option 1: Redirect Logs to a File Inside the Container

FROM python:3.9-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
CMD ["sh", "-c", "python app.py > /app/app.log 2>&1"]

Run & View Logs

docker build -t flask-app-logs .
docker run -d --name flask-log-container flask-app-logs
docker exec -it flask-log-container tail -f /app/app.log

Option 2: Redirect Logs to a Host File

docker run -d --name flask-log-container \
  -v $(pwd)/logs:/app/logs \
  flask-app-logs
tail -f logs/app.log

Option 3: Use Docker's Default Log File Location

docker logs flask-log-container > flask-container.log
tail -f flask-container.log

Option 4: Use Docker Logging Drivers

docker run -d --name flask-log-container \
  --log-driver json-file \
  --log-opt max-size=10m \
  --log-opt max-file=3 \
  flask-app-logs

โœ… Challenge 7: Set up resource limits (memory, CPU) for a container using --memory and --cpus.

Answer:

Step 1: Build the Docker Image

docker build -t flask-resource-limited .

Step 2: Run the Container with CPU & Memory Limits

docker run -d --name flask-container \
  --memory="512m" \
  --cpus="0.5" \
  -p 5000:5000 \
  flask-resource-limited

Step 3: Verify Resource Limits

docker stats flask-container
docker inspect flask-container | grep -i '"memory"\|"cpu"'

Step 4: Test Resource Limits

docker exec -it flask-container stress --vm 1 --vm-bytes 700M --timeout 10s
docker exec -it flask-container stress --cpu 2 --timeout 10s

โœ… Challenge 8: Use docker build --progress=plain to analyze layer caching and optimize the build process.

Answer:

Step 1: Modify Dockerfile for Layer Optimization

Inefficient Dockerfile:

FROM python:3.9
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
EXPOSE 5000
CMD ["python", "app.py"]

Optimized Dockerfile:

FROM python:3.9
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]

Step 2: Build the Image with Plain Progress Output

docker build --progress=plain -t flask-app-optimized .

In conclusion, making Docker containers production-ready involves several key practices that enhance security, efficiency, and performance. By utilizing multi-stage builds, developers can create smaller and more secure images by separating build and runtime environments. Writing optimized Dockerfiles, implementing health checks, and running containers with non-root privileges further contribute to a robust containerization strategy. Additionally, scanning images for vulnerabilities and managing logs effectively are crucial for maintaining security and operational efficiency. Setting resource limits ensures that containers run within defined parameters, preventing resource exhaustion. By following these best practices, developers can ensure their Docker containers are well-prepared for production environments.

0
Subscribe to my newsletter

Read articles from Musaveer Holalkere directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Musaveer Holalkere
Musaveer Holalkere