Docker 自动化部署实战:从构建镜像到一键上线
Docker 工作流自动化教程|一键部署
在现代软件开发中,应用的部署方式正在从“手工配置服务器”逐渐走向“自动化、标准化、可复制”。过去我们部署一个项目,往往需要登录服务器、安装运行环境、配置依赖、修改配置文件、启动进程、排查端口冲突等。不同服务器之间环境差异明显,开发环境能跑,到了测试或生产环境却频繁出错。
Docker 的出现很好地解决了这类问题。它通过容器化技术,将应用、运行环境、依赖库、配置文件等打包到一个镜像中,使应用可以在不同机器上以几乎一致的方式运行。而当 Docker 与自动化脚本、Docker Compose、CI/CD 工具结合后,就可以构建一套完整的工作流,实现从代码提交到服务部署的一键化操作。
本文将围绕 Docker 工作流自动化 展开,带你从基础概念、项目结构、镜像构建、容器编排、自动化脚本,到一键部署实践,系统掌握如何使用 Docker 提升部署效率。
一、为什么需要 Docker 工作流自动化?
在没有自动化部署之前,一个常见的部署流程可能是这样的:
- 登录服务器;
- 拉取最新代码;
- 安装或更新项目依赖;
- 修改环境变量;
- 执行数据库迁移;
- 构建前端资源;
- 停止旧服务;
- 启动新服务;
- 查看日志确认是否成功;
- 出现问题后再手动回滚。
这个过程不仅繁琐,而且容易出错。尤其当项目参与人员较多、服务数量增加、部署频率提高时,手动操作会成为明显的效率瓶颈。
Docker 工作流自动化的目标,就是将上述步骤沉淀为标准流程,通过脚本和配置文件实现:
- 一键构建镜像
- 一键启动服务
- 一键更新版本
- 一键查看日志
- 一键回滚部署
- 多环境一致运行
- 降低人为操作失误
从开发者角度看,自动化部署不仅节省时间,也能让团队成员更容易理解项目运行方式。只要项目仓库中包含 Dockerfile、docker-compose.yml 和部署脚本,新成员就可以快速启动完整环境。
二、Docker 自动化工作流的核心组成
一个完整的 Docker 自动化工作流通常由以下几个部分组成:
| 组成部分 | 作用 |
|---|---|
| Dockerfile | 定义镜像如何构建 |
| docker-compose.yml | 定义多容器服务如何启动 |
| .env | 管理环境变量 |
| Makefile 或 Shell 脚本 | 封装常用命令 |
| CI/CD 配置 | 自动构建、测试、发布 |
| 镜像仓库 | 存储和分发镜像 |
| 日志与监控 | 观察服务状态 |
其中,Dockerfile 和 docker-compose.yml 是最基础也是最重要的两个文件。前者解决“如何打包应用”的问题,后者解决“如何运行应用及其依赖服务”的问题。
例如,一个后端项目可能依赖 MySQL、Redis、Nginx。通过 Docker Compose,我们可以在一个配置文件中描述这些服务,然后使用一条命令启动整个系统:
docker compose up -d
相比手动分别启动数据库、缓存、Web 服务,这种方式更加统一、可靠,也更适合自动化部署。
三、准备一个示例项目结构
为了方便说明,我们假设有一个 Node.js 后端项目,项目目录如下:
my-docker-app/
├── src/
│ └── app.js
├── package.json
├── package-lock.json
├── Dockerfile
├── docker-compose.yml
├── .env
├── deploy.sh
└── Makefile
其中:
src/app.js是应用入口;package.json记录项目依赖;Dockerfile用于构建应用镜像;docker-compose.yml用于定义服务;.env用于存放环境变量;deploy.sh用于执行一键部署;Makefile用于封装常用命令。
示例应用代码如下:
// src/app.js
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.send('Hello Docker Workflow Automation!');
});
app.get('/health', (req, res) => {
res.json({
status: 'ok',
time: new Date().toISOString()
});
});
app.listen(port, () => {
console.log(`App is running on port ${port}`);
});
package.json 示例:
{
"name": "my-docker-app",
"version": "1.0.0",
"description": "Docker workflow automation demo",
"main": "src/app.js",
"scripts": {
"start": "node src/app.js"
},
"dependencies": {
"express": "^4.18.2"
}
}
四、编写 Dockerfile:让应用可复制构建
Dockerfile 是 Docker 镜像构建的说明书。它定义了应用运行所需的基础镜像、依赖安装、文件复制、启动命令等。
一个简单但相对规范的 Node.js Dockerfile 如下:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
逐行解释:
FROM node:20-alpine
表示使用 Node.js 20 的 Alpine 版本作为基础镜像。Alpine 镜像体积较小,适合生产环境。
WORKDIR /app
设置容器内部的工作目录为 /app。
COPY package*.json ./
先复制依赖描述文件,而不是直接复制所有代码。这样做可以充分利用 Docker 构建缓存。只要依赖文件没变,后续构建时就不需要重新安装依赖。
RUN npm ci --only=production
安装生产依赖。npm ci 比 npm install 更适合自动化环境,因为它会严格按照 package-lock.json 安装依赖。
COPY . .
复制项目代码到镜像中。
EXPOSE 3000
声明容器内部服务端口。
CMD ["npm", "start"]
容器启动时执行应用启动命令。
为了减少镜像构建上下文,建议添加 .dockerignore 文件:
node_modules
npm-debug.log
.git
.gitignore
Dockerfile
docker-compose.yml
.env
logs
dist
.dockerignore 的作用类似 .gitignore,可以避免无关文件被复制到镜像构建上下文中,从而减少镜像体积并提高构建速度。
五、使用 Docker Compose 编排服务
单个应用容器比较简单,但实际项目中通常需要多个服务协同运行。例如 Web 应用、数据库、Redis、Nginx 等。此时可以使用 Docker Compose。
下面是一个包含应用和 Redis 的 docker-compose.yml 示例:
services:
app:
build:
context: .
dockerfile: Dockerfile
image: my-docker-app:latest
container_name: my-docker-app
ports:
- "${APP_PORT}:3000"
environment:
NODE_ENV: production
PORT: 3000
REDIS_HOST: redis
REDIS_PORT: 6379
depends_on:
- redis
restart: unless-stopped
networks:
- app-network
redis:
image: redis:7-alpine
container_name: my-docker-redis
volumes:
- redis-data:/data
restart: unless-stopped
networks:
- app-network
volumes:
redis-data:
networks:
app-network:
driver: bridge
对应 .env 文件:
APP_PORT=3000
启动服务:
docker compose up -d
查看运行状态:
docker compose ps
查看日志:
docker compose logs -f app
停止服务:
docker compose down
这里有几个关键点值得注意。
首先,depends_on 可以控制服务启动顺序,但它并不代表被依赖服务已经完全可用。例如 Redis 容器启动了,并不一定表示 Redis 已经准备好处理请求。因此在生产项目中,应用层通常还需要添加重试机制,或者使用健康检查。
其次,restart: unless-stopped 表示容器异常退出后会自动重启,除非用户主动停止。这对生产服务非常有用。
再次,volumes 用于持久化数据。像 Redis、MySQL、PostgreSQL 等服务,如果不配置数据卷,容器删除后数据也可能丢失。
六、封装常用命令:使用 Makefile 提升效率
在团队协作中,如果每个人都需要记住一堆 Docker 命令,会增加沟通成本。因此我们可以使用 Makefile 封装常用操作。
示例 Makefile:
APP_NAME=my-docker-app
build:
docker compose build
up:
docker compose up -d
down:
docker compose down
restart:
docker compose down
docker compose up -d
logs:
docker compose logs -f app
ps:
docker compose ps
clean:
docker compose down -v
docker system prune -f
deploy:
sh deploy.sh
之后常用命令就可以简化为:
make build
make up
make logs
make deploy
这种方式的好处是命令语义清晰,新成员只需要查看 Makefile 就能知道项目支持哪些操作。同时也可以避免不同成员手动输入命令时出现参数不一致的问题。
七、编写一键部署脚本 deploy.sh
一键部署脚本是 Docker 工作流自动化的核心。它可以将拉取代码、构建镜像、启动服务、清理资源、检查状态等操作整合在一起。
下面是一个基础版 deploy.sh:
#!/bin/bash
set -e
echo "=============================="
echo "开始部署 my-docker-app"
echo "=============================="
echo "1. 拉取最新代码"
git pull
echo "2. 构建 Docker 镜像"
docker compose build
echo "3. 停止旧容器并启动新容器"
docker compose up -d
echo "4. 查看容器状态"
docker compose ps
echo "5. 清理无用镜像"
docker image prune -f
echo "=============================="
echo "部署完成"
echo "=============================="
给脚本添加执行权限:
chmod +x deploy.sh
执行部署:
./deploy.sh
或者:
make deploy
set -e 的含义是:当脚本中某一步命令执行失败时,立即终止脚本,避免继续执行后续操作导致状态混乱。
不过,基础版脚本还不够完善。在生产环境中,我们通常希望部署脚本具备以下能力:
- 检查 Docker 是否安装;
- 检查
.env文件是否存在; - 自动备份当前版本信息;
- 支持健康检查;
- 部署失败自动提示;
- 可选择是否清理旧镜像;
- 输出更清晰的日志。
下面给出一个增强版部署脚本。
#!/bin/bash
set -e
APP_NAME="my-docker-app"
HEALTH_URL="http://localhost:3000/health"
log() {
echo ""
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
check_command() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "错误:未检测到命令 $1,请先安装。"
exit 1
fi
}
log "开始部署 ${APP_NAME}"
log "检查基础环境"
check_command docker
check_command git
if [ ! -f ".env" ]; then
echo "错误:未找到 .env 文件,请先创建环境变量配置。"
exit 1
fi
log "拉取最新代码"
git pull
log "构建镜像"
docker compose build
log "启动服务"
docker compose up -d
log "等待服务启动"
sleep 5
log "执行健康检查"
if curl -f "${HEALTH_URL}" >/dev/null 2>&1; then
echo "健康检查通过:${HEALTH_URL}"
else
echo "健康检查失败,请查看日志:"
docker compose logs --tail=100 app
exit 1
fi
log "当前容器状态"
docker compose ps
log "清理无用镜像"
docker image prune -f
log "部署成功"
这个脚本在部署完成后会请求 /health 接口,如果应用没有正常响应,则输出最近日志并退出。这种健康检查机制非常实用,可以帮助我们更快发现部署异常。
八、镜像版本管理:避免 latest 带来的不确定性
很多初学者喜欢使用 latest 标签,例如:
image: my-docker-app:latest
在本地测试中这没有太大问题,但在生产环境中,latest 容易导致版本不明确。我们无法直观看出当前运行的是哪一次构建,也不方便回滚。
更推荐的做法是使用 Git Commit ID、版本号或构建时间作为镜像标签。例如:
IMAGE_TAG=$(git rev-parse --short HEAD)
docker build -t my-docker-app:${IMAGE_TAG} .
在 CI/CD 中,可以生成类似这样的镜像:
my-registry.com/my-docker-app:1.0.0
my-registry.com/my-docker-app:20250201-153000
my-registry.com/my-docker-app:a1b2c3d
这样做有几个好处:
- 版本可追踪:可以明确知道镜像来自哪次代码提交;
- 便于回滚:如果新版本有问题,可以快速切换到旧版本;
- 方便审计:部署记录更加清晰;
- 减少歧义:避免不同机器上的
latest指向不同内容。
如果使用 Docker Compose,可以通过 .env 管理镜像标签:
IMAGE_TAG=a1b2c3d
APP_PORT=3000
docker-compose.yml 中使用:
services:
app:
image: my-docker-app:${IMAGE_TAG}
ports:
- "${APP_PORT}:3000"
部署时更新 .env 中的 IMAGE_TAG,即可切换版本。
九、接入 CI/CD:从提交代码到自动部署
当本地 Docker 工作流已经稳定后,可以进一步接入 CI/CD,实现代码提交后自动构建和部署。常见工具包括:
- GitHub Actions
- GitLab CI/CD
- Jenkins
- Gitea Actions
- Drone
- Argo CD
以 GitHub Actions 为例,一个典型流程是:
- 开发者提交代码到主分支;
- GitHub Actions 自动运行测试;
- 测试通过后构建 Docker 镜像;
- 将镜像推送到镜像仓库;
- 服务器拉取新镜像并重启服务。
下面是一个简化版 GitHub Actions 配置:
name: Docker Deploy
on:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set image tag
run: echo "IMAGE_TAG=${GITHUB_SHA::7}" >> $GITHUB_ENV
- name: Login Docker Registry
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Build image
run: docker build -t yourname/my-docker-app:${IMAGE_TAG} .
- name: Push image
run: docker push yourname/my-docker-app:${IMAGE_TAG}
- name: Deploy to server
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /opt/my-docker-app
git pull
echo "IMAGE_TAG=${{ env.IMAGE_TAG }}" > .env
echo "APP_PORT=3000" >> .env
docker compose pull
docker compose up -d
docker image prune -f
这里使用了 GitHub Secrets 保存敏感信息,例如服务器地址、SSH 密钥、Docker Hub 密码等。不要把密码直接写在仓库配置文件中。
CI/CD 能够进一步减少人工操作,让部署过程更加标准化。但需要注意,自动部署到生产环境前,最好增加测试、审批或灰度发布环节,避免有问题的代码直接影响用户。
十、生产环境部署建议
Docker 让部署变得简单,但生产环境仍然需要谨慎设计。以下是一些实用建议。
1. 不要把敏感信息写进镜像
例如数据库密码、API Key、Token 等,不应该写在 Dockerfile 或代码中。推荐通过环境变量、配置中心、Secret 管理工具传入。
错误示例:
ENV DB_PASSWORD=123456
更好的方式是:
environment:
DB_PASSWORD: ${DB_PASSWORD}
然后在服务器 .env 中配置:
DB_PASSWORD=your_strong_password
2. 容器内尽量只运行一个主进程
一个容器最好只负责一个服务。例如应用容器只运行 Web 应用,数据库放在单独容器中。这样便于扩容、重启、监控和排错。
3. 配置健康检查
Docker Compose 支持 healthcheck:
services:
app:
image: my-docker-app:latest
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
健康检查可以帮助 Docker 或编排系统判断容器状态。
4. 持久化重要数据
数据库、上传文件、日志等需要持久化的数据,应使用 Docker Volume 或挂载宿主机目录。否则容器重建时可能造成数据丢失。
volumes:
mysql-data:
5. 限制容器资源
为了防止单个容器占用过多资源,可以设置 CPU 和内存限制。不同 Docker Compose 版本写法略有差异,在生产环境中应根据实际情况配置。
6. 定期清理无用资源
长期构建镜像会产生大量无用镜像和缓存。可以定期执行:
docker system df
docker image prune
docker builder prune
但在生产环境清理资源时要谨慎,避免误删仍需使用的数据卷。
十一、常见问题与排查方法
1. 容器启动后立即退出
查看日志:
docker compose logs app
常见原因包括:
- 启动命令错误;
- 依赖缺失;
- 环境变量未配置;
- 端口冲突;
- 程序运行时报错。
2. 端口无法访问
检查容器状态:
docker compose ps
检查端口映射是否正确:
ports:
- "3000:3000"
如果服务器使用了防火墙,还需要确认安全组或防火墙是否开放端口。
3. 修改代码后没有生效
可能原因:
- 镜像没有重新构建;
- 容器没有重新创建;
- 浏览器或代理缓存;
- 部署到了错误服务器或错误目录。
可以执行:
docker compose build --no-cache
docker compose up -d
4. docker compose 命令不可用
新版 Docker 使用:
docker compose
旧版可能是:
docker-compose
建议升级 Docker 到较新版本,统一使用 docker compose。
十二、推荐的一键部署完整流程
综合前面的内容,一个适合中小型项目的 Docker 一键部署流程可以设计为:
- 本地编写代码;
- 提交代码到 Git 仓库;
- 服务器执行部署脚本;
- 脚本拉取最新代码;
- 构建或拉取 Docker 镜像;
- 使用 Docker Compose 重启服务;
- 执行健康检查;
- 输出部署结果;
- 清理无用镜像;
- 如失败则查看日志并回滚。
对于团队来说,可以约定以下命令:
make deploy
make logs
make ps
make restart
这样部署就不再依赖某个人的经验,而是由脚本和配置固化下来。无论是新人接手项目,还是服务器迁移,都能显著降低成本。
十三、总结
Docker 工作流自动化的核心价值,在于把复杂、易错、依赖人工经验的部署过程,变成可重复、可追踪、可回滚的标准流程。
通过本文的实践,我们完成了以下内容:
- 使用 Dockerfile 构建应用镜像;
- 使用
.dockerignore优化构建上下文; - 使用 Docker Compose 编排多服务;
- 使用
.env管理环境变量; - 使用 Makefile 封装常用命令;
- 使用
deploy.sh实现一键部署; - 使用健康检查提升部署可靠性;
- 使用镜像标签管理版本;
- 初步接入 CI/CD 自动化部署。
对于个人项目而言,这套流程可以让部署更加轻松;对于团队项目而言,它能够统一环境、减少沟通成本、提升交付效率。随着项目规模扩大,还可以进一步引入 Kubernetes、服务网格、集中式日志、监控告警、灰度发布等能力。
但无论技术栈如何演进,自动化部署的本质始终不变:让部署过程标准化,让系统状态可控,让每一次上线都更稳定、更高效。