上一篇 下一篇 分享链接 返回 返回顶部

从开发到上线:一套可直接复用的 Docker 自动化工作流实战源码

发布人:慈云数据-客服中心 发布时间:14小时前 阅读量:0

Docker 工作流自动化教程|附源码

在日常开发中,Docker 已经成为后端、前端、测试、运维团队非常常用的基础工具。它可以帮助我们快速构建一致的运行环境,减少“我本地可以运行,服务器不行”的问题。但如果只停留在手动执行 docker builddocker rundocker compose up 的阶段,随着项目复杂度提升,工作流依然会变得繁琐。

本文将围绕一个典型 Web 项目,系统讲解如何使用 Docker 实现开发、构建、测试、部署等流程的自动化,并提供完整示例源码,帮助你搭建一套可复用的 Docker 工作流。


一、为什么需要 Docker 工作流自动化?

在没有自动化之前,一个常见的项目发布流程可能是这样的:

  1. 开发人员在本地安装 Node.js、Python、MySQL、Redis 等环境;
  2. 手动执行依赖安装命令;
  3. 手动构建项目;
  4. 手动运行测试;
  5. 手动打包镜像;
  6. 手动上传镜像到服务器;
  7. 登录服务器后手动拉取镜像并重启服务。

这个流程的问题非常明显:

  • 环境不一致:开发、测试、生产环境可能存在版本差异;
  • 步骤繁琐:每次发布都要执行一堆命令;
  • 容易出错:人工操作容易漏步骤;
  • 难以回滚:如果没有统一镜像版本管理,回滚成本较高;
  • 团队协作困难:新人需要花很久才能搭建完整环境。

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 做了几件事:

  1. 使用 node:20-alpine 减小镜像体积;
  2. 设置工作目录 /app
  3. 优先复制 package.jsonpackage-lock.json,利用 Docker 构建缓存;
  4. 只安装生产依赖;
  5. 复制源码;
  6. 暴露 3000 端口;
  7. 使用 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_USERNAME
  • DOCKERHUB_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 会自动:

  1. 拉取代码;
  2. 使用 Docker Compose 执行测试;
  3. 构建生产镜像;
  4. 使用短 commit hash 打标签;
  5. 同时更新 latest 标签;
  6. 推送镜像到 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 等更完整的云原生部署体系。

目录结构
全文