阿里云Docker部署Nuxt4后端golang应用程序

jerry2026年4月5日devops 10 次阅读 约 105 分钟
阿里云Docker部署Nuxt4后端golang应用程序

阿里云 Ubuntu 服务器 Docker 部署指南

本文档详细说明如何将博客平台部署到阿里云 Ubuntu 服务器上,使用 Docker Compose 一键部署。

docker compose down \docker compose up -d \ docker compose restart 讲解这三个命令 我用最简单、最直白、最实用的方式给你讲清楚这三个命令,你以后再也不会混淆! 一、docker compose down 作用:彻底停止 + 删除所有容器、网络相当于:关机 + 把房子拆了 会做什么 停止所有服务 删除所有容器 删除自动创建的网络 保留你的数据、镜像、代码(不会删) 什么时候用 改了 Dockerfile 改了 docker-compose.yml 改了 .env 环境变量 想彻底重建环境 服务出问题,需要干净重启 二、docker compose up -d 作用:创建 + 启动所有服务(后台运行)相当于:重新盖房子 + 开机 会做什么 读取最新的 docker-compose.yml 读取最新的 .env 重新创建容器 后台运行(不占用终端) 什么时候用 第一次启动 down 之后重新启动 改了配置必须用这个 三、docker compose restart 作用:只重启容器,不重建相当于:电脑重启 会做什么 只重启运行中的容器 不会重新加载配置文件 不会读取新的 .env 不会重新创建容器 什么时候用 什么都没改,只想重启服务 服务卡住了,但配置没变 超级重点(你必须知道!) ❌ 错误做法(你之前大概率踩坑了) 改了 .env / 改了 nginx.conf / 改了 docker-compose.yml plaintext docker compose restart <<< 没用!不会生效! 因为 restart 不重新读取配置! ✅ 正确做法(改配置必须用) plaintext docker compose down docker compose up -d 这两句才会 重新读取所有最新配置! 一句话总结 down:停止并删除容器 up -d:重新创建并启动容器(读最新配置) restart:只重启,不读新配置

什么时候需要删除掉挂载的容器文件夹 我给你用最直白、最实用、你马上能用到的方式讲清楚:到底什么时候必须删除挂载的文件夹 / 容器数据。 一、什么时候 必须删除 挂载的文件夹? 只要你改了下面这些东西,就必须删除旧的挂载文件夹,否则改了等于白改!

  1. 你改了 .env 环境变量 → 必须删除容器 / 重新挂载因为环境变量是启动时一次性加载,不会热更新。
  2. 你改了 Nginx 配置文件(nginx.conf) → 必须删除旧容器,重建否则 Nginx 一直用旧配置,你改本地文件没用。
  3. 你改了 前端代码、后端代码、打包后的镜像 → 必须删除旧容器,重新 up不然容器还在运行旧版本。
  4. 你发现 配置怎么改都不生效 → 直接删除挂载目录 + 重建容器99% 是缓存 / 旧文件在捣乱。
  5. 容器启动报错、文件权限错误 → 删除挂载目录重建最快速的解决办法。 二、什么时候 不需要删除? 只是重启服务 只是临时改运行参数 只是看日志 这些都不用删文件夹。 三、你现在的情况 必须删除! 因为你: 改了 .env 改了 Nginx 配置 改了前端接口地址 不删除旧容器,你改的所有东西都不生效!什么时候需要删除掉挂载的容器文件夹 我给你用最直白、最实用、你马上能用到的方式讲清楚:到底什么时候必须删除挂载的文件夹 / 容器数据。 一、什么时候 必须删除 挂载的文件夹? 只要你改了下面这些东西,就必须删除旧的挂载文件夹,否则改了等于白改!
  6. 你改了 .env 环境变量 → 必须删除容器 / 重新挂载因为环境变量是启动时一次性加载,不会热更新。
  7. 你改了 Nginx 配置文件(nginx.conf) → 必须删除旧容器,重建否则 Nginx 一直用旧配置,你改本地文件没用。
  8. 你改了 前端代码、后端代码、打包后的镜像 → 必须删除旧容器,重新 up不然容器还在运行旧版本。
  9. 你发现 配置怎么改都不生效 → 直接删除挂载目录 + 重建容器99% 是缓存 / 旧文件在捣乱。
  10. 容器启动报错、文件权限错误 → 删除挂载目录重建最快速的解决办法。 二、什么时候 不需要删除? 只是重启服务 只是临时改运行参数 只是看日志 这些都不用删文件夹。 三、你现在的情况 必须删除! 因为你: 改了 .env 改了 Nginx 配置 改了前端接口地址 不删除旧容器,你改的所有东西都不生效!

目录

  1. 服务器要求
  2. 服务器初始化
  3. 安装 Docker
  4. 上传项目代码
  5. 配置生产环境
  6. 构建和启动
  7. 初始化管理员
  8. 配置 Nginx 反向代理
  9. 配置 SSL 证书
  10. 防火墙配置
  11. 日常运维
  12. 故障排查

1. 服务器要求

项目 最低配置 推荐配置
CPU 1 核 2 核+
内存 2 GB 4 GB+
磁盘 40 GB SSD 80 GB+ SSD
系统 Ubuntu 22.04 LTS Ubuntu 22.04/24.04 LTS
带宽 1 Mbps 3 Mbps+

需要开放的端口:

  • 22(SSH)
  • 80(HTTP)
  • 443(HTTPS)

2. 服务器初始化

2.1 连接服务器

ssh root@你的服务器IP

2.2 更新系统

apt update && apt upgrade -y

2.3 创建部署用户(推荐,不用 root 运行服务)

adduser deploy
usermod -aG sudo deploy
su - deploy

2.4 设置时区

sudo timedatectl set-timezone Asia/Shanghai

3. 安装 Docker

3.1 安装 Docker Engine

# 安装依赖
sudo apt install -y ca-certificates curl gnupg

# 添加 Docker 官方 GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# 添加 Docker 仓库
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 安装
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

3.2 配置非 root 用户使用 Docker

sudo usermod -aG docker deploy
# 重新登录生效
exit
su - deploy

3.3 验证安装

docker --version
docker compose version

3.4 配置 Docker 镜像加速(阿里云)

登录阿里云容器镜像服务控制台获取加速地址,然后:

sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
  "registry-mirrors": ["https://你的加速地址.mirror.aliyuncs.com"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "3"
  }
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker

4. 上传项目代码

方式一:Git 拉取(推荐)

cd /home/deploy
git clone 你的仓库地址 blog
cd blog

方式二:本地打包上传

在本地执行:

# 排除 node_modules 和其他不需要的文件
tar --exclude='node_modules' --exclude='.nuxt' --exclude='.output' \
    --exclude='*.exe' --exclude='logs' --exclude='.docker-data' \
    -czf blog.tar.gz .

# 上传到服务器
scp blog.tar.gz deploy@你的服务器IP:/home/deploy/

在服务器上:

cd /home/deploy
mkdir blog && cd blog
tar -xzf ../blog.tar.gz
rm ../blog.tar.gz

5. 配置生产环境

5.1 创建生产环境配置文件

cd /home/deploy/blog

创建 .env 文件(Docker Compose 会自动读取):

cat > .env << 'EOF'
# ========== 数据库 ==========
POSTGRES_DB=blog
POSTGRES_USER=blog_prod
POSTGRES_PASSWORD=这里改成一个强密码

# ========== 后端 ==========
DB_HOST=postgres
DB_PORT=5432
DB_USER=blog_prod
DB_PASSWORD=这里改成和上面一样的密码
DB_NAME=blog
JWT_SECRET=这里改成一个随机字符串至少32位
JWT_REFRESH_SECRET=这里改成另一个随机字符串至少32位
SERVER_PORT=8080
GIN_MODE=release
LOG_LEVEL=info

# ========== 存储 ==========
STORAGE_TYPE=s3
S3_ENDPOINT=minio:9000
S3_BUCKET=blog-images
S3_ACCESS_KEY=这里改成MinIO访问密钥
S3_SECRET_KEY=这里改成MinIO密钥至少8位
S3_REGION=us-east-1
S3_USE_SSL=false

# ========== MinIO ==========
MINIO_ROOT_USER=这里和S3_ACCESS_KEY一致
MINIO_ROOT_PASSWORD=这里和S3_SECRET_KEY一致

# ========== 管理员 ==========
ADMIN_USERNAME=admin
ADMIN_EMAIL=你的邮箱@example.com
ADMIN_PASSWORD=这里改成管理员密码至少8位

# ========== 前端 ==========
NUXT_PUBLIC_API_BASE=http://blog-server:8080/api/v1
NUXT_PUBLIC_SITE_URL=https://你的域名.com
EOF

生成随机密钥的方法:

# 生成 JWT Secret
openssl rand -hex 32

# 生成数据库密码
openssl rand -base64 16

5.2 创建生产环境 docker-compose 覆盖文件

cat > docker-compose.prod.yml << 'EOF'
services:
  postgres:
    image: postgres:16-alpine
    restart: always
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./.docker-data/pg-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
    # 生产环境不暴露端口到宿主机
    # ports:
    #   - "5432:5432"

  blog-server:
    build:
      context: ./src/blog-server
      dockerfile: Dockerfile
    restart: always
    environment:
      DB_HOST: ${DB_HOST}
      DB_PORT: ${DB_PORT}
      DB_USER: ${DB_USER}
      DB_PASSWORD: ${DB_PASSWORD}
      DB_NAME: ${DB_NAME}
      JWT_SECRET: ${JWT_SECRET}
      JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET}
      SERVER_PORT: ${SERVER_PORT}
      GIN_MODE: ${GIN_MODE}
      LOG_LEVEL: ${LOG_LEVEL}
      STORAGE_TYPE: ${STORAGE_TYPE}
      S3_ENDPOINT: ${S3_ENDPOINT}
      S3_BUCKET: ${S3_BUCKET}
      S3_ACCESS_KEY: ${S3_ACCESS_KEY}
      S3_SECRET_KEY: ${S3_SECRET_KEY}
      S3_REGION: ${S3_REGION}
      S3_USE_SSL: ${S3_USE_SSL}
      ADMIN_USERNAME: ${ADMIN_USERNAME}
      ADMIN_EMAIL: ${ADMIN_EMAIL}
      ADMIN_PASSWORD: ${ADMIN_PASSWORD}
    volumes:
      - ./.docker-data/blog-logs:/app/logs
      - ./.docker-data/blog-uploads:/app/uploads
    depends_on:
      postgres:
        condition: service_healthy
      minio:
        condition: service_started
    # 生产环境不暴露端口,通过 Nginx 反代
    # ports:
    #   - "8080:8080"

  blog-web:
    build:
      context: ./src/blog-web
      dockerfile: Dockerfile
    restart: always
    environment:
      NUXT_PUBLIC_API_BASE: ${NUXT_PUBLIC_API_BASE}
      NUXT_PUBLIC_SITE_URL: ${NUXT_PUBLIC_SITE_URL}
    depends_on:
      - blog-server
    # 生产环境不暴露端口,通过 Nginx 反代
    # ports:
    #   - "3000:3000"

  minio:
    image: minio/minio:RELEASE.2025-04-22T22-12-26Z
    restart: always
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
    volumes:
      - ./.docker-data/minio-data:/data
    # 生产环境不暴露端口
    # ports:
    #   - "9000:9000"
    #   - "9001:9001"

  nginx:
    image: nginx:alpine
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./certbot/conf:/etc/letsencrypt:ro
      - ./certbot/www:/var/www/certbot:ro
    depends_on:
      - blog-web
      - blog-server
EOF

5.3 创建数据目录

mkdir -p .docker-data/pg-data .docker-data/minio-data .docker-data/blog-logs .docker-data/blog-uploads

6. 构建和启动

6.1 首次构建

cd /home/deploy/blog

# 使用生产配置构建并启动
docker compose -f docker-compose.prod.yml build

# 启动所有服务
docker compose -f docker-compose.prod.yml up -d

6.2 查看服务状态

docker compose -f docker-compose.prod.yml ps

所有服务应显示 Up 状态。

6.3 查看日志

# 查看所有服务日志
docker compose -f docker-compose.prod.yml logs -f

# 查看单个服务日志
docker compose -f docker-compose.prod.yml logs -f blog-server
docker compose -f docker-compose.prod.yml logs -f blog-web
docker compose -f docker-compose.prod.yml logs -f postgres

7. 初始化管理员

首次部署需要运行种子脚本创建管理员账号:

docker compose -f docker-compose.prod.yml exec blog-server ./seed

成功后会显示:管理员用户创建成功: admin (你的邮箱@example.com)


8. 配置 Nginx 反向代理

8.1 创建 Nginx 配置目录

mkdir -p nginx/conf.d

8.2 创建主配置文件

cat > nginx/nginx.conf << 'EOF'
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;
    keepalive_timeout 65;
    client_max_body_size 50m;

    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml;

    include /etc/nginx/conf.d/*.conf;
}
EOF

8.3 创建站点配置(先用 HTTP,后面加 SSL)

cat > nginx/conf.d/blog.conf << 'EOF'
upstream blog_web {
    server blog-web:3000;
}

upstream blog_api {
    server blog-server:8080;
}

server {
    listen 80;
    server_name 你的域名.com www.你的域名.com;

    # Let's Encrypt 验证
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    # API 请求代理到后端
    location /api/ {
        proxy_pass http://blog_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;
    }

    # Swagger 文档
    location /swagger/ {
        proxy_pass http://blog_api;
        proxy_set_header Host $host;
    }

    # 健康检查
    location /health {
        proxy_pass http://blog_api;
    }

    # 上传文件(本地存储模式)
    location /uploads/ {
        proxy_pass http://blog_api;
    }

    # 其他所有请求代理到前端
    location / {
        proxy_pass http://blog_web;
        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_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
EOF

8.4 重启 Nginx

docker compose -f docker-compose.prod.yml restart nginx

9. 配置 SSL 证书

9.1 安装 Certbot

sudo apt install -y certbot

9.2 获取证书

mkdir -p certbot/conf certbot/www

# 先确保 Nginx 已启动且 80 端口可访问
sudo certbot certonly --webroot \
  -w ./certbot/www \
  -d 你的域名.com \
  -d www.你的域名.com \
  --email 你的邮箱@example.com \
  --agree-tos \
  --no-eff-email

9.3 更新 Nginx 配置启用 HTTPS

cat > nginx/conf.d/blog.conf << 'EOF'
upstream blog_web {
    server blog-web:3000;
}

upstream blog_api {
    server blog-server:8080;
}

# HTTP -> HTTPS 重定向
server {
    listen 80;
    server_name 你的域名.com www.你的域名.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

# HTTPS
server {
    listen 443 ssl http2;
    server_name 你的域名.com www.你的域名.com;

    ssl_certificate /etc/letsencrypt/live/你的域名.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/你的域名.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # API
    location /api/ {
        proxy_pass http://blog_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;
    }

    location /swagger/ {
        proxy_pass http://blog_api;
        proxy_set_header Host $host;
    }

    location /health {
        proxy_pass http://blog_api;
    }

    location /uploads/ {
        proxy_pass http://blog_api;
    }

    # 前端
    location / {
        proxy_pass http://blog_web;
        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_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
EOF

9.4 设置证书自动续期

# 添加 crontab 定时任务
(crontab -l 2>/dev/null; echo "0 3 * * * certbot renew --quiet && docker compose -f /home/deploy/blog/docker-compose.prod.yml restart nginx") | crontab -

10. 防火墙配置

10.1 阿里云安全组

在阿里云 ECS 控制台 → 安全组 → 入方向规则,添加:

协议 端口 来源 说明
TCP 22 你的 IP/0.0.0.0 SSH
TCP 80 0.0.0.0/0 HTTP
TCP 443 0.0.0.0/0 HTTPS

不要开放 5432(数据库)、8080(后端)、3000(前端)、9000/9001(MinIO)到公网。

10.2 服务器防火墙(UFW)

sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status

11. 日常运维

11.1 更新部署

cd /home/deploy/blog

# 拉取最新代码
git pull

# 重新构建并启动
docker compose -f docker-compose.prod.yml build
docker compose -f docker-compose.prod.yml up -d

11.2 查看后端日志

# Docker 日志
docker compose -f docker-compose.prod.yml logs -f blog-server --tail 100

# 应用日志文件(按日期和级别分文件)
ls .docker-data/blog-logs/
cat .docker-data/blog-logs/$(date +%Y-%m-%d)/all.log
cat .docker-data/blog-logs/$(date +%Y-%m-%d)/error.log

11.3 数据库备份

# 手动备份
docker compose -f docker-compose.prod.yml exec postgres \
  pg_dump -U blog_prod blog > backup_$(date +%Y%m%d).sql

# 自动备份(添加 crontab)
mkdir -p /home/deploy/backups
(crontab -l 2>/dev/null; echo "0 2 * * * docker compose -f /home/deploy/blog/docker-compose.prod.yml exec -T postgres pg_dump -U blog_prod blog > /home/deploy/backups/blog_\$(date +\%Y\%m\%d).sql && find /home/deploy/backups -name '*.sql' -mtime +30 -delete") | crontab -

11.4 数据库恢复

cat backup_20260403.sql | docker compose -f docker-compose.prod.yml exec -T postgres psql -U blog_prod blog

11.5 重启服务

# 重启所有
docker compose -f docker-compose.prod.yml restart

# 重启单个
docker compose -f docker-compose.prod.yml restart blog-server

11.6 查看磁盘使用

# Docker 磁盘使用
docker system df

# 清理无用镜像
docker system prune -f

# 数据目录大小
du -sh .docker-data/*

12. 故障排查

12.1 服务无法启动

# 查看详细日志
docker compose -f docker-compose.prod.yml logs blog-server
docker compose -f docker-compose.prod.yml logs blog-web

# 检查容器状态
docker compose -f docker-compose.prod.yml ps -a

12.2 数据库连接失败

# 检查 PostgreSQL 是否运行
docker compose -f docker-compose.prod.yml exec postgres pg_isready

# 检查连接
docker compose -f docker-compose.prod.yml exec blog-server wget -qO- http://localhost:8080/health

12.3 前端 502 错误

通常是后端还没启动完成,等几秒再试。如果持续 502:

# 检查后端是否正常
docker compose -f docker-compose.prod.yml exec blog-server wget -qO- http://localhost:8080/health

# 检查 Nginx 日志
docker compose -f docker-compose.prod.yml logs nginx

12.4 图片/音频上传失败

# 检查 MinIO 状态
docker compose -f docker-compose.prod.yml logs minio

# 检查存储配置
docker compose -f docker-compose.prod.yml exec blog-server cat config.yaml

12.5 内存不足

# 查看内存使用
free -h
docker stats --no-stream

如果内存紧张,可以限制容器内存:

# 在 docker-compose.prod.yml 中添加
services:
  blog-server:
    deploy:
      resources:
        limits:
          memory: 512M
  blog-web:
    deploy:
      resources:
        limits:
          memory: 512M

快速部署清单

按顺序执行:

# 1. 连接服务器
ssh root@你的IP

# 2. 安装 Docker
curl -fsSL https://get.docker.com | sh

# 3. 上传代码
git clone 你的仓库 /home/deploy/blog && cd /home/deploy/blog

# 4. 配置环境变量
cp .env.example .env && vim .env

# 5. 创建数据目录
mkdir -p .docker-data/{pg-data,minio-data,blog-logs,blog-uploads} nginx/conf.d certbot/{conf,www}

# 6. 创建 Nginx 配置
# (参考第 8 节)

# 7. 构建启动
docker compose -f docker-compose.prod.yml build
docker compose -f docker-compose.prod.yml up -d

# 8. 初始化管理员
docker compose -f docker-compose.prod.yml exec blog-server ./seed

# 9. 验证
curl http://你的IP/health

方案二:镜像部署(推荐,无需上传源码)

本方案在本地构建 Docker 镜像,导出为文件上传到服务器直接运行。服务器上不需要源码、不需要 Go/Node.js 环境。

整体流程

本地开发机 (Windows/Mac/Linux)          阿里云 Ubuntu 服务器
┌──────────────────────┐              ┌──────────────────────────┐
│ 1. docker build       │              │ 4. docker load            │
│ 2. docker save        │── scp ──>   │ 5. docker compose up -d   │
│ 3. 生成 tar.gz 文件    │              │                           │
└──────────────────────┘              │ 服务器上只需要:            │
                                      │   .env (密码配置)          │
                                      │   docker-compose.yml       │
                                      │   nginx 配置文件           │
                                      └──────────────────────────┘

第一步:本地构建镜像

在你的开发机项目根目录执行:

# 构建后端镜像(Go 编译 + Alpine 运行环境)
docker build -t blog-server:latest ./src/blog-server

# 构建前端镜像(Node 构建 + 精简运行环境)
docker build -t blog-web:latest ./src/blog-web

# 验证镜像
docker images | grep blog

构建完成后两个镜像大约:

  • blog-server: ~30MB(Go 静态编译 + Alpine)
  • blog-web: ~200MB(Node.js + Nuxt SSR)

第二步:导出镜像

# 导出并压缩
docker save blog-server:latest | gzip > blog-server.tar.gz
docker save blog-web:latest | gzip > blog-web.tar.gz

# 查看文件大小
ls -lh blog-*.tar.gz

第三步:上传到服务器

scp blog-server.tar.gz blog-web.tar.gz root@你的服务器IP:/root/

第四步:服务器上加载镜像

SSH 连接到服务器后:

# 加载镜像
docker load < /root/blog-server.tar.gz
docker load < /root/blog-web.tar.gz

# 清理 tar 文件
rm /root/blog-server.tar.gz /root/blog-web.tar.gz

# 验证
docker images | grep blog

第五步:创建部署目录和配置

# 创建部署目录
mkdir -p /opt/blog
cd /opt/blog

# 创建数据持久化目录
mkdir -p .docker-data/{pg-data,minio-data,blog-logs,blog-uploads}
mkdir -p nginx/conf.d
mkdir -p certbot/{conf,www}

5.1 创建 .env 文件

cat > .env << 'ENVEOF'
# ===== 数据库 =====
POSTGRES_DB=blog
POSTGRES_USER=blog_prod
# 用 openssl rand -hex 16 生成
POSTGRES_PASSWORD=luanhj

# ===== 后端 =====
DB_HOST=postgres
DB_PORT=5432
DB_USER=blog_prod
DB_PASSWORD=luanhj
DB_NAME=blog
# 用 openssl rand -hex 32 生成
JWT_SECRET=luanhongjie123123
JWT_REFRESH_SECRET=luanhj123123
SERVER_PORT=8080
GIN_MODE=release
LOG_LEVEL=info

# ===== 存储 =====
STORAGE_TYPE=local
S3_ENDPOINT=minio:9000
S3_BUCKET=blog-images
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
S3_REGION=us-east-1
S3_USE_SSL=false

# ===== MinIO =====
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=minioadmin

# ===== 管理员(首次seed用) =====
ADMIN_USERNAME=jerry
ADMIN_EMAIL=506670809@qq.com
ADMIN_PASSWORD=luanhj123123

# ===== 前端 =====
NUXT_PUBLIC_API_BASE=/api/v1
NUXT_PUBLIC_SITE_URL=http://47.250.92.89
ENVEOF

用以下命令生成随机密钥:

echo "DB密码: $(openssl rand -hex 16)"
echo "JWT_SECRET: $(openssl rand -hex 32)"
echo "JWT_REFRESH: $(openssl rand -hex 32)"
echo "MinIO密钥: $(openssl rand -hex 12)"

5.2 创建 docker-compose.yml

cat > docker-compose.yml << 'DCEOF'
services:
  postgres:
    image: postgres:16-alpine
    restart: always
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./.docker-data/pg-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5

  minio:
    image: minio/minio:RELEASE.2025-04-22T22-12-26Z
    restart: always
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
    volumes:
      - ./.docker-data/minio-data:/data

  blog-server:
    image: blog-server:latest
    restart: always
    environment:
      DB_HOST: ${DB_HOST}
      DB_PORT: ${DB_PORT}
      DB_USER: ${DB_USER}
      DB_PASSWORD: ${DB_PASSWORD}
      DB_NAME: ${DB_NAME}
      JWT_SECRET: ${JWT_SECRET}
      JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET}
      SERVER_PORT: ${SERVER_PORT}
      GIN_MODE: ${GIN_MODE}
      LOG_LEVEL: ${LOG_LEVEL}
      STORAGE_TYPE: ${STORAGE_TYPE}
      S3_ENDPOINT: ${S3_ENDPOINT}
      S3_BUCKET: ${S3_BUCKET}
      S3_ACCESS_KEY: ${S3_ACCESS_KEY}
      S3_SECRET_KEY: ${S3_SECRET_KEY}
      S3_REGION: ${S3_REGION}
      S3_USE_SSL: ${S3_USE_SSL}
      ADMIN_USERNAME: ${ADMIN_USERNAME}
      ADMIN_EMAIL: ${ADMIN_EMAIL}
      ADMIN_PASSWORD: ${ADMIN_PASSWORD}
    volumes:
      - ./.docker-data/blog-logs:/app/logs
      - ./.docker-data/blog-uploads:/app/uploads
    depends_on:
      postgres:
        condition: service_healthy
      minio:
        condition: service_started

  blog-web:
    image: blog-web:latest
    restart: always
    environment:
      NUXT_PUBLIC_API_BASE: ${NUXT_PUBLIC_API_BASE}
      NUXT_PUBLIC_SITE_URL: ${NUXT_PUBLIC_SITE_URL}
    depends_on:
      - blog-server

  nginx:
    image: nginx:alpine
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./certbot/conf:/etc/letsencrypt:ro
      - ./certbot/www:/var/www/certbot:ro
    depends_on:
      - blog-web
      - blog-server
DCEOF

5.3 创建 Nginx 配置

主配置:

cat > nginx/nginx.conf << 'NGEOF'
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;
    sendfile on;
    keepalive_timeout 65;
    client_max_body_size 50m;

    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss image/svg+xml;

    include /etc/nginx/conf.d/*.conf;
}
NGEOF

站点配置(先 HTTP,后面加 SSL):

cat > nginx/conf.d/blog.conf << 'SITEEOF'
upstream blog_web {
    server blog-web:3000;
}

upstream blog_api {
    server blog-server:8080;
}

server {
    listen 80;
    server_name 47.250.92.89;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location /api/ {
        proxy_pass http://blog_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;
    }

    location /swagger/ {
        proxy_pass http://blog_api;
        proxy_set_header Host $host;
    }

    location /health {
        proxy_pass http://blog_api;
    }

    location /uploads/ {
        proxy_pass http://blog_api;
    }

    location / {
        proxy_pass http://blog_web;
        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_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
SITEEOF

第六步:启动

cd /opt/blog
docker compose up -d

# 查看状态(全部 Up 即成功)
docker compose ps

# 查看日志
docker compose logs -f

第七步:初始化管理员

docker compose exec blog-server ./seed

第八步:验证

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

# 从外网访问
curl http://你的服务器IP/health

浏览器访问 http://你的服务器IP 应该能看到博客首页。


后续:配置 SSL

# 安装 certbot
apt install -y certbot

# 获取证书(确保域名已解析到服务器 IP)
certbot certonly --webroot -w /opt/blog/certbot/www \
  -d 你的域名 --email 你的邮箱 --agree-tos --no-eff-email

# 更新 nginx 配置为 HTTPS(参考方案一第 9 节)
# 重启 nginx
docker compose restart nginx

# 自动续期
(crontab -l 2>/dev/null; echo "0 3 * * * certbot renew --quiet && docker compose -f /opt/blog/docker-compose.yml restart nginx") | crontab -

更新部署

场景一:只改了后端代码(Go)

比如加了新 API、改了业务逻辑、修了 bug。

本地执行:

# 只构建后端
docker build -t blog-server:latest ./src/blog-server
docker save blog-server:latest | gzip > blog-server.tar.gz
scp blog-server.tar.gz root@服务器IP:/root/

服务器执行:

docker load < /root/blog-server.tar.gz
rm /root/blog-server.tar.gz
cd /opt/blog
docker compose up -d blog-server
docker image prune -f

只重启 blog-server,前端和数据库不受影响,用户无感知。

场景二:只改了前端代码(Vue/Nuxt)

比如改了页面样式、加了新页面、修了前端 bug。

本地执行:

# 只构建前端
docker build -t blog-web:latest ./src/blog-web
docker save blog-web:latest | gzip > blog-web.tar.gz
scp blog-web.tar.gz root@服务器IP:/root/

服务器执行:

docker load < /root/blog-web.tar.gz
rm /root/blog-web.tar.gz
cd /opt/blog
docker compose up -d blog-web
docker image prune -f

场景三:前后端都改了

本地执行:

docker build -t blog-server:latest ./src/blog-server
docker build -t blog-web:latest ./src/blog-web
docker save blog-server:latest | gzip > blog-server.tar.gz
docker save blog-web:latest | gzip > blog-web.tar.gz
scp blog-server.tar.gz blog-web.tar.gz root@服务器IP:/root/

服务器执行:

docker load < /root/blog-server.tar.gz
docker load < /root/blog-web.tar.gz
rm /root/*.tar.gz
cd /opt/blog
docker compose up -d
docker image prune -f

场景四:新功能涉及数据库表变更

GORM 的 AutoMigrate 会在后端启动时自动创建新表、新增字段。所以:

  • 新增表(如加了音乐、说说功能):直接部署后端,启动时自动建表,无需手动操作
  • 新增字段:同上,AutoMigrate 会自动加列
  • 删除字段/改字段类型:AutoMigrate 不会自动删列或改类型,需要手动执行 SQL

手动执行 SQL 的方法:

# 进入数据库
docker compose exec postgres psql -U blog_prod blog

# 执行 SQL(举例)
ALTER TABLE articles ADD COLUMN new_field VARCHAR(100);
ALTER TABLE articles DROP COLUMN old_field;

# 退出
\q

场景五:需要修改环境变量或配置

比如换了 MinIO 密钥、改了 JWT Secret。

cd /opt/blog

# 编辑 .env
vim .env

# 重启受影响的服务
docker compose up -d

docker compose up -d 会检测环境变量变化,自动重建受影响的容器。

场景六:需要修改 Nginx 配置

比如加了新域名、改了代理规则。

cd /opt/blog

# 编辑配置
vim nginx/conf.d/blog.conf

# 测试配置语法
docker compose exec nginx nginx -t

# 重载(不中断服务)
docker compose exec nginx nginx -s reload

场景七:需要运行数据迁移脚本

比如新功能需要初始化数据。

# 进入后端容器执行
docker compose exec blog-server ./seed

# 或者直接操作数据库
docker compose exec postgres psql -U blog_prod blog -c "INSERT INTO ..."

更新前建议:先备份数据库

cd /opt/blog
docker compose exec -T postgres pg_dump -U blog_prod blog | gzip > backups/before_update_$(date +%Y%m%d%H%M).sql.gz

万一更新出问题可以回滚:

# 回滚数据库
gunzip -c backups/before_update_20260403.sql.gz | docker compose exec -T postgres psql -U blog_prod blog

# 回滚镜像(如果保留了旧的 tar.gz)
docker load < blog-server-old.tar.gz
docker compose up -d blog-server

服务器上的最终目录结构

/opt/blog/
├── .env                          # 环境变量(密码、密钥)
├── docker-compose.yml            # 编排文件
├── nginx/
│   ├── nginx.conf                # Nginx 主配置
│   └── conf.d/
│       └── blog.conf             # 站点配置
├── certbot/
│   ├── conf/                     # SSL 证书
│   └── www/                      # ACME 验证
├── backups/                      # 数据库备份
└── .docker-data/
    ├── pg-data/                  # PostgreSQL 数据(持久化)
    ├── minio-data/               # MinIO 对象存储(持久化)
    ├── blog-logs/                # 应用日志(按日期/级别分文件)
    └── blog-uploads/             # 本地上传文件(持久化)

没有源码,没有 node_modules,没有 Go 编译环境。干净、安全、易维护。


方案三:Jenkins CI/CD 自动构建部署

本方案使用 Jenkins 实现代码推送后自动构建 Docker 镜像、自动部署到阿里云服务器。

整体架构

开发者                Jenkins 服务器                    生产服务器
┌──────┐  git push  ┌──────────────────┐  SSH/SCP    ┌──────────────┐
│ 本地  │ ────────> │ 1. 拉取代码       │ ─────────> │ docker load  │
│ 开发  │           │ 2. docker build   │            │ docker compose│
│      │           │ 3. docker save    │            │ up -d        │
└──────┘           │ 4. scp 到生产机    │            └──────────────┘
                   │ 5. SSH 远程重启    │
                   └──────────────────┘

Jenkins 可以装在你的开发机、单独的 CI 服务器、或者生产服务器上。推荐单独一台或装在生产服务器上。


第一步:安装 Jenkins

方式一:Docker 安装 Jenkins(推荐)

在 Jenkins 所在机器上:

mkdir -p /opt/jenkins
cat > /opt/jenkins/docker-compose.yml << 'EOF'
services:
  jenkins:
    image: jenkins/jenkins:lts-jdk17
    restart: always
    user: root
    ports:
      - "8888:8080"
      - "50000:50000"
    volumes:
      - ./jenkins_home:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
      - /usr/bin/docker:/usr/bin/docker
    environment:
      - TZ=Asia/Shanghai
EOF

cd /opt/jenkins
docker compose up -d

# 获取初始密码
docker compose logs jenkins | grep -A 2 "initial"
# 或者
cat /opt/jenkins/jenkins_home/secrets/initialAdminPassword

浏览器访问 http://Jenkins机器IP:8888,输入初始密码,安装推荐插件。

方式二:直接安装

# Ubuntu
curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | sudo tee /usr/share/keyrings/jenkins-keyring.asc > /dev/null
echo deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/ | sudo tee /etc/apt/sources.list.d/jenkins.list > /dev/null
sudo apt update
sudo apt install -y jenkins
sudo systemctl enable jenkins
sudo systemctl start jenkins

第二步:Jenkins 插件安装

进入 Jenkins → Manage Jenkins → Plugins → Available plugins,安装:

  • SSH Agent Plugin
  • Pipeline
  • Git plugin(通常已预装)
  • Docker Pipeline(可选,如果用 Pipeline DSL 操作 Docker)

第三步:配置凭据

进入 Jenkins → Manage Jenkins → Credentials → System → Global credentials → Add Credentials:

3.1 Git 仓库凭据(如果是私有仓库)

  • Kind: Username with password(或 SSH key)
  • ID: git-credentials
  • Username: 你的 Git 用户名
  • Password: Token 或密码

3.2 生产服务器 SSH 凭据

  • Kind: SSH Username with private key
  • ID: prod-server-ssh
  • Username: deploy(或 root
  • Private Key: 粘贴你的 SSH 私钥

生成 SSH 密钥对(如果没有):

# 在 Jenkins 机器上
ssh-keygen -t ed25519 -C "jenkins-deploy" -f ~/.ssh/jenkins_deploy -N ""

# 把公钥添加到生产服务器
ssh-copy-id -i ~/.ssh/jenkins_deploy.pub deploy@生产服务器IP

第四步:创建 Jenkinsfile

在项目根目录创建 Jenkinsfile

pipeline {
    agent any

    environment {
        // 生产服务器信息
        PROD_HOST = '你的生产服务器IP'
        PROD_USER = 'deploy'
        PROD_DIR  = '/opt/blog'

        // 镜像名称
        SERVER_IMAGE = 'blog-server'
        WEB_IMAGE    = 'blog-web'
    }

    stages {
        stage('拉取代码') {
            steps {
                // 如果是私有仓库,用 credentialsId
                // git credentialsId: 'git-credentials', url: '你的仓库地址', branch: 'main'
                checkout scm
            }
        }

        stage('构建后端镜像') {
            steps {
                sh "docker build -t ${SERVER_IMAGE}:${BUILD_NUMBER} -t ${SERVER_IMAGE}:latest ./src/blog-server"
            }
        }

        stage('构建前端镜像') {
            steps {
                sh "docker build -t ${WEB_IMAGE}:${BUILD_NUMBER} -t ${WEB_IMAGE}:latest ./src/blog-web"
            }
        }

        stage('后端测试') {
            steps {
                sh '''
                    cd src/blog-server
                    docker run --rm ${SERVER_IMAGE}:${BUILD_NUMBER} ls -la
                '''
                // 如果有测试数据库可以跑完整测试:
                // sh 'cd src/blog-server && go test ./...'
            }
        }

        stage('导出镜像') {
            steps {
                sh """
                    docker save ${SERVER_IMAGE}:latest | gzip > ${SERVER_IMAGE}.tar.gz
                    docker save ${WEB_IMAGE}:latest | gzip > ${WEB_IMAGE}.tar.gz
                """
            }
        }

        stage('上传到生产服务器') {
            steps {
                sshagent(credentials: ['prod-server-ssh']) {
                    sh """
                        scp -o StrictHostKeyChecking=no \
                            ${SERVER_IMAGE}.tar.gz ${WEB_IMAGE}.tar.gz \
                            ${PROD_USER}@${PROD_HOST}:/tmp/
                    """
                }
            }
        }

        stage('部署到生产服务器') {
            steps {
                sshagent(credentials: ['prod-server-ssh']) {
                    sh """
                        ssh -o StrictHostKeyChecking=no ${PROD_USER}@${PROD_HOST} << 'DEPLOY_SCRIPT'
                            echo "===== 加载新镜像 ====="
                            docker load < /tmp/${SERVER_IMAGE}.tar.gz
                            docker load < /tmp/${WEB_IMAGE}.tar.gz
                            rm -f /tmp/${SERVER_IMAGE}.tar.gz /tmp/${WEB_IMAGE}.tar.gz

                            echo "===== 重启服务 ====="
                            cd ${PROD_DIR}
                            docker compose up -d

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

                            echo "===== 健康检查 ====="
                            sleep 5
                            curl -sf http://localhost/health && echo " OK" || echo " FAILED"
DEPLOY_SCRIPT
                    """
                }
            }
        }
    }

    post {
        success {
            echo "✅ 部署成功!构建号: ${BUILD_NUMBER}"
        }
        failure {
            echo "❌ 部署失败!请检查日志。"
        }
        always {
            // 清理本地构建产物
            sh "rm -f ${SERVER_IMAGE}.tar.gz ${WEB_IMAGE}.tar.gz"
            // 清理本地 Docker 镜像(保留 latest)
            sh "docker image prune -f"
        }
    }
}

第五步:创建 Jenkins Job

方式一:Pipeline 项目(推荐)

  1. Jenkins 首页 → New Item → 输入名称 blog-deploy → 选择 Pipeline → OK
  2. 配置页面:
    • Pipeline → Definition: Pipeline script from SCM
    • SCM: Git
    • Repository URL: 你的仓库地址
    • Credentials: 选择 git-credentials(私有仓库)
    • Branch: */main
    • Script Path: Jenkinsfile
  3. 保存

方式二:Freestyle 项目

如果不想用 Jenkinsfile,创建 Freestyle 项目,在 Build Steps 中添加 Execute shell:

#!/bin/bash
set -e

PROD_HOST="你的生产服务器IP"
PROD_USER="deploy"
PROD_DIR="/opt/blog"

echo "===== 构建镜像 ====="
docker build -t blog-server:latest ./src/blog-server
docker build -t blog-web:latest ./src/blog-web

echo "===== 导出镜像 ====="
docker save blog-server:latest | gzip > blog-server.tar.gz
docker save blog-web:latest | gzip > blog-web.tar.gz

echo "===== 上传到生产服务器 ====="
scp blog-server.tar.gz blog-web.tar.gz ${PROD_USER}@${PROD_HOST}:/tmp/

echo "===== 远程部署 ====="
ssh ${PROD_USER}@${PROD_HOST} << 'EOF'
docker load < /tmp/blog-server.tar.gz
docker load < /tmp/blog-web.tar.gz
rm -f /tmp/blog-server.tar.gz /tmp/blog-web.tar.gz
cd /opt/blog
docker compose up -d
docker image prune -f
sleep 5
curl -sf http://localhost/health && echo " 部署成功" || echo " 部署失败"
EOF

echo "===== 清理本地 ====="
rm -f blog-server.tar.gz blog-web.tar.gz

第六步:配置自动触发

6.1 Git Webhook 触发(推荐)

在 Git 仓库设置 Webhook:

  • Gitee: 仓库 → 管理 → WebHooks → 添加
  • GitHub: Settings → Webhooks → Add webhook
  • GitLab: Settings → Webhooks

Webhook URL: http://Jenkins机器IP:8888/generic-webhook-trigger/invoke?token=blog-deploy

Jenkins Job 配置:

  1. 安装 Generic Webhook Trigger 插件
  2. Job 配置 → Build Triggers → Generic Webhook Trigger
  3. Token: blog-deploy

6.2 定时构建

Job 配置 → Build Triggers → Build periodically:

# 每天凌晨 2 点构建
H 2 * * *

# 每次 push 后 5 分钟内构建(Poll SCM)
H/5 * * * *

6.3 手动触发

Jenkins Job 页面点击 “Build Now” 即可。


第七步:生产服务器准备

生产服务器上只需要做一次初始化(参考方案二第五步):

# 创建部署目录
mkdir -p /opt/blog/.docker-data/{pg-data,minio-data,blog-logs,blog-uploads}
mkdir -p /opt/blog/nginx/conf.d
mkdir -p /opt/blog/certbot/{conf,www}

# 创建 .env(参考方案二)
vim /opt/blog/.env

# 创建 docker-compose.yml(参考方案二,使用 image: 而非 build:)
vim /opt/blog/docker-compose.yml

# 创建 Nginx 配置(参考方案二)
vim /opt/blog/nginx/nginx.conf
vim /opt/blog/nginx/conf.d/blog.conf

# 首次启动
cd /opt/blog
docker compose up -d

# 初始化管理员
docker compose exec blog-server ./seed

之后每次 Jenkins 自动部署只会更新镜像并重启,配置文件和数据不受影响。


完整流程总结

1. 开发者 git push 到仓库
2. Webhook 触发 Jenkins 构建
3. Jenkins 拉取代码
4. Jenkins 构建 blog-server 和 blog-web Docker 镜像
5. Jenkins 导出镜像为 tar.gz
6. Jenkins 通过 SCP 上传到生产服务器
7. Jenkins 通过 SSH 在生产服务器上 docker load + docker compose up -d
8. 健康检查确认部署成功
9. 清理临时文件和旧镜像

整个过程全自动,push 代码后大约 3-5 分钟完成部署。


回滚

如果新版本有问题,快速回滚:

# 在生产服务器上
cd /opt/blog

# 查看历史镜像(如果 Jenkins 打了版本号 tag)
docker images | grep blog

# 修改 docker-compose.yml 中的镜像 tag 为上一个版本号
# 或者在 Jenkins 上重新构建上一个 commit

# 重启
docker compose up -d

如果 Jenkins 构建时打了版本号 tag(blog-server:42),可以直接指定回滚到某个构建号。


方案三:Jenkins CI/CD 自动构建部署

本方案使用 Jenkins 实现代码推送后自动构建镜像、自动部署到服务器,全程无需手动操作。

整体流程

开发者 push 代码
       │
       ▼
┌─────────────────────────────────────────┐
│  Jenkins 服务器                          │
│                                         │
│  1. 拉取代码                             │
│  2. 运行后端测试 (go test)               │
│  3. 并行构建 Docker 镜像                  │
│  4. 导出镜像 tar.gz                      │
│  5. SCP 上传到生产服务器                  │
│  6. SSH 远程执行: load + restart         │
└─────────────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────┐
│  生产服务器 (阿里云)                      │
│                                         │
│  docker load → docker compose up -d     │
│  健康检查 → 清理旧镜像                    │
└─────────────────────────────────────────┘

前置条件

Jenkins 服务器需要

  • Jenkins 2.x+
  • 插件:Pipeline、Git、SSH Agent、Docker Pipeline
  • 安装 Docker(用于构建镜像)
  • 安装 Go 1.25+(用于运行测试,或用 Docker 内测试)

生产服务器需要

  • 已按方案二完成初始部署(.env、docker-compose.yml、nginx 配置已就绪)
  • Jenkins 服务器的 SSH 公钥已添加到生产服务器的 ~/.ssh/authorized_keys

第一步:Jenkins 凭据配置

在 Jenkins → Manage Jenkins → Credentials 中添加:

凭据 ID 类型 说明
deploy-ssh-key SSH Username with private key Jenkins 连接生产服务器的 SSH 私钥
deploy-host Secret text 生产服务器 IP 地址
deploy-user Secret text SSH 登录用户名(如 deploy)
git-credentials Username with password / SSH key Git 仓库访问凭据

第二步:创建 Jenkins Pipeline

2.1 新建 Pipeline 任务

Jenkins → New Item → Pipeline → 输入名称 blog-deploy

2.2 配置 Pipeline

在 Pipeline 配置页面:

  • Definition: Pipeline script from SCM
  • SCM: Git
  • Repository URL: 你的 Git 仓库地址
  • Credentials: 选择 git-credentials
  • Branch: */main(或你的主分支)
  • Script Path: Jenkinsfile

2.3 配置触发器(可选)

  • Poll SCM: H/5 * * * *(每 5 分钟检查一次)
  • 或配置 Webhook:Git 仓库 push 时触发

第三步:Jenkinsfile 说明

项目根目录已包含 Jenkinsfile,流水线包含以下阶段:

阶段 说明 耗时
检出代码 从 Git 拉取最新代码 ~10s
后端测试 运行 go test ./... ~30s
构建镜像 并行构建后端和前端 Docker 镜像 ~2-5min
导出镜像 docker save 导出为 tar.gz ~30s
上传到服务器 SCP 传输到生产服务器 ~1-3min
部署 SSH 远程加载镜像并重启服务 ~30s

总耗时约 5-10 分钟。


第四步:配置 Webhook 自动触发

Gitee

仓库 → 管理 → WebHooks → 添加:

  • URL: http://你的Jenkins地址/generic-webhook-trigger/invoke?token=blog-deploy
  • 事件: Push

GitHub

仓库 → Settings → Webhooks → Add webhook:

  • Payload URL: http://你的Jenkins地址/github-webhook/
  • Content type: application/json
  • Events: Just the push event

GitLab

仓库 → Settings → Webhooks:

  • URL: http://你的Jenkins地址/project/blog-deploy
  • Trigger: Push events

第五步:验证

  1. 推送一次代码到主分支
  2. 在 Jenkins 查看 Pipeline 是否自动触发
  3. 等待所有阶段完成(绿色)
  4. 访问 https://你的域名 验证更新

进阶:使用阿里云容器镜像服务

如果不想用 SCP 传输镜像文件,可以用阿里云 ACR(容器镜像服务)作为中转:

修改 Jenkinsfile 的构建和部署阶段

environment {
    REGISTRY = 'registry.cn-hangzhou.aliyuncs.com'
    NAMESPACE = '你的命名空间'
    SERVER_IMAGE = "${REGISTRY}/${NAMESPACE}/blog-server"
    WEB_IMAGE = "${REGISTRY}/${NAMESPACE}/blog-web"
}

stage('推送镜像') {
    steps {
        withCredentials([usernamePassword(credentialsId: 'aliyun-acr', usernameVariable: 'ACR_USER', passwordVariable: 'ACR_PASS')]) {
            sh """
                echo ${ACR_PASS} | docker login --username=${ACR_USER} --password-stdin ${REGISTRY}
                docker push ${SERVER_IMAGE}:${IMAGE_TAG}
                docker push ${SERVER_IMAGE}:latest
                docker push ${WEB_IMAGE}:${IMAGE_TAG}
                docker push ${WEB_IMAGE}:latest
            """
        }
    }
}

stage('部署') {
    steps {
        sshagent(credentials: ['deploy-ssh-key']) {
            sh """
                ssh ${DEPLOY_USER}@${DEPLOY_HOST} << 'EOF'
                    echo ${ACR_PASS} | docker login --username=${ACR_USER} --password-stdin ${REGISTRY}
                    docker pull ${SERVER_IMAGE}:latest
                    docker pull ${WEB_IMAGE}:latest
                    cd /opt/blog
                    docker compose up -d
                    docker image prune -f
EOF
            """
        }
    }
}

这样生产服务器直接从阿里云 ACR 拉取镜像,不需要 SCP 传文件,速度更快。


回滚

如果部署后发现问题,可以快速回滚:

# 在生产服务器上
cd /opt/blog

# 查看历史镜像(如果用了 IMAGE_TAG)
docker images | grep blog

# 修改 docker-compose.yml 中的 image tag 为上一个版本号
# 或者重新 load 上一个版本的镜像

# 重启
docker compose up -d

建议每次部署前在 Jenkins 中保留构建产物(Archive Artifacts),方便回滚。

评论

登录 后发表评论

暂无评论