适用人群 ✅
- 个人项目 / 小团队 / 接私活交付
- 不想上 Jenkins / GitLab CI / GitHub Actions
- 服务器只有一台或几台,追求简单可控
目标 🎯
一条命令完成:拉代码 → 构建镜像 → 停旧起新 → 健康检查 → 回滚兜底
🧠 先讲清楚:为什么不是“反对 CI/CD”?
CI/CD 很强,但也有成本:
- 需要平台、Runner、权限、网络、配置维护
- 小项目上线频率不高,反而 “搭平台 > 写业务”
- 最痛的不是构建,而是上线可重复、可回滚
所以这篇做的方案是:
✅保留工程化能力(版本、回滚、健康检查)
❌不引入 CI/CD 平台依赖
👉 让部署回到最朴素的:脚本 + Docker
🗺️ 部署流程总览(看懂这张图就能用)
✅ 你将得到什么(这篇文章的交付物)
🧱 一个生产可用的
Dockerfile(支持多阶段构建)🔥 一个
deploy.sh:- 自动打版本 Tag(时间戳/commit)
- 停旧起新
- 健康检查失败自动回滚
- 可选:Nginx/反代不动,容器热切换
🗂️ 一个最小目录结构(直接套到你的项目)
0)目录结构(建议直接照抄)
以Go 服务为例(Python/Java 同理):
myapp/ cmd/myapp/main.go Dockerfile deploy.sh .env.prod1)Dockerfile(多阶段构建,镜像小、上线快)
这是 Go 版本,其他语言我后面给替换模板。
# -------- build stage -------- FROM golang:1.22 AS builder WORKDIR /app # 1) 依赖缓存 COPY go.mod go.sum ./ RUN go mod download # 2) 拷贝源码并构建 COPY . . RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o myapp ./cmd/myapp # -------- runtime stage -------- FROM gcr.io/distroless/static:nonroot WORKDIR /app COPY --from=builder /app/myapp /app/myapp # 服务端口(按你项目改) EXPOSE 8080 # 健康检查(推荐你在服务里提供 /health) # distroless 没有 curl,健康检查放在 deploy.sh 里做(更通用) USER nonroot:nonroot ENTRYPOINT ["/app/myapp"]2)一键部署脚本 deploy.sh(核心)
特点:
✅ 不依赖 CI 平台
✅ 只要服务器有 Docker + Git
✅ 失败自动回滚(评分加分项)
把下面脚本保存为deploy.sh,并chmod +x deploy.sh
#!/usr/bin/env bashset-euo pipefail# =========================# 配置区:按需修改# =========================APP_NAME="myapp"PORT="8080"# 容器内服务端口HOST_PORT="8080"# 宿主机映射端口HEALTH_URL="http://127.0.0.1:${HOST_PORT}/health"HEALTH_RETRY=20# 健康检查重试次数HEALTH_INTERVAL=1# 每次间隔秒ENV_FILE=".env.prod"# 环境变量文件(可选)DOCKERFILE="Dockerfile"NETWORK=""# e.g. "my-net" 如果你有自建网络可填# =========================# 自动生成版本号(时间戳 + git commit)# =========================TS="$(date+%Y%m%d%H%M%S)"GIT_SHA="$(gitrev-parse --short HEAD2>/dev/null||echo'nogit')"VERSION="${TS}-${GIT_SHA}"IMAGE="${APP_NAME}:${VERSION}"CONTAINER="${APP_NAME}"echo"🚀 Deploy start:${APP_NAME}"echo"📌 Version:${VERSION}"echo"📦 Image:${IMAGE}"# =========================# 记录上一个版本(用于回滚)# =========================PREV_IMAGE=""ifdockerps-a --format'{{.Names}}'|grep-qx"${CONTAINER}";thenPREV_IMAGE="$(dockerinspect -f'{{.Config.Image}}'"${CONTAINER}"2>/dev/null||true)"fiecho"🕰️ Prev image:${PREV_IMAGE:-<none>}"# =========================# 1) 更新代码(可按你的习惯:服务器拉代码 or scp 上传)# =========================echo"🔄 Git pull..."gitpull --rebase# =========================# 2) 构建镜像# =========================echo"🧱 Docker build..."dockerbuild -f"${DOCKERFILE}"-t"${IMAGE}".# =========================# 3) 停旧容器# =========================ifdockerps--format'{{.Names}}'|grep-qx"${CONTAINER}";thenecho"🛑 Stop running container..."dockerstop"${CONTAINER}"fiifdockerps-a --format'{{.Names}}'|grep-qx"${CONTAINER}";thenecho"🧹 Remove old container..."dockerrm"${CONTAINER}"fi# =========================# 4) 启新容器# =========================echo"▶️ Run new container..."RUN_ARGS=(-d --name"${CONTAINER}"-p"${HOST_PORT}:${PORT}"--restart=always)if[[-f"${ENV_FILE}"]];thenRUN_ARGS+=(--env-file"${ENV_FILE}")fiif[[-n"${NETWORK}"]];thenRUN_ARGS+=(--network"${NETWORK}")fidockerrun"${RUN_ARGS[@]}""${IMAGE}"# =========================# 5) 健康检查(失败自动回滚)# =========================echo"🩺 Health check:${HEALTH_URL}"ok=0foriin$(seq1"${HEALTH_RETRY}");doifcurl-fsS"${HEALTH_URL}">/dev/null2>&1;thenok=1breakfisleep"${HEALTH_INTERVAL}"doneif[["${ok}"-ne1]];thenecho"❌ Health check failed. Rolling back..."echo"🧾 Logs (last 120 lines):"dockerlogs --tail120"${CONTAINER}"||truedockerrm-f"${CONTAINER}"||trueif[[-n"${PREV_IMAGE}"]];thenecho"⏪ Rollback to:${PREV_IMAGE}"dockerrun"${RUN_ARGS[@]}""${PREV_IMAGE}"# 再验一次ok2=0foriin$(seq1"${HEALTH_RETRY}");doifcurl-fsS"${HEALTH_URL}">/dev/null2>&1;thenok2=1breakfisleep"${HEALTH_INTERVAL}"doneif[["${ok2}"-ne1]];thenecho"💥 Rollback also failed. Please check service manually."exit2fiecho"✅ Rolled back successfully."exit1elseecho"⚠️ No previous image to rollback."exit1fifiecho"✅ Deploy success!"# =========================# 6) 清理旧镜像(保留最近 5 个版本)# =========================echo"🧽 Cleanup old images..."KEEP=5dockerimages"${APP_NAME}"--format'{{.Repository}}:{{.Tag}} {{.CreatedAt}}'\|awk'{print $1}'\|tail-n +$((KEEP+1))\|xargs-rdockerrmi -f||trueecho"🎉 Done."3)环境变量文件 .env.prod(示例)
APP_ENV=prod LOG_LEVEL=info DB_DSN=mysql://user:pass@tcp(127.0.0.1:3306)/db✅
--env-file的好处:配置不进镜像、不进代码库(更安全)
4)服务器部署方式(两种最常用)
方式 A:服务器直接拉代码部署(最省事)
服务器上:
gitclone<你的仓库>cdmyappchmod+x deploy.sh ./deploy.sh方式 B:本地打包上传到服务器部署(更安全)
本地rsync/scp上传源码或构建产物,然后服务器执行deploy.sh。
5)为什么这个方案“更容易稳定”?
✅可重复:每次执行同样流程
✅可追溯:镜像 Tag 带版本号(时间戳 + commit)
✅可回滚:健康检查失败自动回退上一个版本
✅可部署:只依赖 Docker + Shell + Git
6)常见坑位
⚠️ 1)健康检查要写对
- 不要只返回
200 ok - 至少检查:依赖连通(DB/Redis)/ 主服务线程状态
⚠️ 2)端口占用/反代冲突
- 你用 Nginx 反代的话,尽量固定宿主机端口
- 容器内服务端口可变,但宿主机端口建议固定
⚠️ 3)日志别写到容器里
- 建议 stdout/stderr 打日志,让 Docker 管
- 或挂载卷到宿主机(
-v /var/log/myapp:/logs)
⚠️ 4)别把密钥写进 Dockerfile
- 用
.env.prod或 Secret 管理 - 不要
COPY .env.prod进镜像
✅ 结尾总结
CI/CD 不是必须,但可重复、可回滚、可追溯是必须。
对个人项目/小团队来说:
Shell + Dockerfile足够构建一条轻量但可靠的上线链路。
你少维护平台,多维护业务,效率反而更高。
👉 后续工具会持续补充进《程序员自动化工具箱》,喜欢的朋友别忘了点个关注订阅此专栏。