从开发到上线:一套可直接复用的 Docker 自动化工作流实战源码
Docker 工作流自动化教程|附源码
在日常开发中,Docker 已经成为后端、前端、测试、运维团队非常常用的基础工具。它可以帮助我们快速构建一致的运行环境,减少“我本地可以运行,服务器不行”的问题。但如果只停留在手动执行 docker build、docker run、docker compose up 的阶段,随着项目复杂度提升,工作流依然会变得繁琐。
本文将围绕一个典型 Web 项目,系统讲解如何使用 Docker 实现开发、构建、测试、部署等流程的自动化,并提供完整示例源码,帮助你搭建一套可复用的 Docker 工作流。
一、为什么需要 Docker 工作流自动化?
在没有自动化之前,一个常见的项目发布流程可能是这样的:
- 开发人员在本地安装 Node.js、Python、MySQL、Redis 等环境;
- 手动执行依赖安装命令;
- 手动构建项目;
- 手动运行测试;
- 手动打包镜像;
- 手动上传镜像到服务器;
- 登录服务器后手动拉取镜像并重启服务。
这个流程的问题非常明显:
- 环境不一致:开发、测试、生产环境可能存在版本差异;
- 步骤繁琐:每次发布都要执行一堆命令;
- 容易出错:人工操作容易漏步骤;
- 难以回滚:如果没有统一镜像版本管理,回滚成本较高;
- 团队协作困难:新人需要花很久才能搭建完整环境。
Docker 工作流自动化的目标,就是把这些重复操作变成脚本、配置文件和流水线,让整个过程更加稳定、可追踪、可复用。
二、本文示例项目说明
本文使用一个简单的 Node.js Web 服务作为示例。项目结构如下:
docker-workflow-demo
├── src
│ └── app.js
├── test
│ └── app.test.js
├── package.json
├── Dockerfile
├── Dockerfile.dev
├── docker-compose.yml
├── docker-compose.dev.yml
├── .dockerignore
├── Makefile
└── .github
└── workflows
└── docker-ci.yml
这个项目包含以下能力:
- 使用 Dockerfile 构建生产镜像;
- 使用 Dockerfile.dev 构建开发环境;
- 使用 Docker Compose 启动应用和 Redis;
- 使用 Makefile 封装常用命令;
- 使用 GitHub Actions 自动测试和构建镜像;
- 支持镜像推送到 Docker Hub;
- 支持生产环境部署。
三、初始化 Node.js 示例项目
首先创建项目目录:
mkdir docker-workflow-demo
cd docker-workflow-demo
npm init -y
安装依赖:
npm install express redis
npm install --save-dev jest supertest nodemon
修改 package.json:
{
"name": "docker-workflow-demo",
"version": "1.0.0",
"description": "Docker workflow automation demo",
"main": "src/app.js",
"scripts": {
"start": "node src/app.js",
"dev": "nodemon src/app.js",
"test": "jest --runInBand"
},
"dependencies": {
"express": "^4.18.3",
"redis": "^4.6.13"
},
"devDependencies": {
"jest": "^29.7.0",
"nodemon": "^3.1.0",
"supertest": "^6.3.4"
}
}
创建应用入口文件 src/app.js:
const express = require("express");
const redis = require("redis");
const app = express();
const PORT = process.env.PORT || 3000;
const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379";
let redisClient;
async function initRedis() {
redisClient = redis.createClient({
url: REDIS_URL
});
redisClient.on("error", (err) => {
console.error("Redis Client Error:", err);
});
await redisClient.connect();
}
app.get("/", async (req, res) => {
res.json({
message: "Hello Docker Workflow!",
status: "ok"
});
});
app.get("/visits", async (req, res) => {
if (!redisClient) {
return res.status(500).json({
error: "Redis client not initialized"
});
}
const count = await redisClient.incr("visits");
res.json({
visits: count
});
});
if (require.main === module) {
initRedis()
.then(() => {
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
})
.catch((err) => {
console.error("Failed to start server:", err);
process.exit(1);
});
}
module.exports = app;
创建测试文件 test/app.test.js:
const request = require("supertest");
const app = require("../src/app");
describe("GET /", () => {
it("should return hello message", async () => {
const res = await request(app).get("/");
expect(res.statusCode).toBe(200);
expect(res.body.message).toBe("Hello Docker Workflow!");
expect(res.body.status).toBe("ok");
});
});
四、编写生产环境 Dockerfile
生产环境镜像应尽量保持小体积、安全和稳定。我们使用 Node.js 官方 Alpine 镜像作为基础镜像。
创建 Dockerfile:
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY src ./src
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["npm", "start"]
这个 Dockerfile 做了几件事:
- 使用
node:20-alpine减小镜像体积; - 设置工作目录
/app; - 优先复制
package.json和package-lock.json,利用 Docker 构建缓存; - 只安装生产依赖;
- 复制源码;
- 暴露 3000 端口;
- 使用
npm start启动服务。
构建镜像:
docker build -t docker-workflow-demo:latest .
运行容器:
docker run -p 3000:3000 docker-workflow-demo:latest
不过此时 /visits 接口依赖 Redis,如果没有 Redis 容器会启动失败。因此更推荐使用 Docker Compose 来编排多个服务。
五、编写开发环境 Dockerfile
开发环境和生产环境不同,开发环境通常需要:
- 安装 devDependencies;
- 支持热重载;
- 挂载本地代码;
- 保留调试能力。
创建 Dockerfile.dev:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
ENV NODE_ENV=development
ENV PORT=3000
EXPOSE 3000
CMD ["npm", "run", "dev"]
开发环境不用刻意追求镜像最小化,重点是提升开发效率。
六、编写 .dockerignore
.dockerignore 的作用类似 .gitignore,用于避免把无关文件复制到镜像中。
创建 .dockerignore:
node_modules
npm-debug.log
Dockerfile
Dockerfile.dev
docker-compose.yml
docker-compose.dev.yml
.git
.gitignore
.github
coverage
README.md
.env
这样可以减少镜像构建上下文大小,提高构建速度,同时避免敏感文件进入镜像。
七、使用 Docker Compose 编排服务
生产环境或准生产环境可以使用 docker-compose.yml:
version: "3.9"
services:
app:
build:
context: .
dockerfile: Dockerfile
image: docker-workflow-demo:latest
container_name: docker-workflow-app
ports:
- "3000:3000"
environment:
- REDIS_URL=redis://redis:6379
- NODE_ENV=production
depends_on:
- redis
restart: unless-stopped
redis:
image: redis:7-alpine
container_name: docker-workflow-redis
volumes:
- redis_data:/data
restart: unless-stopped
volumes:
redis_data:
启动服务:
docker compose up -d
查看服务状态:
docker compose ps
查看日志:
docker compose logs -f app
访问接口:
curl http://localhost:3000/
curl http://localhost:3000/visits
停止服务:
docker compose down
如果希望同时删除数据卷:
docker compose down -v
八、开发环境 Compose 配置
为了实现本地开发热重载,我们再创建一个 docker-compose.dev.yml:
version: "3.9"
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
image: docker-workflow-demo:dev
container_name: docker-workflow-app-dev
ports:
- "3000:3000"
environment:
- REDIS_URL=redis://redis:6379
- NODE_ENV=development
volumes:
- .:/app
- /app/node_modules
depends_on:
- redis
command: npm run dev
redis:
image: redis:7-alpine
container_name: docker-workflow-redis-dev
ports:
- "6379:6379"
volumes:
- redis_dev_data:/data
volumes:
redis_dev_data:
开发环境启动:
docker compose -f docker-compose.dev.yml up
现在你修改 src/app.js 后,nodemon 会自动重启服务,不需要手动重新构建镜像。
这里有一个细节:
volumes:
- .:/app
- /app/node_modules
第一行把本地代码挂载到容器内,第二行防止本地空目录覆盖容器中的 node_modules。这是 Node.js 项目使用 Docker 开发时非常常见的写法。
九、使用 Makefile 封装常用命令
虽然 Docker 命令已经比手动部署简单很多,但命令仍然偏长。我们可以使用 Makefile 封装常用操作。
创建 Makefile:
APP_NAME=docker-workflow-demo
IMAGE_NAME=docker-workflow-demo
IMAGE_TAG=latest
.PHONY: install
install:
npm install
.PHONY: test
test:
npm test
.PHONY: docker-build
docker-build:
docker build -t $(IMAGE_NAME):$(IMAGE_TAG) .
.PHONY: docker-run
docker-run:
docker run --rm -p 3000:3000 $(IMAGE_NAME):$(IMAGE_TAG)
.PHONY: dev
dev:
docker compose -f docker-compose.dev.yml up
.PHONY: dev-down
dev-down:
docker compose -f docker-compose.dev.yml down
.PHONY: up
up:
docker compose up -d
.PHONY: down
down:
docker compose down
.PHONY: logs
logs:
docker compose logs -f app
.PHONY: ps
ps:
docker compose ps
.PHONY: clean
clean:
docker compose down -v
docker compose -f docker-compose.dev.yml down -v
docker image prune -f
之后常用操作就可以简化为:
make dev
make up
make logs
make down
这种方式非常适合团队协作。新人拿到项目后,只需要查看 Makefile,就能快速知道项目支持哪些操作。
十、在容器中运行测试
测试自动化是 Docker 工作流中的重要部分。我们希望测试在统一环境里执行,而不是依赖开发者本地环境。
可以直接使用开发镜像执行测试:
docker compose -f docker-compose.dev.yml run --rm app npm test
也可以在 Makefile 中增加命令:
.PHONY: docker-test
docker-test:
docker compose -f docker-compose.dev.yml run --rm app npm test
执行:
make docker-test
这样无论团队成员使用 macOS、Windows 还是 Linux,只要安装了 Docker,就能用相同命令运行测试。
十一、使用多阶段构建优化镜像
前面生产环境 Dockerfile 已经比较简单,但在复杂项目中,通常需要构建步骤。例如前端项目需要 npm run build,Go 项目需要编译二进制文件,Java 项目需要打包 JAR。
对于 Node.js 服务,也可以使用多阶段构建,让测试和构建更加清晰:
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM deps AS test
COPY . .
RUN npm test
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY src ./src
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["npm", "start"]
构建时会先安装完整依赖,然后运行测试,测试通过后才生成最终生产镜像。如果测试失败,镜像构建就会中断。
构建命令:
docker build -t docker-workflow-demo:latest .
这种方式的好处是:
- 构建过程自带质量检查;
- 最终镜像不包含测试依赖;
- 不需要在生产镜像里保留无关文件;
- CI 流程更加简单可靠。
十二、配置 GitHub Actions 自动化流水线
接下来把本地 Docker 工作流升级为 CI/CD 工作流。以 GitHub Actions 为例,当代码推送到 main 分支或提交 Pull Request 时,自动运行测试并构建镜像。
创建 .github/workflows/docker-ci.yml:
name: Docker CI
on:
push:
branches:
- main
pull_request:
branches:
- main
env:
IMAGE_NAME: docker-workflow-demo
jobs:
test-and-build:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build test image
run: |
docker build -t $IMAGE_NAME:test .
- name: Run container smoke test
run: |
docker run -d --name app-test -p 3000:3000 \
-e REDIS_URL=redis://redis:6379 \
$IMAGE_NAME:test || true
docker ps -a
不过上面有一个问题:应用启动依赖 Redis,如果只启动应用镜像,可能会失败。更稳妥的做法是使用 Compose 执行测试。
改进版如下:
name: Docker CI
on:
push:
branches:
- main
pull_request:
branches:
- main
env:
IMAGE_NAME: docker-workflow-demo
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Run tests with Docker Compose
run: |
docker compose -f docker-compose.dev.yml run --rm app npm test
- name: Stop services
if: always()
run: |
docker compose -f docker-compose.dev.yml down -v
build:
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Build production image
run: |
docker build -t $IMAGE_NAME:${{ github.sha }} .
docker build -t $IMAGE_NAME:latest .
这个流水线分为两个任务:
test:使用 Docker Compose 启动测试环境并执行测试;build:测试通过后构建生产镜像。
十三、推送镜像到 Docker Hub
如果要把镜像推送到 Docker Hub,需要在 GitHub 仓库中配置两个 Secrets:
DOCKERHUB_USERNAMEDOCKERHUB_TOKEN
然后扩展 GitHub Actions:
name: Docker CI/CD
on:
push:
branches:
- main
env:
IMAGE_NAME: docker-workflow-demo
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Run tests with Docker Compose
run: |
docker compose -f docker-compose.dev.yml run --rm app npm test
- name: Stop services
if: always()
run: |
docker compose -f docker-compose.dev.yml down -v
build-and-push:
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Set image tags
run: |
echo "IMAGE_TAG=${GITHUB_SHA::7}" >> $GITHUB_ENV
echo "FULL_IMAGE=${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME" >> $GITHUB_ENV
- name: Login to Docker Hub
run: |
echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login \
-u "${{ secrets.DOCKERHUB_USERNAME }}" \
--password-stdin
- name: Build image
run: |
docker build -t $FULL_IMAGE:$IMAGE_TAG -t $FULL_IMAGE:latest .
- name: Push image
run: |
docker push $FULL_IMAGE:$IMAGE_TAG
docker push $FULL_IMAGE:latest
这样每次推送到 main 分支后,GitHub Actions 会自动:
- 拉取代码;
- 使用 Docker Compose 执行测试;
- 构建生产镜像;
- 使用短 commit hash 打标签;
- 同时更新
latest标签; - 推送镜像到 Docker Hub。
十四、生产服务器部署示例
在生产服务器上,可以准备一个简化版 docker-compose.prod.yml:
version: "3.9"
services:
app:
image: your-dockerhub-username/docker-workflow-demo:latest
container_name: docker-workflow-app
ports:
- "3000:3000"
environment:
- REDIS_URL=redis://redis:6379
- NODE_ENV=production
depends_on:
- redis
restart: unless-stopped
redis:
image: redis:7-alpine
container_name: docker-workflow-redis
volumes:
- redis_data:/data
restart: unless-stopped
volumes:
redis_data:
部署命令:
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
docker image prune -f
你也可以写成 deploy.sh:
#!/usr/bin/env bash
set -e
echo "Pulling latest images..."
docker compose -f docker-compose.prod.yml pull
echo "Starting services..."
docker compose -f docker-compose.prod.yml up -d
echo "Cleaning unused images..."
docker image prune -f
echo "Deployment completed."
赋予执行权限:
chmod +x deploy.sh
执行部署:
./deploy.sh
十五、自动化工作流最佳实践
1. 镜像标签不要只依赖 latest
latest 虽然方便,但不适合作为唯一版本标识。建议至少同时使用:
latest- Git commit hash
- 语义化版本,例如
v1.0.0
这样当新版本出现问题时,可以快速回滚到旧版本。
示例:
docker pull yourname/docker-workflow-demo:a1b2c3d
2. 不要把敏感信息写进镜像
不要在 Dockerfile 中写入数据库密码、API Token、私钥等敏感内容。推荐使用:
- 环境变量;
- Docker secrets;
- CI/CD 平台 secrets;
- 云厂商密钥管理服务。
错误示例:
ENV DB_PASSWORD=123456
正确做法是在运行容器时注入:
docker run -e DB_PASSWORD=your_password image_name
3. 控制镜像体积
减小镜像体积可以提升拉取速度,也能减少安全风险。常见做法包括:
- 使用 Alpine 或 slim 镜像;
- 使用多阶段构建;
- 删除无用缓存;
- 配置
.dockerignore; - 不在生产镜像中安装开发依赖。
4. 健康检查
生产环境建议为服务增加健康检查。例如在 Compose 中配置:
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/"]
interval: 30s
timeout: 5s
retries: 3
完整示例:
services:
app:
image: your-dockerhub-username/docker-workflow-demo:latest
ports:
- "3000:3000"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/"]
interval: 30s
timeout: 5s
retries: 3
5. 日志交给标准输出
容器化应用推荐把日志输出到 stdout/stderr,而不是写入容器内部文件。这样可以直接通过以下命令查看日志:
docker logs container_name
docker compose logs -f app
在生产环境中,可以进一步接入 ELK、Loki、Prometheus、Grafana 等监控日志系统。
十六、常见问题排查
1. 修改代码后容器没有更新
如果是生产镜像,需要重新构建:
docker compose up -d --build
如果是开发环境,确认是否正确挂载了代码目录:
volumes:
- .:/app
2. 容器内无法连接 Redis
检查环境变量:
REDIS_URL=redis://redis:6379
在 Docker Compose 网络中,服务名 redis 会自动解析为 Redis 容器地址,而不是 localhost。
很多初学者会写成:
redis://localhost:6379
这在容器内通常是不对的,因为 localhost 指的是应用容器自身,而不是 Redis 容器。
3. 端口被占用
如果本地 3000 端口被占用,可以修改 Compose:
ports:
- "3001:3000"
表示宿主机使用 3001 端口,容器内部仍然使用 3000 端口。
4. node_modules 出现平台兼容问题
如果你在宿主机安装依赖,再挂载到 Linux 容器中,可能出现依赖不兼容。建议依赖安装在容器中完成,并使用匿名卷保护容器内的 node_modules:
volumes:
- .:/app
- /app/node_modules
十七、完整源码汇总
为了方便复制,这里汇总核心文件。
src/app.js
const express = require("express");
const redis = require("redis");
const app = express();
const PORT = process.env.PORT || 3000;
const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379";
let redisClient;
async function initRedis() {
redisClient = redis.createClient({
url: REDIS_URL
});
redisClient.on("error", (err) => {
console.error("Redis Client Error:", err);
});
await redisClient.connect();
}
app.get("/", async (req, res) => {
res.json({
message: "Hello Docker Workflow!",
status: "ok"
});
});
app.get("/visits", async (req, res) => {
if (!redisClient) {
return res.status(500).json({
error: "Redis client not initialized"
});
}
const count = await redisClient.incr("visits");
res.json({
visits: count
});
});
if (require.main === module) {
initRedis()
.then(() => {
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
})
.catch((err) => {
console.error("Failed to start server:", err);
process.exit(1);
});
}
module.exports = app;
Dockerfile
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM deps AS test
COPY . .
RUN npm test
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY src ./src
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["npm", "start"]
docker-compose.dev.yml
version: "3.9"
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
image: docker-workflow-demo:dev
ports:
- "3000:3000"
environment:
- REDIS_URL=redis://redis:6379
- NODE_ENV=development
volumes:
- .:/app
- /app/node_modules
depends_on:
- redis
command: npm run dev
redis:
image: redis:7-alpine
ports:
- "6379:6379"
Makefile
IMAGE_NAME=docker-workflow-demo
IMAGE_TAG=latest
.PHONY: dev
dev:
docker compose -f docker-compose.dev.yml up
.PHONY: docker-test
docker-test:
docker compose -f docker-compose.dev.yml run --rm app npm test
.PHONY: build
build:
docker build -t $(IMAGE_NAME):$(IMAGE_TAG) .
.PHONY: up
up:
docker compose up -d
.PHONY: down
down:
docker compose down
.PHONY: logs
logs:
docker compose logs -f app
十八、总结
本文从一个 Node.js 示例项目出发,完整演示了如何使用 Docker 搭建自动化工作流。我们完成了以下内容:
- 使用 Dockerfile 构建生产镜像;
- 使用 Dockerfile.dev 构建开发环境;
- 使用 Docker Compose 编排应用和 Redis;
- 使用
.dockerignore优化构建上下文; - 使用 Makefile 封装常用命令;
- 使用 Docker 统一测试环境;
- 使用 GitHub Actions 实现自动测试、构建和推送镜像;
- 给出生产环境部署方案和最佳实践。
Docker 工作流自动化的核心思想不是“把命令换成 Docker 命令”,而是把开发、测试、构建、发布这些环节标准化、配置化、可重复化。只要把这些基础设施搭好,团队后续的协作效率、部署稳定性和问题排查能力都会明显提升。
对于中小型项目,可以从 Dockerfile + docker-compose + Makefile 开始;对于团队项目,可以进一步接入 CI/CD;对于生产规模更大的系统,则可以继续演进到 Kubernetes、Helm、Argo CD 等更完整的云原生部署体系。