10-实战项目1-Python项目打包部署
用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"]
优化原理:
- 使用slim基础镜像(更小)
- 多阶段构建(分离构建和运行环境)
- 清理缓存(减小镜像体积)
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应用!
继续学下去,马上就能处理更复杂的项目了!