从零实现个人博客的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(外部构建),主要考虑:

  1. 开发效率:Hugo构建仅需几秒,Docker打包1分钟,总耗时远小于多阶段构建
  2. 关注点分离:构建和运行环境分离是最佳实践
  3. CI/CD灵活性:在GitHub Actions中实现完整自动化,保持本地开发的简洁性
  4. 镜像优化:最终镜像仅包含运行时必需文件,更安全、更小

实施步骤

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流水线设计

核心流程

构建完整的自动化部署流水线,实现:

  1. 自动构建和测试
  2. 推送镜像到Docker Hub(版本备份)
  3. 直接传输镜像到服务器(绕过网络限制)
  4. 服务器本地部署

关键配置片段

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/  ← 端口号丢失

问题根源

  1. HTML中链接使用相对路径 href="/blog"(正确)
  2. Nginx识别到 /blog 是目录,自动添加尾部斜杠
  3. 重定向时生成的Location header丢失了端口号

尝试的方案

  1. 修改Hugo配置 relativeURLs = true

    • 问题:影响RSS、sitemap和SEO
  2. 单独使用 port_in_redirect off

    • 问题:配置未完全生效
  3. 综合配置方案(最终解决)

   # 禁用所有可能导致端口丢失的重定向
   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受网络限制:

  1. 镜像源的工作原理

    • 镜像源(加速器)只同步官方镜像和热门项目
    • 个人镜像不会被同步到加速器
    • 必须直接访问Docker Hub
  2. 不可行的方案

    • 配置镜像加速器:只能解决官方镜像,无法解决个人镜像
    • 使用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/A60MB
环境一致性❌ 依赖服务器✅ 完全一致
可移植性❌ 需重新配置✅ 一键部署
回滚能力手动Git操作✅ 版本化镜像
资源占用~50MB~80MB
端口808081

使用指南

本地开发

# 开发模式
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

技术总结

关键收获

  1. 容器化实践

    • Docker镜像构建和优化技巧
    • 不同构建策略的适用场景
    • 容器健康检查和资源限制
  2. CI/CD设计

    • 完整的自动化流水线
    • 尝试GitHub Actions
    • 版本管理
  3. 问题解决能力

    • 练习浏览器开发者调试
    • Nginx配置
    • 跨平台开发的注意事项
  4. 架构设计思维

    • 在约束条件下做技术选型
    • 生产环境和实验环境的平衡
    • 双轨部署的架构模式

实践

  1. 构建优化

    • 使用Alpine基础镜像减小体积
    • 分离构建和运行环境
  2. 版本管理

    • 使用语义化的版本标签
    • 保留完整的版本历史
    • 实现快速回滚机制
  3. 网络处理

    • 评估网络环境限制
    • 设计备用方案
    • 保持部署的可靠性
  4. 安全性

    • 使用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配置

参考资源

相关笔记

Hugo 分类页面中文路径未更新的问题排查记录

前言 在使用 Hugo 搭建博客并启用分类页面功能时,遇到了一个非常诡异的问题:中文分类路径的页面渲染结果和英文分类页面不一致,并且中文页面似乎始终渲染的是「另外一个模板」。

阅读全文