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
withpython:3.9-slim
orpython: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.
Subscribe to my newsletter
Read articles from Musaveer Holalkere directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
