Putting Phase 2 Concepts Together—From Source Code to Production-Ready Images: Fully Containerizing a Web Application.
1. What You'll Learn
A Complete, Hands-On Project Built in Multiple Phases
Environment Configuration Management Policy
Log Persistence Solutions
Health Checks and Self-Healing Configuration
Mirror Tags and Version Control
2. A Story About Taking Over a Legacy Project
(1) Pain Point: Legacy Projects That Won’t Run in Docker
Charlie took over a legacy Python project that “wouldn’t run in Docker”—it had a 1.2 GB Dockerfile, took 10 minutes to build each time, lacked health checks, wrote logs inside the container (which were lost upon restart), ran as the root user, and had hard-coded environment variables. Deployment took 30 minutes and was unstable.
(2) A Fully Containerized Solution
Alice helped him by analyzing the dependency tree, writing a multi-stage Dockerfile, configuring log persistence, adding health checks, and running the application as a non-root user. In the end, the project was built and deployed in under 3 minutes.
DOCKERFILE
# Multi-stage production Dockerfile
FROM python:3.12-slim AS builder
# ... build optimized venv
FROM python:3.12-slim
# ... copy venv, non-root, healthcheck
(3) Duration: From 30 minutes to 3 minutes
Metric
Before Optimization
After Optimization
Image Size
1.2 GB
150 MB
Build Time
10 minutes
3 minutes
Deployment Time
30 minutes
3 minutes
Security Score
45
85
3. Project Architecture Design
(1) Application Architecture
In this lesson, we'll containerize a Todo List Flask application: Flask + PostgreSQL + Nginx.
100%
graph TB
USER["Browser"] --> NGX["Nginx<br/>Reverse Proxy + Static"]
NGX -->|"proxy_pass :5000"| APP["Flask App<br/>Todo List API + UI"]
APP -->|"psycopg2 :5432"| DB["PostgreSQL<br/>Todo Database"]
DB --> VOL["Named Volume<br/>pg-data"]
APP --> LOGS["Named Volume<br/>app-logs"]
MERMAID
graph TB
USER["Browser"] --> NGX["Nginx<br/>Reverse Proxy + Static"]
NGX -->|"proxy_pass :5000"| APP["Flask App<br/>Todo List API + UI"]
APP -->|"psycopg2 :5432"| DB["PostgreSQL<br/>Todo Database"]
DB --> VOL["Named Volume<br/>pg-data"]
APP --> LOGS["Named Volume<br/>app-logs"]
A The best practice is "one container, one process"—one container for the Flask application, one for Nginx, and one for PostgreSQL. Benefits: independent scaling, independent updates, and fault isolation. Counterexample: If one container runs Nginx, Flask, and PostgreSQL, a failure in any one component brings down the entire system, and independent scaling is not possible.
Q Should log files be written inside the container or to a mounted volume?
A A combination of both approaches: ① The application writes to both stdout (which can be viewed with docker logs) and to a file (persisted on a mounted volume); ② In a production environment, we recommend using stdout combined with centralized log collection (ELK/Loki), and no longer writing to files. The volume-based approach covered in this lesson is an intermediate solution.
Q How do you distinguish between development and production environments during the build process?
A There are three methods: ① ARG + conditional RUN (not recommended, as it makes the Dockerfile more complex); ② Multiple Dockerfiles (Dockerfile.dev / Dockerfile.prod); ③ Multi-stage builds + --target selection (recommended). In this lesson, we’ll use ENV variables to set default values and override them with docker run -e.
Q How are image version numbers managed?
A Semantic Versioning (SemVer): major version.minor version.build number (e.g., v2.1.0). In CI/CD, Git SHA tags (e.g., sha-a1b2c3d) are also applied to facilitate traceability. The latest tag always points to the latest stable version, but in production environments, use the exact version number instead of latest.
Q How do I set the time zone for a container?
A There are two ways: ① docker run -e TZ=Asia/Shanghai (requires installing tzdata on the base image); ② -v /etc/localtime:/etc/localtime:ro (uses the host’s time zone directly). The Alpine image requires RUN apk add --no-cache tzdata to support the TZ variable.
📖 Summary
Multi-stage build: The venv is created during the Builder stage, and the Runner stage only copies the venv; the image size is reduced from 1.2 GB to 150 MB
Hierarchical management of environment variables: Dockerfile ENV (default) → docker run -e (runtime override) → .env file (multiple variables)
Log Persistence: A Named Volume is mounted at /app/logs; logs are retained after the container is restarted or rebuilt
Health checks: curl /health endpoint; Docker automatically detects issues and implements a restart policy for self-healing
Running as non-root + HEALTHCHECK + log persistence = The three essential elements of a production-grade Dockerfile
📝 Exercises
Basic Task (Difficulty: ⭐): Clone an open-source Flask/Express project, write a Dockerfile to build an image, and run it.
Advanced Exercise (Difficulty: ⭐⭐): Write a multi-stage Dockerfile for this project and compare the differences in image size between single-stage and multi-stage images.
Challenge (Difficulty: ⭐⭐⭐): Use dive to analyze the optimized image, identify layers that can be further optimized, then fix them and verify the reduction in file size.
Web-Tutorial Tech Team
A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.