从零实现个人博客的Docker容器化部署
内容目录
项目背景
个人博客最初采用传统的Git Hook方式部署:代码推送后,云服务器通过post-receive钩子自动执行Hugo构建并部署到Nginx。这种方式稳定可靠,但缺少容器化带来的环境一致性和可移植性优势。
本文记录了将博客完整容器化的实践过程,包括技术选型、CI/CD流程设计、遇到的问题及解决方案,以及最终实现的双轨部署架构。
技术栈
- 静态网站生成: Hugo + hugoplate模板
- 样式框架: Tailwind CSS 4.x
- 容器化: Docker + Docker Compose
- CI/CD: GitHub Actions
- Web服务器: Nginx (Alpine)
- 镜像仓库: Docker Hub
- 开发环境: Windows 11 + Docker Desktop
容器化方案设计
构建策略选择
在实施过程中评估了两种Docker构建方案:
方案1:多阶段构建(完全自包含)
# 构建阶段
FROM node:18-alpine AS builder
RUN apk add --no-cache hugo git
WORKDIR /src
COPY . .
RUN npm ci && npm run build
# 运行阶段
FROM nginx:alpine
COPY --from=builder /src/public /usr/share/nginx/html
优势:
- 完全自包含,不依赖外部环境
- 适合团队协作
- 符合"构建一次,到处运行"的理念
劣势:
- 构建时间较长(需下载依赖)
- Alpine的Hugo版本可能落后
- Windows环境下网络访问不稳定
方案2:外部构建 + 镜像打包(最终选择)
FROM nginx:alpine
COPY public /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
优势:
- 构建速度极快(约1分钟)
- 最终镜像极小(约60MB)
- 本地开发迭代效率高
- 充分利用Hugo的快速构建特性
劣势:
- 需要在容器外完成Hugo构建
- 依赖本地Node.js和Hugo环境
最终决策
选择方案2(外部构建),主要考虑:
- 开发效率:Hugo构建仅需几秒,Docker打包1分钟,总耗时远小于多阶段构建
- 关注点分离:构建和运行环境分离是最佳实践
- CI/CD灵活性:在GitHub Actions中实现完整自动化,保持本地开发的简洁性
- 镜像优化:最终镜像仅包含运行时必需文件,更安全、更小
实施步骤
1. Dockerfile配置
创建最小化的生产镜像:
FROM nginx:alpine
# 复制静态文件
COPY public /usr/share/nginx/html
# 自定义Nginx配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
2. Nginx优化配置
针对静态博客的优化配置:
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm;
# 关键:禁用可能导致端口丢失的重定向
absolute_redirect off;
port_in_redirect off;
server_name_in_redirect off;
# 路由配置(避免触发301重定向)
location / {
try_files $uri $uri/index.html $uri.html /index.html;
}
# 静态资源长期缓存
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot|webp)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Gzip压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript
application/x-javascript application/xml+rss
application/javascript application/json
image/svg+xml;
}
配置要点:
absolute_redirect off等三个指令防止重定向丢失端口号try_files使用$uri/index.html而非$uri/,避免触发301重定向- 静态资源设置1年缓存,减少带宽消耗
3. Docker Compose本地开发
简化本地开发环境配置:
version: '3.8'
services:
blog:
build:
context: .
dockerfile: Dockerfile
container_name: welblog-nginx
ports:
- "8080:80"
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
使用方式:
# 一键启动
docker-compose up -d
# 查看日志
docker-compose logs -f
# 停止
docker-compose down
4. CI/CD流水线设计
核心流程
构建完整的自动化部署流水线,实现:
- 自动构建和测试
- 推送镜像到Docker Hub(版本备份)
- 直接传输镜像到服务器(绕过网络限制)
- 服务器本地部署
关键配置片段
name: Docker Build, Push and Deploy
on:
push:
branches: [ main, master ]
workflow_dispatch:
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_IMAGE: welblog
ENABLE_DEPLOY: true
jobs:
build-push-deploy:
runs-on: ubuntu-latest
steps:
# 构建Hugo网站
- name: 📥 Checkout code
uses: actions/checkout@v4
- name: 🔧 Setup Hugo
uses: peaceiris/actions-hugo@v3
with:
hugo-version: 'latest'
extended: true
- name: 🔧 Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: 📦 Install dependencies
run: npm ci
- name: 🔨 Build Hugo site
run: |
npm run build
test -f "public/index.html" || exit 1
# 构建和测试Docker镜像
- name: 🐳 Build Docker image
run: |
SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7)
docker build -t $DOCKER_USERNAME/$DOCKER_IMAGE:$SHORT_SHA \
-t $DOCKER_USERNAME/$DOCKER_IMAGE:latest .
echo "IMAGE_TAG=$SHORT_SHA" >> $GITHUB_ENV
- name: 🧪 Test Docker container
run: |
docker run -d -p 8080:80 --name test $DOCKER_USERNAME/$DOCKER_IMAGE:latest
sleep 5
curl -f http://localhost:8080 || exit 1
docker stop test && docker rm test
# 推送到Docker Hub(版本备份)
- name: 📤 Push to Docker Hub
if: github.event_name == 'push'
run: |
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "$DOCKER_USERNAME" --password-stdin
docker push $DOCKER_USERNAME/$DOCKER_IMAGE:${{ env.IMAGE_TAG }}
docker push $DOCKER_USERNAME/$DOCKER_IMAGE:latest
# 导出镜像并传输到服务器
- name: 📦 Export image
run: |
FILENAME="welblog-${{ env.IMAGE_TAG }}.tar.gz"
docker save $DOCKER_USERNAME/$DOCKER_IMAGE:${{ env.IMAGE_TAG }} | gzip > $FILENAME
chmod 644 $FILENAME
echo "IMAGE_FILENAME=$FILENAME" >> $GITHUB_ENV
- name: 📤 Transfer to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SERVER_HOST }}
port: ${{ secrets.SERVER_PORT }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "${{ env.IMAGE_FILENAME }}"
target: "/tmp/"
# SSH部署
- name: 🚀 Deploy
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SERVER_HOST }}
port: ${{ secrets.SERVER_PORT }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
docker load -i /tmp/${{ env.IMAGE_FILENAME }}
docker stop welblog-docker || true
docker rm welblog-docker || true
docker run -d --name welblog-docker -p 8081:80 \
--restart unless-stopped welblog:latest
rm /tmp/${{ env.IMAGE_FILENAME }}
版本管理策略
使用Git commit SHA前7位作为镜像版本标签:
# 示例
Commit: ab22979e0559bf46b6afd07b553eae027ec36c9e
镜像标签: welblog:ab22979
优势:
- 每个版本可精确追溯到源码
- 便于问题定位和调试
- 支持快速回滚到任意历史版本
5. 版本回滚机制
回滚脚本设计
在服务器上创建回滚脚本,支持本地镜像和远程拉取:
#!/bin/bash
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
DOCKER_USER="your-dockerhub-username"
show_usage() {
echo -e "${YELLOW}Usage: $0 <image-tag> [--remote]${NC}"
echo ""
echo "Examples:"
echo " $0 ab22979 # 回滚到本地镜像"
echo " $0 ab22979 --remote # 从Docker Hub拉取并回滚"
echo ""
echo -e "${BLUE}Available local versions:${NC}"
docker images welblog --format "table {{.Tag}}\t{{.CreatedAt}}\t{{.Size}}"
exit 1
}
if [ -z "$1" ]; then
show_usage
fi
TAG=$1
REMOTE=${2:-""}
LOCAL_IMAGE="welblog:$TAG"
REMOTE_IMAGE="${DOCKER_USER}/welblog:$TAG"
# 检查本地镜像
if docker images --format "{{.Repository}}:{{.Tag}}" | grep -q "^$LOCAL_IMAGE$"; then
echo -e "${GREEN}Found: $LOCAL_IMAGE${NC}"
IMAGE_TO_USE=$LOCAL_IMAGE
elif [ "$REMOTE" = "--remote" ]; then
echo -e "${YELLOW}Pulling from Docker Hub...${NC}"
docker pull $REMOTE_IMAGE || exit 1
IMAGE_TO_USE=$REMOTE_IMAGE
else
echo -e "${RED}Image not found${NC}"
echo "Try: $0 $TAG --remote"
exit 1
fi
# 执行回滚
echo -e "${BLUE}=== Rolling back to $IMAGE_TO_USE ===${NC}"
docker stop welblog-docker 2>/dev/null || true
docker rm welblog-docker 2>/dev/null || true
docker run -d --name welblog-docker -p 8081:80 \
--restart unless-stopped $IMAGE_TO_USE
# 验证
sleep 3
if docker ps | grep -q welblog-docker && curl -f http://localhost:8081 > /dev/null 2>&1; then
echo -e "${GREEN}Rollback successful${NC}"
exit 0
else
echo -e "${RED}Rollback failed${NC}"
docker logs welblog-docker
exit 1
fi
回滚操作示例
# 查看可用版本
./rollback-welblog.sh
# 回滚到本地镜像
./rollback-welblog.sh ab22979
# 从Docker Hub拉取并回滚
./rollback-welblog.sh ab22979 --remote
# 回滚到最新版本
./rollback-welblog.sh latest
回滚特性:
- 支持本地镜像快速切换(<30秒)
- 支持从Docker Hub拉取历史版本
- 自动健康检查,确保回滚成功
- 彩色输出,操作状态一目了然
遇到的问题及解决方案
问题1:Hugo版本兼容性
问题:Alpine Linux官方仓库的Hugo版本(0.139)低于项目要求(0.141),导致构建失败。
错误信息:
Error: permalink attribute not recognised
WARN Module "hugoplate" is not compatible with this Hugo version
解决方案: 采用外部构建策略,在本地或CI环境使用最新版Hugo,避免了版本限制问题。
问题2:Windows文件系统锁定
问题:在Windows环境执行 npm run build 时遇到文件占用错误。
错误信息:
Error: The requested operation cannot be performed on a file
with a user-mapped section open
原因分析:
- Hugo server进程未正确关闭
- VS Code文件监视占用
- Docker Desktop文件访问
- Windows Defender实时扫描
解决方案:
# 停止Hugo进程
Get-Process hugo -ErrorAction SilentlyContinue | Stop-Process -Force
# 清理构建产物
Remove-Item -Recurse -Force public, resources -ErrorAction SilentlyContinue
# 等待文件句柄释放
Start-Sleep -Seconds 3
# 重新构建
npm run build
问题3:容器端口重定向问题
问题:容器运行在8080端口,首页正常访问,但点击导航链接后端口号丢失。
现象:
期望: http://localhost:8080/blog
实际: http://localhost/blog (404错误)
问题分析:
通过浏览器开发者工具(F12)分析网络请求:
请求: GET http://localhost:8080/blog
响应: 301 Moved Permanently
Location: http://localhost/blog/ ← 端口号丢失
问题根源:
- HTML中链接使用相对路径
href="/blog"(正确) - Nginx识别到
/blog是目录,自动添加尾部斜杠 - 重定向时生成的Location header丢失了端口号
尝试的方案:
❌ 修改Hugo配置
relativeURLs = true- 问题:影响RSS、sitemap和SEO
❌ 单独使用
port_in_redirect off- 问题:配置未完全生效
✅ 综合配置方案(最终解决)
# 禁用所有可能导致端口丢失的重定向
absolute_redirect off;
port_in_redirect off;
server_name_in_redirect off;
# 优化try_files,避免触发目录重定向
location / {
# $uri/ 会触发301重定向
# $uri/index.html 直接查找文件,不触发重定向
try_files $uri $uri/index.html $uri.html /index.html;
}
关键技术点:
$uri/触发Nginx目录处理逻辑,产生301重定向$uri/index.html直接查找文件,避免重定向- 三个redirect off指令配合使用,解决问题
问题4:云服务器Docker Hub访问受限
问题:云服务器拉取个人镜像时遇到超时错误。
错误信息:
Error response from daemon: Get "https://registry-1.docker.io/v2/":
context deadline exceeded
原因分析:
国内云服务器访问Docker Hub受网络限制:
镜像源的工作原理:
- 镜像源(加速器)只同步官方镜像和热门项目
- 个人镜像不会被同步到加速器
- 必须直接访问Docker Hub
不可行的方案:
- 配置镜像加速器:只能解决官方镜像,无法解决个人镜像
- 使用VPN:违反云服务商使用条款
最终方案:双路径部署
方案设计:
├─ Docker Hub:版本备份,任何环境都可拉取
└─ 直接传输:GitHub Actions构建后通过SCP传输镜像到服务器
优势:
- 不依赖Docker Hub网络连接
- 保留完整版本历史(Docker Hub)
- 部署可靠(直接传输)
- 灵活性高(支持两种部署方式)
实现细节:
# 1. 推送到Docker Hub(版本备份)
- name: Push to Docker Hub
run: docker push $DOCKER_USERNAME/$DOCKER_IMAGE:$TAG
# 2. 导出并传输到服务器(实际部署)
- name: Export and transfer
run: |
docker save $IMAGE | gzip > image.tar.gz
scp image.tar.gz server:/tmp/
ssh server "docker load -i /tmp/image.tar.gz"
架构设计亮点
1. 双轨部署架构
保持传统部署和容器部署并行运行:
传统部署(80端口):
- Git Hook + Hugo + Nginx
- 推送后1-2分钟自动更新
- 稳定、成熟、久经验证
- 生产环境首选
容器部署(8081端口):
- Docker + GitHub Actions
- 完整CI/CD流水线
- 环境一致性保证
- 技术储备和实验
架构优势:
- 生产稳定性不受影响
- 新技术学习风险可控
- 两套系统互不干扰
- 随时可切换到容器部署
2. 镜像优化策略
最终镜像大小约60MB:
组成:
nginx:alpine 基础镜像 ~7MB
网站静态文件 ~18MB
nginx配置文件 <1MB
总计 ~25MB
压缩传输后 ~8-10MB
优化措施:
- 使用Alpine Linux基础镜像
- 只包含运行时必需文件
- 不包含构建工具和源码
- .dockerignore排除无关文件
3. 完整的CI/CD流水线
自动化流程:
git push → 触发GitHub Actions
↓
安装依赖(Hugo + Node.js)
↓
构建网站(npm run build)
↓
构建Docker镜像(带版本标签)
↓
容器健康检查测试
↓
推送到Docker Hub(版本备份)
↓
导出并压缩镜像
↓
SCP传输到服务器
↓
SSH远程部署
↓
清理旧版本镜像
↓
生成构建报告
总耗时:3-5分钟
4. 版本管理和回滚
版本标记策略:
每次部署生成两个标签:
1. commit SHA(精确版本): welblog:ab22979
2. latest(最新版本): welblog:latest
示例:
docker images welblog
REPOSITORY TAG CREATED SIZE
welblog c5d8f21 5 minutes ago 60MB ← 最新
welblog ab22979 1 hour ago 60MB
welblog 9fe3c84 1 day ago 59MB
welblog latest 5 minutes ago 60MB
回滚能力:
- 本地镜像回滚:<30秒
- 远程镜像回滚:1-2分钟
- 自动健康检查
- 回滚失败自动日志输出
性能数据
镜像和部署指标
| 指标 | 数值 |
|---|---|
| 最终镜像大小 | ~60MB |
| 压缩后传输大小 | ~20MB |
| 容器启动时间 | <5秒 |
| 容器内存占用 | ~80MB |
| Hugo构建时间 | ~20秒 |
| Docker构建时间 | ~1分钟 |
| CI/CD全流程 | 3-5分钟 |
传统 vs 容器化对比
| 对比项 | 传统部署 | Docker容器化 |
|---|---|---|
| 部署时间 | 1-2分钟 | 3-5分钟 |
| 镜像大小 | N/A | 60MB |
| 环境一致性 | ❌ 依赖服务器 | ✅ 完全一致 |
| 可移植性 | ❌ 需重新配置 | ✅ 一键部署 |
| 回滚能力 | 手动Git操作 | ✅ 版本化镜像 |
| 资源占用 | ~50MB | ~80MB |
| 端口 | 80 | 8081 |
使用指南
本地开发
# 开发模式
npm run dev
# 构建网站
npm run build
# Docker Compose启动
docker-compose up -d
# 查看日志
docker-compose logs -f
# 访问网站
open http://localhost:8080
# 停止容器
docker-compose down
生产部署
# 推送到GitHub(触发CI/CD)
git push github main
# → GitHub Actions自动构建并部署
# 推送到云服务器(传统部署)
git push origin main
# → Git Hook自动构建并部署
# 两种方式可同时使用,互不干扰
版本管理
# 查看所有版本
docker images welblog
# 查看运行中的容器
docker ps | grep welblog
# 查看容器日志
docker logs welblog-docker
# 回滚到指定版本
./rollback-welblog.sh ab22979
# 从Docker Hub拉取历史版本
./rollback-welblog.sh ab22979 --remote
技术总结
关键收获
容器化实践
- Docker镜像构建和优化技巧
- 不同构建策略的适用场景
- 容器健康检查和资源限制
CI/CD设计
- 完整的自动化流水线
- 尝试GitHub Actions
- 版本管理
问题解决能力
- 练习浏览器开发者调试
- Nginx配置
- 跨平台开发的注意事项
架构设计思维
- 在约束条件下做技术选型
- 生产环境和实验环境的平衡
- 双轨部署的架构模式
实践
构建优化
- 使用Alpine基础镜像减小体积
- 分离构建和运行环境
版本管理
- 使用语义化的版本标签
- 保留完整的版本历史
- 实现快速回滚机制
网络处理
- 评估网络环境限制
- 设计备用方案
- 保持部署的可靠性
安全性
- 使用Secrets管理敏感信息
- 最小化镜像内容
- 定期更新基础镜像
项目文件结构
welblog/
├── .github/
│ └── workflows/
│ └── docker-build.yml # CI/CD配置
├── content/
│ └── blog/
│ └── docker-deploy.md # 本文
├── Dockerfile # 容器配置
├── docker-compose.yml # 本地开发配置
├── nginx.conf # Web服务器配置
├── .dockerignore # 构建优化
├── package.json # 依赖管理
└── hugo.toml # Hugo配置