用Docker部署Python项目

今天我们来做第一个实战项目:把一个Python Web应用打包成Docker镜像并部署。

项目背景

假设我们有一个Flask Web应用:

  • 提供RESTful API
  • 使用MySQL数据库
  • 使用Redis缓存
  • 需要持久化数据

项目结构

my-flask-app/
├── app.py              # Flask应用主文件
├── requirements.txt    # Python依赖
├── Dockerfile         # Docker镜像定义
├── docker-compose.yml # 多容器编排
├── .env              # 环境变量
├── .dockerignore     # Docker忽略文件
└── data/            # 本地数据目录(可选)

1. 创建Flask应用

app.py

from flask import Flask, jsonify, request
from flask_cors import CORS
import os
import mysql.connector
import redis
import datetime

app = Flask(__name__)
CORS(app)

# 数据库配置
db_config = {
    'host': os.getenv('DB_HOST', 'localhost'),
    'user': os.getenv('DB_USER', 'root'),
    'password': os.getenv('DB_PASSWORD', '123456'),
    'database': os.getenv('DB_NAME', 'mydb')
}

# Redis配置
redis_config = {
    'host': os.getenv('REDIS_HOST', 'localhost'),
    'port': int(os.getenv('REDIS_PORT', 6379)),
    'db': 0
}

def get_db_connection():
    """获取数据库连接"""
    return mysql.connector.connect(**db_config)

def get_redis_connection():
    """获取Redis连接"""
    return redis.Redis(**redis_config)

@app.route('/')
def index():
    """首页"""
    return jsonify({
        'message': 'Welcome to Flask API',
        'version': '1.0',
        'timestamp': datetime.datetime.now().isoformat()
    })

@app.route('/health')
def health():
    """健康检查"""
    try:
        # 检查数据库
        conn = get_db_connection()
        cursor = conn.cursor()
        cursor.execute("SELECT 1")
        cursor.close()
        conn.close()

        # 检查Redis
        r = get_redis_connection()
        r.ping()

        return jsonify({'status': 'healthy', 'checks': {'db': 'ok', 'redis': 'ok'}})
    except Exception as e:
        return jsonify({'status': 'unhealthy', 'error': str(e)}), 500

@app.route('/users', methods=['GET'])
def get_users():
    """获取所有用户"""
    try:
        # 先从Redis缓存获取
        r = get_redis_connection()
        cached = r.get('users')

        if cached:
            return jsonify({'source': 'cache', 'data': eval(cached)})

        # 缓存未命中,从数据库获取
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        cursor.execute("SELECT * FROM users")
        users = cursor.fetchall()
        cursor.close()
        conn.close()

        # 写入缓存,有效期300秒
        r.setex('users', 300, str(users))

        return jsonify({'source': 'database', 'data': users})

    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/users', methods=['POST'])
def create_user():
    """创建用户"""
    try:
        data = request.json
        name = data.get('name')
        email = data.get('email')

        if not name or not email:
            return jsonify({'error': 'Name and email are required'}), 400

        conn = get_db_connection()
        cursor = conn.cursor()
        cursor.execute(
            "INSERT INTO users (name, email) VALUES (%s, %s)",
            (name, email)
        )
        conn.commit()
        user_id = cursor.lastrowid
        cursor.close()
        conn.close()

        # 清除缓存
        r = get_redis_connection()
        r.delete('users')

        return jsonify({'id': user_id, 'name': name, 'email': email}), 201

    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    """获取单个用户"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
        user = cursor.fetchone()
        cursor.close()
        conn.close()

        if not user:
            return jsonify({'error': 'User not found'}), 404

        return jsonify(user)

    except Exception as e:
        return jsonify({'error': str(e)}), 500

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000, debug=os.getenv('DEBUG', 'False') == 'True')

2. 依赖文件

requirements.txt

flask==2.3.0
flask-cors==4.0.0
mysql-connector-python==8.1.0
redis==4.6.0
gunicorn==21.2.0

3. Docker镜像定义

Dockerfile

# 多阶段构建

# 构建阶段
FROM python:3.11-slim AS builder

WORKDIR /app

# 安装系统依赖
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    gcc \
    default-libmysqlclient-dev \
    pkg-config \
    && rm -rf /var/lib/apt/lists/*

# 只复制依赖文件(利用构建缓存)
COPY requirements.txt .

# 安装Python依赖
RUN pip install --no-cache-dir -r requirements.txt

# 运行阶段
FROM python:3.11-slim

WORKDIR /app

# 复制依赖
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin

# 复制应用代码
COPY . .

# 创建非root用户
RUN groupadd -r appuser && useradd -r -g appuser appuser
RUN chown -R appuser:appuser /app
USER appuser

# 设置环境变量
ENV PYTHONUNBUFFERED=1
ENV TZ=Asia/Shanghai

# 暴露端口
EXPOSE 8000

# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD python -c "import requests; requests.get('http://localhost:8000/health')" || exit 1

# 启动命令
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "--timeout", "120", "app:app"]

.dockerignore

# Git文件
.git
.gitignore

# Python缓存
__pycache__
*.py[cod]
*$py.class
*.so
.Python

# 虚拟环境
venv/
env/
ENV/
.venv

# IDE配置
.vscode/
.idea/
*.swp
*.swo

# 日志文件
*.log
logs/

# 环境变量
.env

# 测试文件
tests/
.pytest_cache/
.coverage

# 文档
docs/
*.md

4. 多容器编排

docker-compose.yml

version: '3.8'

services:
  # Flask应用
  app:
    build: .
    container_name: flask-app
    ports:
      - "${APP_PORT:-8000}:8000"
    environment:
      - DB_HOST=db
      - DB_USER=root
      - DB_PASSWORD=${DB_PASSWORD:-123456}
      - DB_NAME=${DB_NAME:-mydb}
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - DEBUG=${DEBUG:-False}
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - app-network
    restart: unless-stopped

  # MySQL数据库
  db:
    image: mysql:8.0
    container_name: mysql-db
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-123456}
      MYSQL_DATABASE: ${DB_NAME:-mydb}
    volumes:
      - mysql-data:/var/lib/mysql
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "${DB_PORT:-3306}:3306"
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${DB_PASSWORD:-123456}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    restart: unless-stopped

  # Redis缓存
  redis:
    image: redis:7-alpine
    container_name: redis-cache
    ports:
      - "${REDIS_PORT:-6379}:6379"
    volumes:
      - redis-data:/data
    command: redis-server --appendonly yes
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3
    restart: unless-stopped

networks:
  app-network:
    driver: bridge

volumes:
  mysql-data:
  redis-data:

init.sql(数据库初始化脚本)

-- 创建用户表
CREATE TABLE IF NOT EXISTS users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 插入测试数据
INSERT INTO users (name, email) VALUES
('Alice', 'alice@example.com'),
('Bob', 'bob@example.com'),
('Charlie', 'charlie@example.com');

.env(环境变量文件)

# 应用配置
APP_PORT=8000
DEBUG=False

# 数据库配置
DB_PASSWORD=123456
DB_NAME=mydb
DB_PORT=3306

# Redis配置
REDIS_PORT=6379

5. 技术原理解释

为什么用Docker?

问题1:环境一致性

  • 传统部署:开发环境、测试环境、生产环境不一致
  • Docker方案:所有环境使用相同的镜像

问题2:依赖管理

  • 传统部署:手动安装依赖,容易出错
  • Docker方案:依赖打包在镜像中

问题3:配置管理

  • 传统部署:配置散落在各个地方
  • Docker方案:通过环境变量统一管理

Docker如何实现隔离?

1. 命名空间(Namespace)隔离

  • PID Namespace:进程隔离(容器内只能看到自己的进程)
  • Network Namespace:网络隔离(独立的网络栈)
  • Mount Namespace:文件系统隔离(独立的文件系统)
  • UTS Namespace:主机名隔离(独立的主机名)

2. Cgroups资源限制

  • CPU限制:--cpus 限制CPU使用量
  • 内存限制:-m 限制内存使用量
  • 磁盘限制:--device-write-bps 限制磁盘写入速度

3. 联合文件系统(UnionFS)

  • 分层存储:镜像由多层叠加而成
  • 写时复制:容器运行时在镜像最上层添加可写层
  • 共享基础层:多个镜像共享相同的层

Docker Compose如何协调多容器?

1. 服务发现

  • 自动创建Docker网络
  • 服务之间通过容器名称互相访问
  • 内置DNS自动解析容器名称

2. 依赖管理

  • depends_on:控制启动顺序
  • healthcheck:等待服务就绪后再启动依赖

3. 资源管理

  • 统一管理所有容器的生命周期
  • 一键启动、停止、重启所有服务

数据持久化原理

1. 数据卷(Volume)

  • 独立于容器生命周期
  • 由Docker管理
  • 存储在宿主机的 /var/lib/docker/volumes/

2. 绑定挂载(Bind Mount)

  • 映射宿主机目录
  • 开发环境常用
  • 实时同步代码

6. 构建和部署

本地构建镜像

# 构建镜像
docker build -t my-flask-app:latest .

# 查看镜像
docker images my-flask-app

# 测试运行镜像
docker run -d -p 8000:8000 --name test-app my-flask-app

使用Docker Compose部署

# 启动所有服务
docker-compose up -d

# 查看服务状态
docker-compose ps

# 查看日志
docker-compose logs -f

# 查看特定服务日志
docker-compose logs -f app

验证部署

# 健康检查
curl http://localhost:8000/health

# 获取所有用户
curl http://localhost:8000/users

# 创建用户
curl -X POST http://localhost:8000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "David", "email": "david@example.com"}'

# 获取单个用户
curl http://localhost:8000/users/1

停止和清理

# 停止所有服务
docker-compose stop

# 删除所有容器、网络
docker-compose down

# 删除所有容器、网络、数据卷
docker-compose down -v

7. 进阶:多阶段构建优化

优化前(镜像大小:900MB)

FROM python:3.11
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

优化后(镜像大小:150MB)

# 构建阶段
FROM python:3.11-slim AS builder
RUN apt-get update && apt-get install -y gcc default-libmysqlclient-dev
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 运行阶段
FROM python:3.11-slim
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY . .
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:app"]

优化原理:

  1. 使用slim基础镜像(更小)
  2. 多阶段构建(分离构建和运行环境)
  3. 清理缓存(减小镜像体积)

8. 生产环境部署

创建生产环境配置

docker-compose.prod.yml

version: '3.8'

services:
  app:
    image: my-flask-app:latest
    environment:
      - DB_HOST=${DB_HOST}
      - DB_USER=${DB_USER}
      - DB_PASSWORD=${DB_PASSWORD}
      - DB_NAME=${DB_NAME}
      - REDIS_HOST=${REDIS_HOST}
      - DEBUG=False
    ports:
      - "8000:8000"
    restart: always

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl
    depends_on:
      - app
    restart: always

部署到服务器

# 1. 复制项目到服务器
scp -r my-flask-app user@server:/opt/

# 2. 在服务器上启动
ssh user@server
cd /opt/my-flask-app
docker-compose -f docker-compose.prod.yml up -d

# 3. 配置反向代理(可选)
# 使用nginx做反向代理和负载均衡

配置Nginx反向代理

nginx.conf

events {
    worker_connections 1024;
}

http {
    upstream flask_app {
        server app:8000;
    }

    server {
        listen 80;
        server_name example.com;

        location / {
            proxy_pass http://flask_app;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}

本章小结

  • 项目结构:Flask应用 + MySQL + Redis
  • Dockerfile:多阶段构建,优化镜像体积
  • Docker Compose:编排多个服务
  • 数据持久化:使用数据卷保存数据
  • 生产部署:配置文件分离,使用Nginx反向代理

现在已经会部署Python项目了,下一章我们来实战完整的Web应用!

继续学下去,马上就能处理更复杂的项目了!