Docker 容器化从 Dockerfile 到 Compose 编排
Docker 容器化应用部署入门到实践:从 Dockerfile 到 Compose 编排
本教程面向零基础开发者,系统讲解如何通过 Docker 将应用容器化,逐步掌握从编写 Dockerfile、构建镜像,到利用 Docker Compose 编排多服务应用的全流程。你将通过两个完整实例(单服务 Node.js 应用、多服务 Web+Redis 应用)边学边练,最终具备独立部署中小型应用的能力。
1. 基本功:理解容器与搭建环境
1.1 Docker 解决了什么问题
传统部署方式下,不同机器间的环境差异(操作系统、依赖库版本、配置文件)常导致“我这能跑,你那不行”的问题。Docker 通过容器技术将应用与其运行环境整体打包,形成轻量、可移植的镜像,运行时就成为相互隔离的容器。这与虚拟机相比,粒度更精细、资源消耗更低。
核心三要素:
- 镜像(Image):应用的模板,包含代码、运行时、系统库等。
- 容器(Container):镜像的运行实例,可启动、停止、删除。
- 仓库(Registry):存放镜像的地方(如 Docker Hub,私有仓库 Harbor 等)。
1.2 安装 Docker 与 Docker Compose
在 Linux/macOS/Windows 上均可安装 Docker Desktop(自带 Compose)或单独安装 Docker Engine。安装后执行以下命令验证:
docker --version
docker compose version
如果你的系统尚未安装,可参考官方文档:
- Linux:使用脚本自动安装
curl -fsSL https://get.docker.com | sh - macOS/Windows:下载 Docker Desktop 安装包
1.3 配置镜像加速(推荐)
国内访问 Docker Hub 较慢,建议配置镜像加速器(如阿里云、腾讯云等)。以 Linux 为例,编辑 /etc/docker/daemon.json:
{
"registry-mirrors": ["https://你的镜像加速地址"]
}
重启 Docker 服务生效。
2. 编写应用的说明书:Dockerfile
Dockerfile 是一个文本文件,包含构建镜像所需的全部指令。理解它是迈入容器化部署的第一步。
2.1 常用指令速查
| 指令 | 作用 | 示例 |
|---|---|---|
FROM |
指定基础镜像 | FROM node:18-alpine |
WORKDIR |
设置工作目录 | WORKDIR /app |
COPY |
将文件从宿主机复制到镜像 | COPY package*.json ./ |
RUN |
在镜像构建时执行命令 | RUN npm install |
CMD |
容器启动时默认执行的命令 | CMD ["node", "server.js"] |
EXPOSE |
声明容器监听的端口 | EXPOSE 3000 |
ENV |
设置环境变量 | ENV NODE_ENV=production |
ENTRYPOINT |
结合 CMD 使用,定义容器入口 | ENTRYPOINT ["docker-entrypoint.sh"] |
2.2 实战:编写 Node.js 应用的 Dockerfile
假设有一个简单的 Express 应用,目录结构如下:
myapp/
├── package.json
├── server.js
└── public/
package.json 示例片段:
{
"name": "myapp",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2"
}
}
server.js 内容:
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
app.get('/', (req, res) => res.send('Hello Docker!'));
app.listen(port, () => console.log(`App running on port ${port}`));
在项目根目录创建 Dockerfile(无扩展名):
# 第一步:选择极精简的Node.js基础镜像
FROM node:18-alpine
# 第二步:设置工作目录
WORKDIR /app
# 第三步:优先复制依赖描述文件,利用缓存层
COPY package*.json ./
# 第四步:安装生产依赖
RUN npm ci --only=production
# 第五步:复制应用源码
COPY . .
# 第六步:暴露端口(仅声明,实际映射需在运行时指定)
EXPOSE 3000
# 第七步:定义健康检查
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1
# 第八步:容器启动命令
CMD ["node", "server.js"]
2.3 编写 Dockerfile 的最佳实践
- 选择合适的基础镜像:优先使用
alpine版本,体积小、攻击面少。 - 利用构建缓存:先 COPY 不常变的文件(如
package.json)并执行安装,再 COPY 频繁改动的源码。 - 减少层数:将多条
RUN命令合并为一条,并用&&连接,同时清理缓存(rm -rf /var/lib/apt/lists/*)。 - 不要以 root 运行:通过
RUN addgroup和USER指令切换至非特权用户。 - 使用 .dockerignore:忽略
node_modules、.git等无关文件,避免复制到镜像。
示例 .dockerignore:
node_modules
npm-debug.log
.git
.env
3. 构建镜像与运行容器
3.1 构建镜像
在 myapp/ 目录下执行:
docker build -t myapp:1.0 .
-t指定镜像名称和标签(tag).表示构建上下文为当前目录
构建完成后查看镜像列表:
docker images
3.2 运行容器并映射端口
docker run -d --name myapp-container -p 8080:3000 myapp:1.0
-d后台运行--name容器命名,便于管理-p 宿主机端口:容器端口,访问http://localhost:8080即可看到 “Hello Docker!”
常用管理命令:
docker ps # 查看运行中的容器
docker stop myapp-container
docker start myapp-container
docker rm myapp-container # 删除已停止的容器
docker logs myapp-container # 查看日志
3.3 数据持久化与卷挂载
容器删除后内部数据会丢失。可通过两种方式实现持久化:
1. 绑定挂载 – 将宿主机目录映射到容器:
docker run -d -p 8080:3000 -v $(pwd)/data:/app/data myapp:1.0
2. 命名卷 – 由 Docker 管理,易迁移和备份:
docker volume create app-data
docker run -d -p 8080:3000 -v app-data:/app/data myapp:1.0
4. 多容器协同:走进 Docker Compose
当应用由多个服务(如 Web + 数据库 + 缓存)组成时,手动管理每个容器的启动顺序、网络连接、卷挂载会非常繁琐。Docker Compose 提供声明式的 YAML 文件来定义和运行一套服务。
4.1 Compose 文件核心结构
一个典型的 docker-compose.yml 包含以下顶层元素:
| 元素 | 说明 |
|---|---|
services |
定义各个服务(容器),是 Compose 的主干 |
networks |
自定义网络,服务间默认通过服务名通信 |
volumes |
定义可共享的命名卷 |
4.2 实战:定义 Web + Redis 应用
我们扩充之前的 Node.js 应用,使其访问 Redis 记录页面访问次数。
更新后的 server.js:
const express = require('express');
const redis = require('redis');
const app = express();
const client = redis.createClient({ url: 'redis://redis:6379' });
client.on('error', (err) => console.log('Redis Client Error', err));
client.connect().then(() => console.log('Connected to Redis'));
app.get('/', async (req, res) => {
const visits = await client.incr('visits');
res.send(`Hello Docker! This page has been visited ${visits} times.`);
});
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`App running on port ${port}`));
更新 package.json 依赖,加入 redis。
docker-compose.yml 文件:
version: "3.9"
services:
web:
build: .
ports:
- "8080:3000"
environment:
- NODE_ENV=production
- PORT=3000
depends_on:
- redis
restart: unless-stopped
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
restart: unless-stopped
volumes:
redis-data:
这个 Compose 文件完成了以下几件事:
- 构建
web服务(使用当前目录的 Dockerfile) - 拉取
redis官方镜像并挂载命名卷持久化数据 depends_on保证 Redis 先启动(但不确保 service 完全就绪,后续可加入 healthcheck 和 depends_on condition)- 两个服务默认处于同一自定义网络,web 可直接通过
redis主机名访问 Redis
4.3 启动、停止与扩展
在 docker-compose.yml 所在目录执行:
# 启动所有服务(后台)
docker compose up -d
# 查看服务状态
docker compose ps
# 查看各服务日志
docker compose logs -f
# 暂停所有服务
docker compose stop
# 完全删除容器、网络(卷默认保留)
docker compose down
# 删除同时清除卷
docker compose down -v
为了验证 Redis 工作,浏览器多次访问 http://localhost:8080,每次计数应增加。
扩展服务数量 对于无状态服务,可轻松伸缩:
docker compose up -d --scale web=3
此时 Docker 会以 web 服务为模板创建 3 个实例,并通过内置的 DNS 负载均衡分发请求。需要注意:如果端口映射固定为 8080:3000,会因宿主机端口冲突而失败。通常在生产中会使用反向代理(如 Nginx)处理,此处不做深入。
5. 编排实践:微服务型应用示例
让我们构建一个更接近真实场景的示例:采用 Python Flask (API服务) + Redis (缓存) + Nginx (反向代理) 的结构,展示 Compose 中网络隔离、环境变量控制、健康检查定义的完整用法。
5.1 项目目录结构
flask-redis-nginx/
├── api/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── app.py
├── nginx/
│ ├── Dockerfile
│ └── nginx.conf
└── docker-compose.yml
5.2 API 服务 (Flask)
api/app.py:
from flask import Flask, jsonify
import redis
import os
app = Flask(__name__)
cache = redis.Redis(host=os.environ.get('REDIS_HOST', 'redis'),
port=6379, decode_responses=True)
@app.route('/')
def index():
visits = cache.incr('hits')
return jsonify({"message": "Hello from Flask", "visits": visits})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
api/requirements.txt:
flask
redis
api/Dockerfile:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
HEALTHCHECK --interval=10s --timeout=3s \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/')" || exit 1
CMD ["python", "app.py"]
5.3 Nginx 反向代理
nginx/nginx.conf:
events {}
http {
upstream flask_app {
server api:5000;
}
server {
listen 80;
location / {
proxy_pass http://flask_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
nginx/Dockerfile:
FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
HEALTHCHECK --interval=10s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1
5.4 编写总编排文件
docker-compose.yml:
version: "3.9"
services:
api:
build: ./api
environment:
- REDIS_HOST=redis
networks:
- backend
depends_on:
redis:
condition: service_healthy
restart: unless-stopped
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis-data:/data
networks:
- backend
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped
nginx:
build: ./nginx
ports:
- "80:80"
networks:
- frontend
- backend
depends_on:
- api
restart: unless-stopped
networks:
frontend:
backend:
volumes:
redis-data:
本编排实现了:
- 网络隔离:API 和 Redis 处于
backend网络,Nginx 同时连接backend和frontend,对外只暴露 Nginx。 - 启动顺序控制:通过
depends_on配合healthcheck,确保 Redis 健康之后再启动 API 服务。 - 数据持久化:Redis 使用 AOF 模式,卷
redis-data保证断电不丢数据。
启动整个栈:
docker compose up -d
访问 http://localhost,即可看到 JSON 响应,每次刷新 visits 递增。
5.5 调试与常用命令
- 进入容器内部:
docker compose exec api sh - 临时运行一次性命令:
docker compose run --rm api python -c "import redis; ..." - 重新构建单个服务:
docker compose up -d --no-deps --build api - 查看资源使用:
docker stats
6. 从实践走向生产:下一步建议
- 镜像优化:使用多阶段构建减少最终镜像体积,移除构建工具。
- 安全扫描:集成
docker scan或 Trivy 检查镜像漏洞。 - 持续集成:在 CI 流程中自动构建、测试并推送镜像至私有仓库。
- 编排升级:当服务规模进一步扩大,可迁移至 Kubernetes;Docker Compose 可平滑过渡,两者概念相通。
- 配置管理:敏感信息使用 Docker Secrets(Swarm 模式)或外部加密服务,不在镜像和环境变量中硬编码。
7. 常见问题排查
- 容器启动后立即退出:检查
CMD指定的进程是否前台运行,查看日志docker logs <容器名>。 - 依赖服务连不上:确认 Compose 网络配置,服务间应使用服务名通信;若使用
depends_on,应用程序需具备重试逻辑应对服务的短暂不可用。 - 端口冲突:修改宿主机映射端口或停止占用进程。
- 构建缓存未更新:使用
docker compose build --no-cache强制重新构建。
通过本教程,你已掌握从单一 Dockerfile 编写到多服务 Compose 编排的完整技能链。即刻动手,将自己的项目容器化吧!