去年Stack Overflow调研显示,67%的开发者把"部署"列为最头疼的环节——比写代码还烦。不是不会写Dockerfile,是每次改完代码都要手动登录服务器、拉取、重启,像给自行车换胎还要先考个汽修证。
GitHub Actions + Docker这套组合,本质上是把"人肉运维"变成"代码说话"。但官方文档散得像拼图,新手往往在SSH密钥这一步摔跟头。本文按实际踩坑顺序重组,从本地能跑,到云端自动跑。
本地先能跑:一个最简Express服务
Node.js 16+和npm装好后,新建文件夹,npm init一路回车。装Express和类型定义:npm install express,npm install @types/express --save-dev。类型定义是TypeScript的"说明书",让编辑器能提示req.body里有什么。
端口别写死。const PORT = process.env.PORT || 8080,这是容器时代的生存法则——云厂商给你什么端口,你就用什么。写死8080,本地能跑,上云就炸。
根路由返回一句'Hello from the server!',够用了。console.log打印端口确认启动,这是调试时的"心跳信号"。
Dockerfile三行核心:FROM node:18-alpine(精简版镜像,省70%体积),WORKDIR /app,COPY . . && npm install && CMD ["node", "index.js"]。alpine版本没有bash,但部署场景不需要。
docker build -t node-app . 这行命令里,-t是打标签,.是当前目录作为构建上下文。注意:构建前确保Docker Desktop在运行,Windows用户常忘这一步。
测试运行:docker run -d -p 8080:8080 --rm node-app。-d是后台运行,-p是端口映射,--rm是容器停止后自动删除,省得docker ps -a看到一堆僵尸。
curl http://localhost:8080 或浏览器访问验证。能通,说明镜像没问题。停掉容器,准备上编排。
Docker Compose:从"能跑"到"好管"
单容器用docker run够了,但真实项目有数据库、缓存、日志收集。Compose把多容器关系写成YAML,一行命令起全套。
docker-compose.yml结构:version: '3.8',services下定义node-app,build: . 指向当前目录Dockerfile,ports做"8080:8080"映射,environment可写PORT=8080。现在改代码后,docker-compose up --build 自动重建镜像、重启容器。
关键区别:docker run是"手动挡",compose是"自动挡"。前者适合调试单容器,后者适合团队协作——新人clone仓库,一条命令环境就绪。
本地验证通过后,git init,git remote add origin <你的仓库地址>,push到GitHub。下一步是让云端服务器"自动"执行刚才的手动操作。
EC2准备:一台会听话的Ubuntu
AWS控制台启动t3.micro实例,Ubuntu 22.04 LTS,安全组开22(SSH)和8080(应用)端口。密钥文件.pem下载后chmod 400改权限,Windows用户用PuTTY或WSL,别直接双击。
SSH登录后先sudo apt update && sudo apt upgrade -y。更新不是形式主义——旧版OpenSSL漏洞能让你的容器被秒破。
Docker安装有两条路。Ubuntu仓库版:sudo apt install docker.io docker-compose -y,稳定但版本滞后。官方版:curl -fsSL https://get.docker.com | sh,装的是最新版,生产环境推荐。
装完sudo usermod -aG docker $USER,退出重登,免sudo运行docker。测试:docker run hello-world,看到"Hello from Docker"才算数。
服务器端mkdir -p /home/ubuntu/node-app,这是代码落地的目录。权限设成当前用户可写,避免GitHub Actions过来时报Permission denied。
GitHub Actions:把人工操作"代码化"
项目根目录建.github/workflows/deploy.yml。YAML对缩进敏感,用空格别用Tab,这是90%新手报错的原因。
触发条件:on: push: branches: [main]。main分支有代码推送,流水线自动启动。也可以加pull_request触发预部署检查,但单服务器场景没必要。
核心任务用appleboy/ssh-action@v1.0.3,这是社区维护的SSH连接器,比官方action省20行配置。需要四个secrets:SSH_HOST(EC2公网IP)、SSH_USERNAME(ubuntu或root)、SSH_KEY(私钥全文)、SSH_PORT(默认22)。
私钥生成:本地ssh-keygen -t rsa -b 4096,不设置passphrase(GitHub Actions无法交互输入)。id_rsa.pub内容写入EC2的~/.ssh/authorized_keys,id_rsa全文粘贴到GitHub Secrets。
流水线脚本分四步:cd进目录,docker-compose down停旧容器,docker system prune -f清废弃镜像,docker-compose up -d --build重建启动。set -e让任何命令失败时立即退出,防止"半吊子部署"。
echo打印日志是调试神器。GitHub Actions的网页控制台会实时显示,卡住时看最后一条echo就知道死在哪一步。
验证与排雷:从"通了"到"稳了"
push代码到main分支,GitHub仓库的Actions标签页看流水线状态。绿色勾表示服务器端已执行完docker-compose up。
浏览器访问http://:8080,看到"Hello from the server!"即成功。看不到?检查安全组8080端口是否对0.0.0.0/0开放,这是AWS新手第一坑。
容器日志排查:SSH登录后docker logs <容器ID>,或docker-compose logs -f实时跟踪。代码抛异常时,这里能看到堆栈。
一个隐蔽问题:docker-compose down会停容器但不会删镜像,长期运行磁盘会满。脚本里的docker system prune -f自动清理 dangling 镜像,但注意它会删除所有未被容器引用的镜像,包括手动build的测试镜像。
进阶可以加固:Dockerfile里用node:18-alpine而非latest,锁定版本防"昨天还能跑";docker-compose.yml加restart: unless-stopped,服务器重启后容器自动拉起;healthcheck指令让Docker自动重启崩溃的服务。
这套流程跑通后,每次git push约90秒自动上线。对比传统FTP上传或手动SSH部署,省下的时间够写两个功能模块。
但有个问题很少被讨论:当GitHub Actions成为部署的唯一入口,本地docker-compose up还重要吗?或者说,开发环境的"能跑"和生产环境的"自动跑",中间那道鸿沟,到底是技术问题还是习惯问题?
热门跟贴