Phase 2 Integrated Practice

Putting Phase 2 Concepts Together—From Source Code to Production-Ready Images: Fully Containerizing a Web Application.

1. What You'll Learn


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"]

(2) Containerization Strategy

Component Base Image Policy
Flask Application python:3.12-slim Multi-stage Build + Non-root + HEALTHCHECK
PostgreSQL postgres:15-alpine Official Image + Named Volume
Nginx nginx:1.25-alpine Official Image + Configuration File Bind Mount

4. Dockerfile for a Flask Application

(1) Project Structure

TEXT
todo-app/
├── app.py              # Flask application
├── requirements.txt    # Python dependencies
├── templates/          # HTML templates
│   └── index.html
├── Dockerfile          # Multi-stage production build
├── .dockerignore       # Exclude unnecessary files
├── nginx.conf          # Nginx reverse proxy config
└── docker-run.sh       # One-click deployment script

▶ Example: Writing a Multi-Stage Dockerfile (Difficulty: ⭐⭐⭐)

DOCKERFILE
# ============================================
# Multi-stage Dockerfile for Flask Todo App
# Stage 1: Build virtual environment
# Stage 2: Production runtime
# ============================================

# ---------- Stage 1: Builder ----------
FROM python:3.12-slim AS builder

WORKDIR /build

# Cache: install dependencies first
COPY requirements.txt .
RUN python -m venv /opt/venv && \
    /opt/venv/bin/pip install --no-cache-dir -r requirements.txt

# ---------- Stage 2: Production ----------
FROM python:3.12-slim

# Install curl for healthcheck
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

# Create non-root user
RUN groupadd -r appgroup && \
    useradd -r -g appgroup -u 1000 -m -d /home/appuser appuser

WORKDIR /app

# Copy virtual environment from builder
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# Copy application code
COPY --chown=appuser:appgroup . .

# Create log directory
RUN mkdir -p /app/logs && chown appuser:appgroup /app/logs

# Switch to non-root user
USER appuser

# Environment variables (defaults, override at runtime)
ENV FLASK_ENV=production \
    LOG_LEVEL=info \
    APP_PORT=5000

EXPOSE 5000

# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
  CMD curl -f http://localhost:5000/health || exit 1

# Start the application
CMD ["python", "app.py"]

(2) .dockerignore

TEXT
# .dockerignore
__pycache__
*.pyc
*.pyo
.git
.env
venv
.venv
*.md
Dockerfile
docker-compose*.yml

5. Environment Configuration Management

(1) Environment Variable Hierarchy

Level Method Priority Applicable Scenarios
Dockerfile ENV Default Value Minimum (1) General Default Configuration
docker run -e Runtime Override Medium (2) Deployment Configuration
.env File --env-file Part 3 Managing Multiple Variables
Docker Secrets --secret Highest Sensitive Information

▶ Example: Configuring Environment Variables (Difficulty: ⭐⭐)

BASH
# Run with custom environment variables
docker run -d \
  --name todo-app \
  -e FLASK_ENV=production \
  -e DATABASE_URL="postgresql://appuser:apppass@todo-db:5432/tododb" \
  -e LOG_LEVEL=warning \
  -p 5000:5000 \
  todo-app:1.0

▶ Example: Using a .env file (Difficulty: ⭐⭐)

BASH
# Create .env file
cat > .env << 'EOF'
FLASK_ENV=production
DATABASE_URL=postgresql://appuser:apppass@todo-db:5432/tododb
LOG_LEVEL=warning
APP_PORT=5000
EOF

# Run with --env-file
docker run -d \
  --name todo-app \
  --env-file .env \
  -p 5000:5000 \
  todo-app:1.0

6. Log Persistence

▶ Example: Configuring Log Persistence (Difficulty: ⭐⭐)

BASH
# Mount a named volume for application logs
docker run -d \
  --name todo-app \
  -v app-logs:/app/logs \
  -e LOG_LEVEL=info \
  todo-app:1.0

(1) Comparison of Logging Policies

Solution Advantages Disadvantages
Logging inside the container Simple Lost when the container is deleted
Volume Persistence Persists after the container is deleted Log rotation must be managed manually
stdout + log driver Native Docker management Complex configuration
Centralized Logging (ELK/Loki) Full-stack searchability Requires additional infrastructure

7. Mirror Tags and Version Management

(1) Tagging Strategy

Strategy Tag Format Example Applicable Scenarios
Semantic Versioning vPrimary.Secondary.修订 v2.1.0 Production Release
Git SHA sha-<commit> sha-a1b2c3d CI/CD tracking
latest latest latest Development/Quick Start
Branch Name <branch> main, dev Development Environment

▶ Example: Multi-tag Push (Difficulty: ⭐⭐)

BASH
# Build with multiple tags
docker build -t todo-app:v1.0.0 -t todo-app:latest .

# Push all tags
docker push localhost:5000/todo-app:v1.0.0
docker push localhost:5000/todo-app:latest

8. Deploying the Full Stack

▶ Example: One-Click Deployment of Flask + PostgreSQL + Nginx (Difficulty: ⭐⭐⭐)

BASH
# ============================================
# One-click deployment script
# ============================================

# 1. Create network and volumes
docker network create todo-net
docker volume create pg-data
docker volume create app-logs

# 2. Start PostgreSQL
docker run -d \
  --name todo-db \
  --network todo-net \
  -e POSTGRES_USER=appuser \
  -e POSTGRES_PASSWORD=apppass \
  -e POSTGRES_DB=tododb \
  -v pg-data:/var/lib/mysql \
  --restart=unless-stopped \
  postgres:15-alpine

# 3. Wait for PostgreSQL to initialize
sleep 15

# 4. Build and start Flask app
docker build -t todo-app:1.0 ./todo-app
docker run -d \
  --name todo-app \
  --network todo-net \
  -e DATABASE_URL="postgresql://appuser:apppass@todo-db:5432/tododb" \
  -v app-logs:/app/logs \
  --restart=unless-stopped \
  todo-app:1.0

# 5. Start Nginx reverse proxy
docker run -d \
  --name todo-nginx \
  --network todo-net \
  -p 80:80 \
  -v $(pwd)/todo-app/nginx.conf:/etc/nginx/conf.d/default.conf \
  --restart=unless-stopped \
  nginx:1.25-alpine

# 6. Verify
docker ps
curl http://localhost/health

9. Validation and Optimization Results

(1) Validate the optimization using dive

BASH
# Analyze the multi-stage image
dive todo-app:1.0

# Expected: efficiency score > 90%, no wasted space

(2) Comparison of Image Sizes

version Dockerfile Image size Build time
V1 Single Stage FROM python:3.12 + COPY . . + RUN pip install 1.2 GB 5 minutes
V2 Optimized Base Image FROM python:3.12-slim + Cache Cleanup 350 MB 3 minutes
V3 Multi-stage builder + slim + non-root + HEALTHCHECK 150 MB 3 minutes (30 seconds with cache hit)

10. Complete Example: Containerizing a To-Do List App

BASH
# ============================================
# Complete walkthrough: Containerize a Flask app
# Covers: Dockerfile, build, multi-container deploy
# ============================================

# 1. Build the optimized image
docker build -t todo-app:v1.0.0 -t todo-app:latest ./todo-app

# 2. Verify the image
docker images todo-app
docker history todo-app:v1.0.0

# 3. Create infrastructure
docker network create todo-net
docker volume create pg-data
docker volume create app-logs

# 4. Start PostgreSQL with persistent storage
docker run -d \
  --name todo-db \
  --network todo-net \
  -e POSTGRES_USER=appuser \
  -e POSTGRES_PASSWORD=apppass \
  -e POSTGRES_DB=tododb \
  -v pg-data:/var/lib/postgresql/data \
  --restart=unless-stopped \
  postgres:15-alpine

# 5. Start Flask application
docker run -d \
  --name todo-app \
  --network todo-net \
  -e DATABASE_URL="postgresql://appuser:apppass@todo-db:5432/tododb" \
  -v app-logs:/app/logs \
  --restart=unless-stopped \
  todo-app:v1.0.0

# 6. Start Nginx reverse proxy
docker run -d \
  --name todo-nginx \
  --network todo-net \
  -p 8080:80 \
  -v $(pwd)/todo-app/nginx.conf:/etc/nginx/conf.d/default.conf \
  --restart=unless-stopped \
  nginx:1.25-alpine

# 7. Verify full stack
echo "=== Containers ===" && docker ps
echo "=== Health Check ===" && docker inspect todo-app --format='{{.State.Health.Status}}'
echo "=== App Response ===" && curl -s http://localhost:8080/health

# 8. Push to private registry (optional)
docker tag todo-app:v1.0.0 localhost:5000/todo-app:v1.0.0
docker push localhost:5000/todo-app:v1.0.0

# 9. Clean up (optional)
# docker stop todo-nginx todo-app todo-db
# docker rm todo-nginx todo-app todo-db

❓ FAQ

Q Does one container run one process?
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


📝 Exercises

  1. Basic Task (Difficulty: ⭐): Clone an open-source Flask/Express project, write a Dockerfile to build an image, and run it.
  2. 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.
  3. 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.com

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.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏