返回文章

用 Gitea、Jenkins 和 Docker 发布个人博客

把 Astro 静态博客打包成 Docker 容器,通过 Cloudflare Tunnel 暴露到公网。

个人技术博客不一定要放在托管平台上。如果服务器上已经有 Gitea、Jenkins、Docker 和 cloudflared tunnel,就可以搭出一条很清楚的发布链路:代码自己管,构建自己跑,博客以容器方式运行,公网入口交给 Cloudflare。

本地写作 -> Gitea -> Jenkins -> Astro build -> Docker image -> Blog container -> Cloudflare Tunnel

这套方案里,Astro 只负责生成静态文件,Docker 镜像负责承载这些静态文件,容器内的 Nginx 负责提供访问。宿主机上是否直装 Nginx 不重要,因为 tunnel 可以直接指向博客容器暴露出来的本机端口。

第一版目标

先让博客具备最核心的发布能力:

  • Markdown 写文章
  • Astro 静态构建
  • Jenkins 自动构建
  • Docker 镜像封装
  • Docker Compose 启动博客容器
  • Cloudflare Tunnel 指向博客容器端口

对静态博客来说,这条链路已经足够稳定。后面如果要加评论、搜索、统计,再单独扩展。

运行链路

博客容器只监听服务器本机端口:

127.0.0.1:3001 -> container:80

公网访问链路是:

浏览器 -> Cloudflare -> cloudflared -> localhost:3001 -> blog container -> nginx -> dist

这样做的好处是容器端口不直接暴露公网,HTTPS 和公网入口仍然由 Cloudflare 处理。

Docker 镜像

博客先在 Jenkins workspace 里执行构建:

npm ci
npm run build

构建完成后会生成 dist/。然后用一个很薄的 Nginx 镜像承载它:

FROM nginx:alpine

COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
COPY dist /usr/share/nginx/html

Nginx 配置只需要服务静态文件:

server {
    listen 80;
    server_name _;

    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Docker Compose

deploy/docker-compose.yml 负责启动博客容器:

services:
  blog-frontend:
    image: blog-frontend-astro:latest
    container_name: blog-frontend-astro
    restart: unless-stopped
    ports:
      - "127.0.0.1:3001:80"

因为镜像已经包含 dist/ 和 Nginx 配置,所以这里不需要再挂载站点目录。

Jenkins 流水线

Jenkinsfile 的核心流程是:

pipeline {
    agent any

    tools {
        nodejs 'NodeJS 26.1.0'
    }

    environment {
        IMAGE_NAME = 'blog-frontend-astro:latest'
        COMPOSE_FILE = 'deploy/docker-compose.yml'
    }

    stages {
        stage('Install') {
            steps {
                sh 'npm ci'
            }
        }

        stage('Build') {
            steps {
                sh 'npm run build'
            }
        }

        stage('Deploy') {
            steps {
                sh '''
                  docker build -t "$IMAGE_NAME" .
                  docker compose -f "$COMPOSE_FILE" up -d --force-recreate
                '''
            }
        }
    }
}

Jenkins 运行在 Docker 容器里时,需要能调用宿主机 Docker。常见做法是给 Jenkins 容器挂载 Docker socket,并在 Jenkins 镜像里安装 Docker CLI:

volumes:
  - /var/run/docker.sock:/var/run/docker.sock

这样 Jenkins 容器里执行的 docker builddocker compose up -d,实际操作的是宿主机 Docker。

cloudflared tunnel

tunnel 只需要把 zengqing.top 指向博客容器端口:

http://localhost:3001

如果 Cloudflare 页面提示 DNS 记录已存在,只要当前域名已经正确指向 tunnel,就不需要重复创建 DNS 记录。

上线前检查

第一次上线前建议检查:

  • astro.config.mjs 里的 sitehttps://zengqing.top
  • Jenkins 能成功执行 npm ci
  • Jenkins 能成功执行 npm run build
  • Jenkins 容器里有 Docker CLI
  • Jenkins 容器挂载了 /var/run/docker.sock
  • docker ps 能看到 blog-frontend-astro
  • 服务器本机访问 http://localhost:3001 正常
  • Cloudflare Tunnel 的 zengqing.top 指向 http://localhost:3001

整条链路跑通之后,日常写文章只需要提交代码,剩下的构建和发布交给 Jenkins。