这篇教程适合已经有一台 Linux 服务器,并且使用宝塔面板管理网站和 Docker 的情况。
最终要实现的效果很简单:
本地修改代码
↓
git push
↓
GitHub Actions
↓
服务器上的 Self-hosted Runner 收到任务
↓
自动拉取最新代码
↓
自动构建 Docker 镜像
↓
自动更新容器
↓
健康检查
↓
部署完成
以后发布后端项目时,不需要再进入宝塔手动构建镜像、删除容器、重新创建容器。
整套流程只需要:
git push origin main
一、准备条件
开始前需要准备:
- 一台 Linux 服务器
- 已安装宝塔面板
- 已安装 Docker
- GitHub 仓库
- Node.js 后端项目
- 项目可以通过 Docker 运行
- GitHub 仓库默认分支为
main
本文以一个 Node/Koa 项目为例。
假设项目名称:
my-node-service
后端端口:
3000
服务器项目目录:
/www/git/my-node-service
生产环境变量:
/www/env/my-node-service.env
二、项目需要准备的文件
项目根目录至少需要:
my-node-service/
├── .github/
│ └── workflows/
│ └── deploy.yml
│
├── Dockerfile
├── docker-compose.yml
├── .dockerignore
├── package.json
├── package-lock.json
├── app.js
└── 其他项目文件
三、编写 Dockerfile
如果你的 Node 项目不需要 TypeScript 编译,也没有 dist,可以直接运行源码。
例如:
FROM node:20-bookworm-slim
WORKDIR /app
COPY package.json package-lock.json ./
ENV NODE_ENV=production
RUN npm ci --omit=dev && npm cache clean --force
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]
如果你的启动入口不是:
app.js
而是:
src/index.js
那么改成:
CMD ["node", "src/index.js"]
也可以直接通过 package.json:
{
"scripts": {
"start": "node app.js"
}
}
然后 Dockerfile 使用:
CMD ["npm", "start"]
四、创建 .dockerignore
项目根目录创建:
.dockerignore
内容:
node_modules
npm-debug.log
.git
.github
.env
.env.*
README.md
主要目的是避免:
- 本地
node_modules被打进镜像 .git被复制到 Docker- GitHub Actions 文件进入镜像
.env等敏感配置进入镜像
生产环境变量不要直接放进 GitHub 仓库。
五、增加健康检查接口
Docker 自动部署最好有一个专门的健康检查接口。
例如 Koa:
router.get('/health', async (ctx) => {
ctx.status = 200
ctx.body = {
status: 'ok'
}
})
这个接口必须满足几个条件:
不需要登录
不需要 JWT
不查数据库
不请求第三方接口
直接返回 HTTP 200
例如:
curl http://127.0.0.1:3000/health
应该返回:
{
"status": "ok"
}
不要给 /health 加鉴权。
否则 Docker 会认为:
401 = unhealthy
如果接口不存在:
404 = unhealthy
健康检查的作用只是确认:
Node 进程已经正常启动
HTTP 服务能够正常响应
六、编写 docker-compose.yml
项目根目录创建:
docker-compose.yml
例如:
services:
api:
build:
context: .
dockerfile: Dockerfile
image: my-node-service:latest
container_name: my-node-service
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
env_file:
- /www/env/my-node-service.env
healthcheck:
test:
[
"CMD",
"node",
"-e",
"fetch('http://127.0.0.1:3000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
这里:
ports:
- "127.0.0.1:3000:3000"
表示:
服务器 127.0.0.1:3000
↓
Docker 容器 3000
这样 Node 的 3000 端口不会直接暴露到公网。
网站访问通过:
公网
↓
宝塔 Nginx
↓
127.0.0.1:3000
↓
Docker
↓
Node
七、创建生产环境变量
不要把真实生产 .env 提交到 GitHub。
服务器创建:
mkdir -p /www/env
然后:
vi /www/env/my-node-service.env
例如:
NODE_ENV=production
PORT=3000
DB_HOST=数据库地址
DB_USER=数据库用户
DB_PASSWORD=数据库密码
DB_NAME=数据库名称
JWT_SECRET=你的JWT密钥
保存以后:
/www/env/my-node-service.env
会由 Docker Compose 自动读取。
不要执行:
docker compose config
然后把完整结果发到聊天、日志或者公开页面。
因为这个命令会把 env_file 中的真实环境变量展开出来。
八、第一次把项目克隆到服务器
统一把 Git 项目放在:
/www/git/
创建目录:
mkdir -p /www/git
cd /www/git
克隆:
git clone git@github.com:你的GitHub用户名/my-node-service.git
如果服务器访问 GitHub 很慢,可以使用浅克隆:
git clone \
--depth=1 \
--branch main \
--single-branch \
git@github.com:你的GitHub用户名/my-node-service.git
进入目录:
cd /www/git/my-node-service
检查:
ls -la
应该能看到:
Dockerfile
docker-compose.yml
package.json
...
九、第一次手动测试 Docker
在自动部署之前,一定先手动把 Docker 跑通。
执行:
cd /www/git/my-node-service
docker compose up -d --build
查看:
docker compose ps
理想状态:
my-node-service Up ... (healthy)
再测试:
curl http://127.0.0.1:3000/health
应该返回:
{"status":"ok"}
如果这一步都不成功,先不要配置自动部署。
十、端口被占用怎么办
如果出现:
Bind for 127.0.0.1:3000 failed:
port is already allocated
说明 3000 已经被其他服务占用。
查看:
ss -lntp | grep ':3000'
再看 Docker:
docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Ports}}"
如果占用端口的是旧版项目,可以停止并删除旧容器。
如果是其他项目,就换端口。
例如:
ports:
- "127.0.0.1:3005:3000"
表示:
服务器:3005
容器:3000
此时宝塔反向代理也需要改成:
http://127.0.0.1:3005
十一、创建专门的 deploy 用户
不要让 GitHub Runner 一直使用 root。
root 下执行:
useradd -m -s /bin/bash deploy
把 deploy 加入 Docker 用户组:
usermod -aG docker deploy
确认:
id deploy
应该看到:
docker
测试:
su - deploy
然后:
docker ps
如果能看到 Docker 容器列表,说明权限正常。
十二、让 deploy 用户可以访问 GitHub
Runner 是使用 deploy 用户运行的,因此 deploy 自己也需要能够通过 SSH 拉 GitHub。
切换:
su - deploy
创建 SSH Key:
ssh-keygen \
-t ed25519 \
-C "deploy@server" \
-f ~/.ssh/id_ed25519 \
-N ""
查看公钥:
cat ~/.ssh/id_ed25519.pub
复制这一整行。
进入 GitHub:
头像
→ Settings
→ SSH and GPG keys
→ New SSH key
填写:
Title:
Server Deploy
Key type:
Authentication Key
把:
cat ~/.ssh/id_ed25519.pub
输出的公钥粘贴进去。
然后服务器测试:
ssh -T git@github.com
第一次会提示:
Are you sure you want to continue connecting?
输入:
yes
成功应该显示:
Hi xxx! You've successfully authenticated,
but GitHub does not provide shell access.
十三、把项目目录交给 deploy 用户
如果 /www/git/my-node-service 是之前用 root 克隆的,deploy 用户可能会出现:
fatal: detected dubious ownership in repository
不要只加 safe.directory。
直接把项目目录交给 deploy:
root 下:
chown -R deploy:deploy /www/git/my-node-service
然后:
su - deploy
cd /www/git/my-node-service
测试:
git remote -v
再:
git fetch --depth=1 origin main
如果没有报错就可以。
十四、安装 GitHub Self-hosted Runner
每个 GitHub 仓库注册一个 Runner。
Runner 统一放:
/home/deploy/runners/
例如:
/home/deploy/runners/my-node-service
创建目录:
mkdir -p /home/deploy/runners/my-node-service
修改所有者:
chown -R deploy:deploy /home/deploy/runners
切换:
su - deploy
进入:
cd /home/deploy/runners/my-node-service
十五、获取 GitHub Runner
进入仓库:
GitHub 仓库
→ Settings
→ Actions
→ Runners
→ New self-hosted runner
选择:
Linux
x64
GitHub 页面会提供下载命令。
例如:
curl -o actions-runner-linux-x64-x.x.x.tar.gz \
-L GitHub给出的下载地址
然后解压:
tar xzf actions-runner-linux-x64-x.x.x.tar.gz
解压完成应该看到:
config.sh
run.sh
svc.sh
bin/
externals/
十六、Runner 安装包只需要下载一次
Runner 包一般有 200MB 左右。
国内服务器下载 GitHub 可能很慢,所以建议缓存。
创建:
mkdir -p /home/deploy/runner-cache
把安装包移动进去:
mv actions-runner-linux-x64-x.x.x.tar.gz \
/home/deploy/runner-cache/
以后其他仓库直接:
mkdir -p /home/deploy/runners/其他项目
cd /home/deploy/runners/其他项目
tar xzf /home/deploy/runner-cache/actions-runner-linux-x64-x.x.x.tar.gz
不需要重新下载。
十七、注册 Runner
还是在 GitHub:
Settings
→ Actions
→ Runners
→ New self-hosted runner
页面会给一条:
./config.sh \
--url https://github.com/你的用户名/my-node-service \
--token 临时Token
直接执行。
接下来会询问配置。
Runner group:
直接回车
Runner 名称建议:
my-node-service-runner
Labels:
my-node-service,docker
Work folder:
_work
成功后应该显示:
Runner successfully added
Settings Saved
GitHub 页面:
Settings
→ Actions
→ Runners
就能看到 Runner。
十八、不要长期使用 ./run.sh
测试阶段可以:
./run.sh
看到:
Connected to GitHub
Listening for Jobs
说明注册成功。
但是不要长期这样运行。
因为关闭宝塔终端、SSH 断开或者终端异常,都可能导致 Runner 出问题。
正式环境应该使用 systemd。
十九、把 Runner 安装成系统服务
如果之前运行了:
./run.sh
先退出:
Ctrl + C
如果出现重复 Session:
A session for this runner already exists
检查:
pgrep -af "Runner.Listener"
如果有重复进程,可以停止:
pkill -f "/home/deploy/runners/my-node-service/bin/Runner.Listener"
然后 root 用户进入:
cd /home/deploy/runners/my-node-service
安装服务:
./svc.sh install deploy
启动:
./svc.sh start
查看状态:
./svc.sh status
正常应该看到:
Active: active (running)
这样即使:
关闭宝塔
关闭 SSH
服务器重启
Runner 也能继续工作。
二十、编写 GitHub Actions
项目创建:
.github/workflows/deploy.yml
完整示例:
name: Deploy Node API
on:
push:
branches:
- main
concurrency:
group: my-node-service-production
cancel-in-progress: false
jobs:
deploy:
name: Deploy Production
runs-on:
- self-hosted
- Linux
- X64
- my-node-service
timeout-minutes: 20
steps:
- name: Update source code
run: |
set -e
cd /www/git/my-node-service
echo "========== 更新代码 =========="
git fetch --depth=1 origin main
git reset --hard origin/main
echo ""
echo "========== 当前部署版本 =========="
git log -1 --oneline
- name: Deploy Docker
run: |
set -e
cd /www/git/my-node-service
echo "========== 构建并更新容器 =========="
if ! docker compose up \
-d \
--build \
--remove-orphans \
--wait \
--wait-timeout 60; then
echo ""
echo "========== 部署失败 =========="
docker compose ps || true
echo ""
echo "========== 容器日志 =========="
docker compose logs --tail=150 || true
echo ""
echo "========== 健康检查详情 =========="
docker inspect my-node-service \
--format='{{json .State.Health}}' || true
exit 1
fi
- name: Check container
run: |
cd /www/git/my-node-service
echo "========== 容器状态 =========="
docker compose ps
docker inspect my-node-service \
--format='Status={{.State.Status}} Health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}'
- name: Verify API
run: |
set -e
echo "========== API 健康检查 =========="
curl \
--fail \
--silent \
--show-error \
http://127.0.0.1:3000/health
echo ""
echo "API 健康检查成功"
- name: Cleanup Docker images
run: |
echo "========== 清理旧镜像 =========="
docker image prune -f
- name: Deployment completed
run: |
cd /www/git/my-node-service
echo ""
echo "========================================"
echo "my-node-service 部署成功"
echo "========================================"
git log -1 --oneline
docker compose ps
需要修改的地方主要有:
my-node-service
改成自己的项目名称。
以及:
3000
改成自己的宿主机端口。
二十一、为什么不用 actions/checkout
普通 GitHub Actions 经常这样:
- uses: actions/checkout@v4
但国内服务器访问 GitHub HTTPS 有时比较慢或者不稳定。
服务器已经有:
/www/git/my-node-service
而且已经配置:
deploy → GitHub SSH
所以直接:
cd /www/git/my-node-service
git fetch --depth=1 origin main
git reset --hard origin/main
更简单。
二十二、第一次测试自动部署
本地修改代码后:
git add .
提交:
git commit -m "test: auto deploy"
推送:
git push origin main
然后 GitHub 会自动执行:
Deploy Node API
打开:
GitHub 仓库
→ Actions
正常会依次看到:
Update source code ✅
Deploy Docker ✅
Check container ✅
Verify API ✅
Cleanup Docker images ✅
Deployment completed ✅
全部绿色说明部署成功。
二十三、最终部署流程
以后开发完成只需要:
git add .
git commit -m "feat: xxx"
git push origin main
服务器自动执行:
git fetch
↓
git reset
↓
docker build
↓
更新容器
↓
等待 healthy
↓
请求 /health
↓
清理旧镜像
↓
部署完成
整个过程不需要进入宝塔手动操作。
二十四、宝塔 Nginx 反向代理
假设 Docker:
127.0.0.1:3000
宝塔网站反向代理设置:
目标 URL:
http://127.0.0.1:3000
Nginx 大致类似:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
}
整个访问链路:
https://api.example.com
↓
宝塔 Nginx
↓
127.0.0.1:3000
↓
Docker
↓
Node/Koa
二十五、常用 Runner 管理命令
进入:
cd /home/deploy/runners/my-node-service
查看状态:
./svc.sh status
停止:
./svc.sh stop
启动:
./svc.sh start
也可以使用:
systemctl status actions.runner.xxx.service
二十六、常用 Docker 排查命令
查看容器:
docker ps
查看 Compose:
cd /www/git/my-node-service
docker compose ps
查看日志:
docker compose logs --tail=100
或者:
docker logs --tail=100 my-node-service
查看健康检查:
docker inspect my-node-service \
--format='{{json .State.Health}}'
测试接口:
curl -i http://127.0.0.1:3000/health
重新构建:
docker compose up -d --build
二十七、常见问题
1. port is already allocated
报错:
Bind for 127.0.0.1:3000 failed:
port is already allocated
检查:
ss -lntp | grep ':3000'
Docker:
docker ps
确认是不是旧容器占用。
2. Docker 显示 unhealthy
先测试:
docker exec my-node-service node -e \
"fetch('http://127.0.0.1:3000/health').then(async r => { console.log(r.status); console.log(await r.text()) })"
如果:
401
说明 /health 被鉴权拦截。
如果:
404
说明 /health 不存在或者地址写错。
正确应该是:
200
3. dubious ownership
报错:
fatal: detected dubious ownership in repository
说明项目目录不是 deploy 用户所有。
root 执行:
chown -R deploy:deploy /www/git/my-node-service
4. deploy 无法拉 GitHub
报错:
Permission denied (publickey)
测试:
su - deploy
ssh -T git@github.com
如果失败,重新检查:
/home/deploy/.ssh/id_ed25519
对应的公钥是否已经添加到 GitHub 账号。
5. Runner 提示 session already exists
检查:
pgrep -af "Runner.Listener"
同一个 Runner 应该只有一个 Listener。
如果有多个:
pkill -f "/home/deploy/runners/my-node-service/bin/Runner.Listener"
正式环境不要再运行:
./run.sh
统一使用:
./svc.sh start
6. Runner lost communication
先检查 Runner:
./svc.sh status
检查系统:
dmesg -T | grep -Ei "oom|out of memory|killed process"
检查内存:
free -h
检查 GitHub Actions 网络:
curl -I https://broker.actions.githubusercontent.com/
Runner 正式部署后尽量使用 systemd 服务,而不是宝塔终端里的 ./run.sh。
二十八、多项目怎么管理
如果一台服务器部署多个 Docker 项目,可以这样:
/www/git/
├── project-a
├── project-b
├── project-c
└── project-d
环境变量:
/www/env/
├── project-a.env
├── project-b.env
├── project-c.env
└── project-d.env
Runner:
/home/deploy/runners/
├── project-a
├── project-b
├── project-c
└── project-d
Runner 安装包统一缓存:
/home/deploy/runner-cache/
└── actions-runner-linux-x64-x.x.x.tar.gz
每个仓库注册自己的 Runner,例如:
project-a-runner
project-b-runner
project-c-runner
Labels:
project-a,docker
project-b,docker
project-c,docker
Workflow:
runs-on:
- self-hosted
- Linux
- X64
- project-a
这样不同项目不会串任务。
二十九、最终推荐的服务器目录
完整整理后:
/home/deploy/
│
├── runner-cache/
│ └── actions-runner-linux-x64-x.x.x.tar.gz
│
└── runners/
├── project-a/
├── project-b/
└── project-c/
/www/git/
├── project-a/
├── project-b/
└── project-c/
/www/env/
├── project-a.env
├── project-b.env
└── project-c.env
职责非常清楚:
/home/deploy/runners
→ GitHub Actions Runner
/www/git
→ 项目源码和 Docker 构建目录
/www/env
→ 生产环境配置
三十、部署完成后的日常使用
整套配置只需要做一次。
以后正常开发:
git add .
git commit -m "feat: 新功能"
git push origin main
然后等 GitHub Actions 变绿。
不需要:
登录服务器
进入宝塔
打开 Docker
重新构建镜像
删除容器
创建容器
重新填写环境变量
手动启动
只要看到:
Deploy Production ✅
就说明新版本已经部署完成。
这套方式适合:
Koa
Express
NestJS
Fastify
普通 Node.js API
其他 Docker 化 Node 后端
如果项目需要 TypeScript 编译或者 NestJS 构建,只需要调整 Dockerfile 的构建阶段,GitHub Actions 和 Self-hosted Runner 部署方式基本不需要改变。
private note
交流
文章暂不开放公开评论。如果你有想法、问题或建议,欢迎私下联系站长。
联系站长 →