完整的Web应用部署方案

上一章我们部署了Python后端API,今天我们来做前后端分离的完整Web应用部署。

项目架构

┌─────────────────────────────────────────────────────────┐
│                        Nginx                             │
│                    (反向代理 + SSL)                      │
│                     ┌────────┬────────┐                 │
│                     │        │        │                 │
│                     ↓        ↓        ↓                 │
│                  React    Flask     Admin              │
│                  (前端)   (API)   (后台)                │
│                     │        │        │                 │
│                     └────────┴────────┘                 │
│                              ↓                          │
│                    ┌──────────────────┐                 │
│                    │   MySQL + Redis   │                 │
│                    └──────────────────┘                 │
└─────────────────────────────────────────────────────────┘

1. 前端项目(React)

项目结构

frontend/
├── package.json
├── Dockerfile
├── nginx.conf
└── src/
    ├── App.js
    └── index.js

Dockerfile

# 构建阶段
FROM node:18-alpine AS builder

WORKDIR /app

# 复制依赖文件
COPY package.json package-lock.json ./

# 安装依赖
RUN npm ci --only=production

# 复制源代码
COPY . .

# 构建应用
RUN npm run build

# 运行阶段
FROM nginx:alpine

# 复制构建产物
COPY --from=builder /app/build /usr/share/nginx/html

# 复制nginx配置
COPY nginx.conf /etc/nginx/conf.d/default.conf

# 暴露端口
EXPOSE 80

# 启动nginx
CMD ["nginx", "-g", "daemon off;"]

nginx.conf

server {
    listen 80;
    server_name localhost;

    root /usr/share/nginx/html;
    index index.html;

    # Gzip压缩
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json;

    # 路由配置(React Router)
    location / {
        try_files $uri $uri/ /index.html;
    }

    # API代理
    location /api/ {
        proxy_pass http://api:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # 静态资源缓存
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

package.json

{
  "name": "frontend",
  "version": "1.0.0",
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.14.0",
    "axios": "^1.4.0"
  },
  "devDependencies": {
    "react-scripts": "5.0.1"
  }
}

2. 后端API(Flask)

项目结构

backend/
├── app.py
├── requirements.txt
└── Dockerfile

Dockerfile

FROM python:3.11-slim

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

# 复制应用代码
COPY . .

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

# 环境变量
ENV PYTHONUNBUFFERED=1

# 暴露端口
EXPOSE 8000

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

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. 管理后台(Flask Admin)

项目结构

admin/
├── app.py
├── requirements.txt
└── Dockerfile

Dockerfile

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

RUN groupadd -r admin && useradd -r -g admin admin
RUN chown -R admin:admin /app
USER admin

ENV PYTHONUNBUFFERED=1

EXPOSE 8001

CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:8001", "app:app"]

requirements.txt

flask==2.3.0
flask-admin==1.6.1
flask-sqlalchemy==3.0.5
mysql-connector-python==8.1.0
gunicorn==21.2.0

4. Nginx反向代理

nginx/nginx.conf

events {
    worker_connections 1024;
}

http {
    # 前端
    upstream frontend {
        server frontend:80;
    }

    # 后端API
    upstream api {
        server api:8000;
    }

    # 管理后台
    upstream admin {
        server admin:8001;
    }

    # 限流配置
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

    server {
        listen 80;
        server_name example.com www.example.com;

        # SSL重定向
        return 301 https://$server_name$request_uri;
    }

    server {
        listen 443 ssl http2;
        server_name example.com www.example.com;

        # SSL证书配置
        ssl_certificate /etc/nginx/ssl/cert.pem;
        ssl_certificate_key /etc/nginx/ssl/key.pem;

        # SSL优化
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers on;
        ssl_session_cache shared:SSL:10m;
        ssl_session_timeout 10m;

        # Gzip压缩
        gzip on;
        gzip_vary on;
        gzip_min_length 1024;
        gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json;

        # 前端
        location / {
            proxy_pass http://frontend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # API限流
        location /api/ {
            limit_req zone=api_limit burst=20 nodelay;

            proxy_pass http://api;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            # 超时配置
            proxy_connect_timeout 60s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;
        }

        # 管理后台
        location /admin/ {
            proxy_pass http://admin/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            # IP白名单(可选)
            # allow 192.168.1.0/24;
            # deny all;
        }

        # 日志配置
        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;
    }
}

5. Docker Compose编排

docker-compose.yml

version: '3.8'

services:
  # Nginx反向代理
  nginx:
    image: nginx:alpine
    container_name: nginx-proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/ssl:/etc/nginx/ssl
      - nginx-logs:/var/log/nginx
    depends_on:
      - frontend
      - api
      - admin
    networks:
      - app-network
    restart: unless-stopped

  # 前端
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    container_name: frontend-app
    networks:
      - app-network
    restart: unless-stopped

  # 后端API
  api:
    build:
      context: ./backend
      dockerfile: Dockerfile
    container_name: api-server
    environment:
      - DB_HOST=db
      - DB_USER=root
      - DB_PASSWORD=${DB_PASSWORD:-123456}
      - DB_NAME=${DB_NAME:-mydb}
      - REDIS_HOST=redis
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - app-network
    restart: unless-stopped

  # 管理后台
  admin:
    build:
      context: ./admin
      dockerfile: Dockerfile
    container_name: admin-panel
    environment:
      - DB_HOST=db
      - DB_USER=root
      - DB_PASSWORD=${DB_PASSWORD:-123456}
      - DB_NAME=${DB_NAME:-mydb}
    depends_on:
      db:
        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
    volumes:
      - redis-data:/data
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-123456}
    ports:
      - "${REDIS_PORT:-6379}:6379"
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-123456}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3
    restart: unless-stopped

networks:
  app-network:
    driver: bridge

volumes:
  mysql-data:
  redis-data:
  nginx-logs:

6. 数据库初始化

init.sql

-- 创建数据库表
USE mydb;

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

-- 文章表
CREATE TABLE IF NOT EXISTS articles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    content TEXT,
    author_id INT,
    status ENUM('draft', 'published') DEFAULT 'draft',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 评论表
CREATE TABLE IF NOT EXISTS comments (
    id INT AUTO_INCREMENT PRIMARY KEY,
    article_id INT,
    user_id INT,
    content TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 插入测试数据
INSERT INTO users (username, email, password) VALUES
('admin', 'admin@example.com', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyY6X7yGKJ2i'),
('user1', 'user1@example.com', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyY6X7yGKJ2i');

INSERT INTO articles (title, content, author_id, status) VALUES
('第一篇文章', '这是第一篇文章的内容', 1, 'published'),
('第二篇文章', '这是第二篇文章的内容', 1, 'published'),
('草稿文章', '这是一篇草稿', 1, 'draft');

7. 部署和测试

构建和启动

# 构建所有镜像
docker-compose build

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

# 查看状态
docker-compose ps

# 查看日志
docker-compose logs -f

测试各服务

# 测试前端
curl http://localhost

# 测试API健康检查
curl http://localhost/api/health

# 测试管理后台
curl http://localhost/admin/

# 查看数据库
docker exec -it mysql-db mysql -uroot -p123456 -e "USE mydb; SELECT * FROM users;"

性能测试

# 使用ab进行压力测试
ab -n 1000 -c 100 http://localhost/api/health

# 查看Nginx日志
docker exec nginx-proxy tail -f /var/log/nginx/access.log

8. 监控和日志

安装监控工具

# 添加到docker-compose.yml
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus-data:/prometheus
    ports:
      - "9090:9090"
    networks:
      - app-network

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    volumes:
      - grafana-data:/var/lib/grafana
    ports:
      - "3000:3000"
    networks:
      - app-network

volumes:
  prometheus-data:
  grafana-data:

日志聚合

# 查看所有服务日志
docker-compose logs

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

# 导出日志
docker-compose logs --since 24h > logs.txt

9. 备份和恢复

数据库备份

# 备份数据库
docker exec mysql-db mysqldump -uroot -p123456 mydb > backup_$(date +%Y%m%d).sql

# 恢复数据库
docker exec -i mysql-db mysql -uroot -p123456 mydb < backup_20231201.sql

定时备份

# 添加到crontab
0 2 * * * /path/to/backup.sh

# backup.sh内容
#!/bin/bash
BACKUP_DIR="/data/backups"
DATE=$(date +%Y%m%d)
docker exec mysql-db mysqldump -uroot -p123456 mydb > ${BACKUP_DIR}/backup_${DATE}.sql
find ${BACKUP_DIR} -name "backup_*.sql" -mtime +7 -delete

10. SSL证书配置

使用Let’s Encrypt

# 安装certbot
sudo apt-get install certbot

# 获取证书
sudo certbot certonly --standalone -d example.com -d www.example.com

# 复制证书到项目
sudo cp /etc/letsencrypt/live/example.com/fullchain.pem ./nginx/ssl/cert.pem
sudo cp /etc/letsencrypt/live/example.com/privkey.pem ./nginx/ssl/key.pem

本章小结

  • 完整架构:前端 + 后端 + 后台 + 数据库 + 缓存
  • 反向代理:Nginx + SSL + 负载均衡 + 限流
  • 部署方案:Docker Compose一键部署
  • 监控日志:Prometheus + Grafana + 日志聚合
  • 备份恢复:数据库备份 + 定时任务

现在已经掌握了完整Web应用部署,下一章我们来实战微服务架构!

继续学下去,马上就能处理企业级项目了!