辣评 Docker 容器化与部署方案(二十六)

 

Docker 容器化概述

为了简化部署流程、提高环境一致性和便于扩展,我们为辣评项目实现了完整的 Docker 容器化方案。本文详细记录了 Docker 镜像构建、容器编排和部署的完整过程。

容器化目标

  • 简化部署流程
  • 保证环境一致性
  • 便于横向扩展
  • 提高资源利用率
  • 简化运维管理

Docker 镜像构建

多阶段构建 Dockerfile

# Dockerfile
# 多阶段构建,减小最终镜像体积

# 阶段1:构建前端
FROM node:18-alpine AS frontend-builder

WORKDIR /app/admin-vue

# 复制前端依赖文件
COPY admin-vue/package*.json ./

# 安装依赖
RUN npm install --registry=https://registry.npmmirror.com

# 复制前端源码
COPY admin-vue/ ./

# 构建前端
RUN npm run build

# 阶段2:构建后端
FROM golang:1.21-alpine AS backend-builder

WORKDIR /app

# 安装构建依赖
RUN apk add --no-cache git

# 复制 Go 模块文件
COPY go.mod go.sum ./

# 下载依赖
RUN go mod download

# 复制后端源码
COPY cmd/ ./cmd/
COPY VERSION ./

# 构建后端
RUN cd cmd/server && \
    CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o /app/laping-server main.go

# 阶段3:最终镜像
FROM alpine:latest

# 安装运行时依赖
RUN apk --no-cache add \
    ca-certificates \
    tzdata \
    sqlite \
    && rm -rf /var/cache/apk/*

# 设置时区
ENV TZ=Asia/Shanghai

# 创建应用目录
WORKDIR /app

# 从构建阶段复制文件
COPY --from=backend-builder /app/laping-server .
COPY --from=frontend-builder /app/admin-vue/dist ./frontend
COPY config.json.example ./config.json
COPY VERSION .

# 创建数据目录
RUN mkdir -p /app/data /app/uploads

# 设置权限
RUN chmod +x /app/laping-server

# 暴露端口
EXPOSE 8888

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:8888/api/health || exit 1

# 启动应用
CMD ["./laping-server"]

构建脚本

#!/bin/bash
# docker-build.sh

set -e

echo "========================================="
echo "辣评 Docker 镜像构建脚本"
echo "========================================="

# 读取版本号
VERSION=$(cat VERSION 2>/dev/null || echo "latest")
echo "版本号:$VERSION"

# 镜像名称
IMAGE_NAME="laping"
IMAGE_TAG="${IMAGE_NAME}:${VERSION}"
IMAGE_LATEST="${IMAGE_NAME}:latest"

# 构建镜像
echo ""
echo "构建 Docker 镜像..."
docker build -t ${IMAGE_TAG} -t ${IMAGE_LATEST} .

# 显示镜像信息
echo ""
echo "镜像构建完成:"
docker images | grep ${IMAGE_NAME}

echo ""
echo "========================================="
echo "构建完成!"
echo "镜像标签:"
echo "  - ${IMAGE_TAG}"
echo "  - ${IMAGE_LATEST}"
echo ""
echo "运行命令:"
echo "  docker run -d -p 8888:8888 --name laping ${IMAGE_TAG}"
echo "========================================="

docker-compose 配置

基础配置

# docker-compose.yml
version: '3.8'

services:
  laping:
    build:
      context: .
      dockerfile: Dockerfile
    image: laping:latest
    container_name: laping-server
    restart: unless-stopped

    ports:
      - "8888:8888"

    volumes:
      # 数据持久化
      - ./data:/app/data
      - ./uploads:/app/uploads
      # 配置文件
      - ./config.json:/app/config.json:ro
      # 日志
      - ./logs:/app/logs

    environment:
      - TZ=Asia/Shanghai
      - GIN_MODE=release

    networks:
      - laping-network

    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8888/api/health"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 10s

networks:
  laping-network:
    driver: bridge

生产环境配置

# docker-compose.prod.yml
version: '3.8'

services:
  laping:
    image: laping:1.5.0
    container_name: laping-prod
    restart: always

    ports:
      - "8888:8888"

    volumes:
      - /opt/laping/data:/app/data
      - /opt/laping/uploads:/app/uploads
      - /opt/laping/config.json:/app/config.json:ro
      - /opt/laping/logs:/app/logs

    environment:
      - TZ=Asia/Shanghai
      - GIN_MODE=release

    networks:
      - laping-network

    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 1G
        reservations:
          cpus: '0.5'
          memory: 256M

  # Nginx 反向代理(可选)
  nginx:
    image: nginx:alpine
    container_name: laping-nginx
    restart: always

    ports:
      - "80:80"
      - "443:443"

    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - ./nginx/logs:/var/log/nginx

    depends_on:
      - laping

    networks:
      - laping-network

networks:
  laping-network:
    driver: bridge

Nginx 反向代理

Nginx 配置

# nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    # Gzip 压缩
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml text/javascript
               application/json application/javascript application/xml+rss
               application/rss+xml font/truetype font/opentype
               application/vnd.ms-fontobject image/svg+xml;

    # 上游服务器
    upstream laping_backend {
        server laping:8888;
    }

    # HTTP 服务器
    server {
        listen 80;
        server_name your-domain.com;

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

    # HTTPS 服务器
    server {
        listen 443 ssl http2;
        server_name your-domain.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;

        # 安全头
        add_header Strict-Transport-Security "max-age=31536000" always;
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;

        # 客户端最大上传大小
        client_max_body_size 50M;

        # 反向代理到后端
        location / {
            proxy_pass http://laping_backend;
            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;

            # WebSocket 支持
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";

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

        # 静态文件缓存
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
            proxy_pass http://laping_backend;
            expires 1y;
            add_header Cache-Control "public, immutable";
        }

        # 健康检查
        location /health {
            proxy_pass http://laping_backend/api/health;
            access_log off;
        }
    }
}

环境变量管理

.env 文件

# .env
# 应用配置
APP_ENV=production
APP_PORT=8888

# 数据库配置
DB_PATH=/app/data/laping.db

# JWT 配置
JWT_SECRET=your-secret-key-change-in-production
JWT_EXPIRE_HOURS=24
JWT_REFRESH_HOURS=168

# 管理员配置
ADMIN_USERNAME=admin
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=change-this-password

# 日志配置
LOG_LEVEL=info
LOG_PATH=/app/logs

docker-compose 使用环境变量

# docker-compose.yml
version: '3.8'

services:
  laping:
    image: laping:${VERSION:-latest}
    container_name: laping-${APP_ENV:-dev}

    ports:
      - "${APP_PORT:-8888}:8888"

    environment:
      - APP_ENV=${APP_ENV}
      - JWT_SECRET=${JWT_SECRET}
      - JWT_EXPIRE_HOURS=${JWT_EXPIRE_HOURS}
      - LOG_LEVEL=${LOG_LEVEL}

    env_file:
      - .env

部署最佳实践

部署脚本

#!/bin/bash
# deploy.sh

set -e

echo "========================================="
echo "辣评 Docker 部署脚本"
echo "========================================="

# 检查 Docker 和 docker-compose
if ! command -v docker &> /dev/null; then
    echo "错误:未安装 Docker"
    exit 1
fi

if ! command -v docker-compose &> /dev/null; then
    echo "错误:未安装 docker-compose"
    exit 1
fi

# 读取版本号
VERSION=$(cat VERSION 2>/dev/null || echo "latest")
echo "部署版本:$VERSION"

# 停止旧容器
echo ""
echo "停止旧容器..."
docker-compose down

# 备份数据
echo ""
echo "备份数据..."
BACKUP_DIR="./backups/$(date +%Y%m%d%H%M%S)"
mkdir -p ${BACKUP_DIR}
if [ -d "./data" ]; then
    cp -r ./data ${BACKUP_DIR}/
    echo "数据已备份到:${BACKUP_DIR}"
fi

# 拉取最新镜像(如果使用远程镜像)
# echo ""
# echo "拉取最新镜像..."
# docker-compose pull

# 构建镜像(如果本地构建)
echo ""
echo "构建镜像..."
docker-compose build

# 启动容器
echo ""
echo "启动容器..."
docker-compose up -d

# 等待服务启动
echo ""
echo "等待服务启动..."
sleep 5

# 检查服务状态
echo ""
echo "检查服务状态..."
docker-compose ps

# 检查健康状态
echo ""
echo "检查健康状态..."
for i in {1..10}; do
    if curl -f http://localhost:8888/api/health > /dev/null 2>&1; then
        echo "✓ 服务健康检查通过"
        break
    fi
    if [ $i -eq 10 ]; then
        echo "✗ 服务健康检查失败"
        docker-compose logs --tail=50
        exit 1
    fi
    echo "等待服务就绪... ($i/10)"
    sleep 3
done

# 显示日志
echo ""
echo "最近日志:"
docker-compose logs --tail=20

echo ""
echo "========================================="
echo "部署完成!"
echo "访问地址:http://localhost:8888"
echo ""
echo "常用命令:"
echo "  查看日志:docker-compose logs -f"
echo "  停止服务:docker-compose down"
echo "  重启服务:docker-compose restart"
echo "========================================="

更新脚本

#!/bin/bash
# update.sh

set -e

echo "========================================="
echo "辣评 Docker 更新脚本"
echo "========================================="

# 拉取最新代码
echo "拉取最新代码..."
git pull

# 读取新版本号
NEW_VERSION=$(cat VERSION)
echo "新版本:$NEW_VERSION"

# 备份当前数据
echo ""
echo "备份数据..."
BACKUP_DIR="./backups/$(date +%Y%m%d%H%M%S)"
mkdir -p ${BACKUP_DIR}
cp -r ./data ${BACKUP_DIR}/ 2>/dev/null || true
cp ./config.json ${BACKUP_DIR}/ 2>/dev/null || true

# 停止旧容器
echo ""
echo "停止旧容器..."
docker-compose down

# 构建新镜像
echo ""
echo "构建新镜像..."
docker-compose build

# 启动新容器
echo ""
echo "启动新容器..."
docker-compose up -d

# 清理旧镜像
echo ""
echo "清理旧镜像..."
docker image prune -f

echo ""
echo "========================================="
echo "更新完成!"
echo "新版本:$NEW_VERSION"
echo "========================================="

监控和日志

日志查看

# 查看实时日志
docker-compose logs -f

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

# 查看最近 100 行日志
docker-compose logs --tail=100

# 查看带时间戳的日志
docker-compose logs -f --timestamps

容器监控

# 查看容器状态
docker-compose ps

# 查看容器资源使用
docker stats laping-server

# 查看容器详细信息
docker inspect laping-server

# 进入容器
docker exec -it laping-server sh

故障排查

常见问题

1. 容器无法启动

# 查看详细日志
docker-compose logs laping

# 检查配置文件
docker-compose config

# 检查端口占用
netstat -tulpn | grep 8888

2. 数据库文件权限问题

# 修复权限
sudo chown -R 1000:1000 ./data

3. 内存不足

# 调整资源限制
deploy:
  resources:
    limits:
      memory: 2G

总结

Docker 容器化方案为辣评项目提供了标准化、可复制的部署方式。通过多阶段构建、docker-compose 编排和完善的部署脚本,我们实现了简单、可靠的部署流程。

关键成果

  • 多阶段构建减小镜像体积
  • docker-compose 简化容器编排
  • Nginx 反向代理支持 HTTPS
  • 完善的部署和更新脚本
  • 健康检查和监控机制

技术亮点

  • 多阶段构建优化
  • 数据持久化
  • 环境变量管理
  • 日志和监控
  • 自动化部署

部署优势

  • 环境一致性
  • 快速部署
  • 易于扩展
  • 简化运维
  • 提高可靠性

Docker 容器化方案为辣评项目的部署和运维带来了显著的便利,也为后续的微服务化和云原生改造奠定了基础。

本文遵守 Attribution-NonCommercial 4.0 International 许可协议。 Attribution-NonCommercial 4.0 International