阿里云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:只重启,不读新配置
什么时候需要删除掉挂载的容器文件夹 我给你用最直白、最实用、你马上能用到的方式讲清楚:到底什么时候必须删除挂载的文件夹 / 容器数据。 一、什么时候 必须删除 挂载的文件夹? 只要你改了下面这些东西,就必须删除旧的挂载文件夹,否则改了等于白改!
- 你改了 .env 环境变量 → 必须删除容器 / 重新挂载因为环境变量是启动时一次性加载,不会热更新。
- 你改了 Nginx 配置文件(nginx.conf) → 必须删除旧容器,重建否则 Nginx 一直用旧配置,你改本地文件没用。
- 你改了 前端代码、后端代码、打包后的镜像 → 必须删除旧容器,重新 up不然容器还在运行旧版本。
- 你发现 配置怎么改都不生效 → 直接删除挂载目录 + 重建容器99% 是缓存 / 旧文件在捣乱。
- 容器启动报错、文件权限错误 → 删除挂载目录重建最快速的解决办法。 二、什么时候 不需要删除? 只是重启服务 只是临时改运行参数 只是看日志 这些都不用删文件夹。 三、你现在的情况 必须删除! 因为你: 改了 .env 改了 Nginx 配置 改了前端接口地址 不删除旧容器,你改的所有东西都不生效!什么时候需要删除掉挂载的容器文件夹 我给你用最直白、最实用、你马上能用到的方式讲清楚:到底什么时候必须删除挂载的文件夹 / 容器数据。 一、什么时候 必须删除 挂载的文件夹? 只要你改了下面这些东西,就必须删除旧的挂载文件夹,否则改了等于白改!
- 你改了 .env 环境变量 → 必须删除容器 / 重新挂载因为环境变量是启动时一次性加载,不会热更新。
- 你改了 Nginx 配置文件(nginx.conf) → 必须删除旧容器,重建否则 Nginx 一直用旧配置,你改本地文件没用。
- 你改了 前端代码、后端代码、打包后的镜像 → 必须删除旧容器,重新 up不然容器还在运行旧版本。
- 你发现 配置怎么改都不生效 → 直接删除挂载目录 + 重建容器99% 是缓存 / 旧文件在捣乱。
- 容器启动报错、文件权限错误 → 删除挂载目录重建最快速的解决办法。 二、什么时候 不需要删除? 只是重启服务 只是临时改运行参数 只是看日志 这些都不用删文件夹。 三、你现在的情况 必须删除! 因为你: 改了 .env 改了 Nginx 配置 改了前端接口地址 不删除旧容器,你改的所有东西都不生效!
目录
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 项目(推荐)
- Jenkins 首页 → New Item → 输入名称
blog-deploy→ 选择 Pipeline → OK - 配置页面:
- Pipeline → Definition:
Pipeline script from SCM - SCM: Git
- Repository URL: 你的仓库地址
- Credentials: 选择
git-credentials(私有仓库) - Branch:
*/main - Script Path:
Jenkinsfile
- Pipeline → Definition:
- 保存
方式二: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 配置:
- 安装 Generic Webhook Trigger 插件
- Job 配置 → Build Triggers → Generic Webhook Trigger
- 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
第五步:验证
- 推送一次代码到主分支
- 在 Jenkins 查看 Pipeline 是否自动触发
- 等待所有阶段完成(绿色)
- 访问
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),方便回滚。
评论
登录 后发表评论
暂无评论