Docker 与 Docker Compose 从理论到实战教程
Docker 与 Docker Compose 从理论到实战教程
适用环境:Ubuntu 26.04 LTS(resolute)/ Docker 29.6.1 / Docker Compose v5.2.0
是否实测:所有镜像版本、命令、配置均已实测
覆盖范围:镜像/容器/网络/卷/Dockerfile 理论 + 11 个实战案例(全部跑通)
目录
第一篇:理论基础
- Docker 是什么
- 核心架构与名词
- 镜像(Image)详解
- 容器(Container)生命周期
- 网络(Network)模型
- 卷(Volume)持久化
- Dockerfile 指令全景
- Docker Compose 概念
第二篇:实战案例
- 案例 1:单容器运行 Nginx
- 案例 2:自定义静态站点 + 端口映射
- 案例 3:Nginx 反向代理多站点
- 案例 4:MySQL 持久化与健康检查
- 案例 5:Redis 持久化与配置
- 案例 6:PostgreSQL 18 数据卷备份
- 案例 7:Node.js 多阶段构建
- 案例 8:Python Flask 多阶段构建
- 案例 9:WordPress 全栈(MySQL + Redis + WP + Nginx)
- 案例 10:Compose 高级特性(profile / secrets / 资源限制)
- 案例 11:Portainer 可视化管理
第三篇:附录
第一篇:理论基础
1. Docker 是什么
Docker 是一个操作系统级虚拟化平台,基于 Linux 内核的 cgroup、namespace、unionFS 三大机制,把应用及其依赖打包成标准化的"镜像",再以"容器"形式运行。
1.1 与传统虚拟机的对比
| 维度 | 虚拟机(VM) | 容器(Container) |
|---|---|---|
| 虚拟化层级 | 硬件级(Hypervisor) | 操作系统级 |
| 启动时间 | 30s~分钟 | 毫秒~秒 |
| 镜像大小 | GB 级 | MB 级 |
| 性能损耗 | 10%~20% | < 2% |
| 隔离性 | 强(独立内核) | 中(共享内核) |
| 单机密度 | 数十个 | 数百~数千个 |
| 典型代表 | VMware / VirtualBox | Docker / containerd / Podman |
1.2 Docker 的三大核心机制
┌────────────────────────────────────────────┐
│ Namespace(命名空间):隔离视图 │
│ - PID:进程号 │
│ - NET:网络(IP/端口/路由) │
│ - MNT:文件系统挂载点 │
│ - UTS:主机名/域名 │
│ - IPC:进程间通信 │
│ - USER:用户/用户组 │
└────────────────────────────────────────────┘
┌────────────────────────────────────────────┐
│ Cgroup(控制组):限制资源 │
│ - CPU、内存、磁盘 IO、网络带宽 │
│ - 用于 docker run --memory / --cpus │
└────────────────────────────────────────────┘
┌────────────────────────────────────────────┐
│ UnionFS(联合文件系统):分层镜像 │
│ - Overlay2(Docker 默认) │
│ - 每个 Dockerfile 指令产生一层 │
│ - 镜像层只读,容器层可写 │
└────────────────────────────────────────────┘
2. 核心架构与名词
2.1 Docker 引擎架构
Docker CLI(docker / docker compose 命令)
│
▼
Docker REST API(Unix socket / TCP)
│
▼
dockerd(Docker 守护进程,常驻后台)
│
┌─────────┼─────────┐
▼ ▼ ▼
containerd runc network/volume 插件
(容器管理) (运行时) (网络/卷驱动)
2.2 关键名词
| 名词 | 含义 |
|---|---|
| Image(镜像) | 只读模板,由多层叠加而成,用于创建容器 |
| Container(容器) | 镜像的运行实例,有可写层 |
| Registry(仓库) | 存储镜像的服务(Docker Hub / 阿里云 ACR / 私有 Harbor) |
| Volume(卷) | 绕过容器 UFS 的持久化存储 |
| Network(网络) | 容器间通信的虚拟网络 |
| Dockerfile | 构建镜像的配方文件 |
| Compose | 多容器编排工具,使用 YAML 描述服务栈 |
2.3 本机环境(本教程基于)
| 组件 | 版本 |
|---|---|
| OS | Ubuntu 26.04 LTS(resolute) |
| Docker Engine | 29.6.1(build 8900f1d) |
| Docker Compose | v5.2.0(插件版) |
| containerd | v2.2.5 |
| 存储驱动 | overlay2(已关闭 containerd-snapshotter) |
3. 镜像(Image)详解
3.1 镜像分层结构
┌──────────────────────────────────────┐ ← 顶层:容器可写层(thin R/W)
├──────────────────────────────────────┤ ← CMD/ENTRYPOINT 配置
├──────────────────────────────────────┤ ← RUN apt install xxx
├──────────────────────────────────────┤ ← COPY ./app
├──────────────────────────────────────┤ ← WORKDIR /app
├──────────────────────────────────────┤ ← 基础镜像(如 node:24-alpine)
核心特性:
- 镜像层是只读的,多个容器共享同一镜像层,节省磁盘
- 同一基础镜像的多个容器,只额外占用各自的可写层
- 镜像分发时只传输差异层(CDN 友好)
3.2 镜像标识
完整格式:[REGISTRY/]NAME[:TAG][@DIGEST]
nginx:1.30.3-alpine → 等同 docker.io/library/nginx:1.30.3-alpine
myregistry.com/myapp:v1 → 私有仓库
nginx@sha256:abc123... → 摘要(不可变,最安全)
3.3 镜像操作命令速览
docker pull nginx:1.30.3-alpine # 拉取
docker images # 列出本地镜像
docker rmi nginx:1.30.3-alpine # 删除镜像
docker tag nginx:1.30.3-alpine my:v1 # 打标签
docker save nginx -o nginx.tar # 导出
docker load -i nginx.tar # 导入
docker image prune -a # 清理未使用
docker history nginx:1.30.3-alpine # 查看层历史
docker inspect nginx:1.30.3-alpine # 元数据 JSON
4. 容器(Container)生命周期
4.1 状态机
docker create
↓
created ──→ running ──→ paused ──→ running
↓ ↑
docker start docker unpause
↓
stopped ──→ removed
4.2 核心命令
# 创建 + 启动
docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
-d, --detach 后台运行
-i, --interactive 保持 STDIN
-t, --tty 分配伪终端(与 -i 合写 -it)
--name NAME 容器名
-p HOST:CONT 端口映射
-v HOST:CONT[:ro] 卷挂载
--rm 容器停止后自动删除
--restart POLICY 重启策略(no/always/on-failure/unless-stopped)
--network NET 加入网络
-e KEY=VAL 环境变量
--hostname HOST 容器内主机名
--user UID 运行用户
--cpus N 限制 CPU
--memory SIZE 限制内存
# 生命周期
docker start NAME # 启动已停止
docker stop NAME # 优雅停止(SIGTERM,10s 后 SIGKILL)
docker kill NAME # 强制停止
docker restart NAME # 重启
docker pause NAME # 暂停(cgroup freezer)
docker rm NAME # 删除容器
docker rm -f NAME # 强制删除运行中的
# 运维
docker logs NAME # 查看日志(-f 跟踪)
docker exec -it NAME bash # 进入容器
docker stats # 实时资源占用
docker top NAME # 容器内进程
docker port NAME # 端口映射
docker diff NAME # 容器相对镜像的文件系统变更
docker cp NAME:/path ./local # 复制
5. 网络(Network)模型
5.1 网络驱动
| 驱动 | 用途 | 特点 |
|---|---|---|
| bridge | 默认,单机容器互联 | NAT,容器通过 IP 互通;自定义 bridge 支持 DNS |
| host | 直接用宿主机网络栈 | 无网络隔离,最高网络性能 |
| none | 完全无网络 | 用于离线批处理 |
| overlay | 跨主机(Swarm 模式) | 多机网络 |
| macvlan | 容器有独立 MAC | 接入物理网络,像真实设备 |
| ipvlan | 类似 macvlan 但共享 MAC | 解决 MAC 表溢出 |
5.2 bridge 网络默认行为
Docker0 Bridge(172.17.0.0/16)
│ │ │
veth veth veth
┌──────┐ ┌──────┐ ┌──────┐
│ CT 1 │ │ CT 2 │ │ CT 3 │
│.1 │ │.2 │ │.3 │
└──────┘ └──────┘ └──────┘
↑ ↑
│ 默认 bridge 容器只能用 IP 互访 │
│ 自定义 bridge 支持 DNS 名称解析 │
5.3 自定义网络的优势
docker network create my-net # 创建自定义 bridge
docker run -d --network my-net --name web nginx
docker run -d --network my-net --name api myapi
# 在 api 容器里:ping web ← DNS 解析到 web 容器 IP
5.4 端口发布(Port Mapping)
-p 8080:80 # 主机 8080 → 容器 80(TCP 默认)
-p 127.0.0.1:8080:80 # 仅监听 127.0.0.1(更安全)
-p 8080:80/tcp # 显式 TCP
-p 8080:80/udp # UDP
-P # 随机主机端口(暴露 Dockerfile EXPOSE 的所有端口)
6. 卷(Volume)持久化
6.1 三种数据持久化方式
| 类型 | 说明 | 用途 |
|---|---|---|
| named volume | Docker 管理的命名卷,路径在 /var/lib/docker/volumes/ |
生产首选,便于备份迁移 |
| bind mount | 挂载主机任意路径 | 开发(热更新)、配置文件注入 |
| tmpfs | 内存盘(Linux 宿主机) | 临时敏感数据(密码、token) |
6.2 命令对照
# Named Volume
docker volume create my-data
docker run -v my-data:/path/in/container image
# Bind Mount
docker run -v /host/path:/path/in/container[:ro] image
# tmpfs
docker run --tmpfs /path:rw,size=100m image
6.3 备份与迁移 named volume
# 备份 volume 到当前目录
docker run --rm \
-v my-data:/source:ro \
-v $(pwd):/backup \
alpine tar czf /backup/my-data.tar.gz -C /source .
# 从备份恢复
docker run --rm \
-v my-data:/target \
-v $(pwd):/backup \
alpine tar xzf /backup/my-data.tar.gz -C /target
7. Dockerfile 指令全景
7.1 指令分类速查
| 类别 | 指令 | 用途 |
|---|---|---|
| 基础 | FROM |
指定基础镜像 |
| 基础 | ARG |
构建期变量 |
| 元数据 | LABEL |
标签(key=value) |
| 元数据 | ENV |
环境变量(运行时可见) |
| 元数据 | MAINTAINER |
维护者(已废弃,用 LABEL) |
| 工作目录 | WORKDIR |
设置工作目录(不存在则创建) |
| 文件 | COPY |
复制本地文件到镜像 |
| 文件 | ADD |
COPY + 自动解压 + 支持 URL(一般用 COPY) |
| 构建 | RUN |
执行命令(产生新层) |
| 构建 | EXPOSE |
声明监听端口(文档作用) |
| 构建 | VOLUME |
声明匿名卷挂载点 |
| 构建 | USER |
切换运行用户 |
| 构建 | ONBUILD |
当前镜像作为基础镜像时触发 |
| 构建 | HEALTHCHECK |
健康检查 |
| 启动 | CMD |
默认启动命令(可被覆盖) |
| 启动 | ENTRYPOINT |
入口点(接收参数) |
| 启动 | SHELL |
切换默认 shell |
| 构建 | STOPSIGNAL |
停止信号 |
7.2 CMD vs ENTRYPOINT
# CMD 三种写法(仅最后一条生效)
CMD ["node", "server.js"] # exec 形式(推荐)
CMD ["node", "server.js"] # exec 形式
CMD node server.js # shell 形式(包装为 sh -c)
# ENTRYPOINT 不会被 docker run CMD 覆盖,会拼接到 ENTRYPOINT 末尾
ENTRYPOINT ["node"]
CMD ["server.js"]
# docker run myimg → node server.js
# docker run myimg other.js → node other.js ← CMD 被覆盖
7.3 多阶段构建模板
# 阶段1:构建(带编译器)
FROM golang:1.25-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o app .
# 阶段2:运行时(极简)
FROM alpine:3.23
LABEL version="1.0.0"
RUN apk add --no-cache ca-certificates && adduser -D -u 1000 app
COPY --from=builder /build/app /usr/local/bin/app
USER app
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/app"]
8. Docker Compose 概念
8.1 演进
docker-compose v1(Python 写的独立二进制,已废弃)
↓
docker compose v2(Go 插件,Docker CLI 子命令,2026 年现状)
↓
docker compose v3+(与 Swarm/Kubernetes 集成)
8.2 关键概念
- Project:当前 compose 文件所在目录就是一个项目
- Service:一个容器配置
- Network:服务互联网络
- Volume:命名卷
- Config / Secret:配置/敏感数据注入
- Profile:按需启用的服务分组
8.3 常用命令
docker compose up -d # 启动(后台)
docker compose down # 停止并删除容器、网络
docker compose down -v # 同时删除卷
docker compose ps # 服务状态
docker compose logs -f [service] # 日志
docker compose exec SERVICE sh # 进入容器
docker compose pull # 拉取所有镜像
docker compose build # 构建
docker compose config # 校验 + 显示最终配置
docker compose --profile dev up -d # 启用 profile
docker compose --env-file .env.prod up # 指定环境文件
docker compose scale api=3 # 扩缩容(旧写法)
8.4 compose.yaml vs docker-compose.yml
compose.yaml是新规范名(Compose Spec)docker-compose.yml仍然兼容- 同一目录只能有一个
第二篇:实战案例
所有案例均经过实测验证(2026-06-27),命令和配置可直接复制运行。
工作目录:/home/lhz/opt/docker-lab/。
9. 案例 1:单容器运行 Nginx
目标:5 分钟跑通第一个容器。
# 1. 拉取镜像(实测:1.30.3 是当前 stable,2026-04-14 发布)
sudo docker pull nginx:1.30.3-alpine
# 2. 后台启动
sudo docker run -d --name web -p 8080:80 nginx:1.30.3-alpine
# 3. 查看容器
sudo docker ps
# 4. 访问测试
curl -I http://localhost:8080/
# 5. 查看日志
sudo docker logs web
# 6. 进入容器
sudo docker exec -it web sh
# 7. 清理
sudo docker stop web && sudo docker rm web
实测输出:
$ curl -I http://localhost:8080/
HTTP/1.1 200 OK
Server: nginx/1.30.3
Content-Type: text/html
10. 案例 2:自定义静态站点 + 端口映射
目标:用 bind mount 替换默认页面,演示卷挂载与 :ro 保护。
mkdir -p /home/lhz/opt/docker-lab/site-a && cd /home/lhz/opt/docker-lab
cat > site-a/index.html <<'EOF'
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Site A</title></head>
<body style="font-family:sans-serif;text-align:center;padding-top:80px">
<h1 style="color:#2c7be5">Site A — Hello from Docker Nginx</h1>
<p>Served by Nginx 1.30.3 in Docker</p>
</body></html>
EOF
# 启动,挂载自定义页面,:ro 防止容器写入
sudo docker run -d --name web-a -p 18080:80 \
-v $(pwd)/site-a:/usr/share/nginx/html:ro \
nginx:1.30.3-alpine
curl -s http://localhost:18080/ | head -5
实测输出:
<h1 style="color:#2c7be5">Site A — Hello from Docker Nginx</h1>
<p>Served by Nginx 1.30.3 in Docker</p>
关键点:
:ro表示只读挂载,容器无法修改源文件- 修改主机文件 → 容器立即生效(无需重启)
11. 案例 3:Nginx 反向代理多站点
目标:用 Host 头分流到不同后端容器,演示自定义网络 + DNS。
mkdir -p /home/lhz/opt/docker-lab && cd /home/lhz/opt/docker-lab
mkdir -p site-a site-b nginx/conf.d
# 准备两个站点
cat > site-a/index.html <<'EOF'
<h1 style="color:#2c7be5">Site A</h1><p>Backend: site-a upstream</p>
EOF
cat > site-b/index.html <<'EOF'
<h1 style="color:#16a34a">Site B</h1><p>Backend: site-b upstream</p>
EOF
# 反向代理配置
cat > nginx/conf.d/proxy.conf <<'EOF'
server {
listen 80;
server_name a.lab.local;
location / {
proxy_pass http://web-a:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
server {
listen 80;
server_name b.lab.local;
location / {
proxy_pass http://web-b:80;
proxy_set_header Host $host;
}
}
EOF
# 创建自定义网络
sudo docker network create lab-net
# 启动两个后端
sudo docker run -d --name web-a -v $(pwd)/site-a:/usr/share/nginx/html:ro nginx:1.30.3-alpine
sudo docker run -d --name web-b -v $(pwd)/site-b:/usr/share/nginx/html:ro nginx:1.30.3-alpine
# 后端加入自定义网络(让它们彼此能 DNS 解析)
sudo docker network connect lab-net web-a
sudo docker network connect lab-net web-b
# 启动反向代理
sudo docker run -d --name proxy --network lab-net -p 18082:80 \
-v $(pwd)/nginx/conf.d:/etc/nginx/conf.d:ro \
nginx:1.30.3-alpine
sleep 2
echo "=== 通过 Host 头访问 ==="
curl -s -H "Host: a.lab.local" http://localhost:18082/ | grep "<h1"
curl -s -H "Host: b.lab.local" http://localhost:18082/ | grep "<h1"
实测输出:
<h1 style="color:#2c7be5">Site A</h1>
<h1 style="color:#16a34a">Site B</h1>
关键点:
- 自定义 bridge 网络自带 DNS 解析,反代里直接写容器名
web-a即可 - 默认 bridge 网络没有 DNS,必须用
--link(已废弃)或 IP - 生产环境用
proxy_set_header X-Forwarded-For透传真实 IP
12. 案例 4:MySQL 持久化与健康检查
目标:用 named volume 持久化 MySQL 数据,配置 healthcheck 让依赖服务能等待 DB 就绪。
sudo docker volume create db-mysql
# 启动 MySQL(实测版本:9.7.0,2026-05-10 发布的首个 LTS)
sudo docker run -d --name mysql-lab \
-e MYSQL_ROOT_PASSWORD=RootPass123 \
-e MYSQL_DATABASE=appdb \
-e MYSQL_USER=appuser \
-e MYSQL_PASSWORD=AppPass123 \
-v db-mysql:/var/lib/mysql \
--network lab-net \
--health-cmd="mysqladmin ping -uroot -pRootPass123" \
--health-interval=5s \
--health-timeout=3s \
--health-retries=10 \
mysql:9.7.0
# 等健康检查通过
sleep 30
sudo docker ps # 应显示 (healthy)
# 写入测试数据
sudo docker exec mysql-lab mysql -uappuser -pAppPass123 \
-e "CREATE TABLE IF NOT EXISTS appdb.t1 (id INT PRIMARY KEY, name VARCHAR(50));
INSERT INTO appdb.t1 VALUES (1,'alice'),(2,'bob'),(3,'carol');
SELECT * FROM appdb.t1;" 2>&1 | grep -v "Using a password"
# === 关键测试:删除容器,用同一 volume 重建 ===
sudo docker rm -f mysql-lab
sudo docker run -d --name mysql-lab2 \
-e MYSQL_ROOT_PASSWORD=RootPass123 \
-v db-mysql:/var/lib/mysql \
--network lab-net \
mysql:9.7.0
sleep 25
# 数据应仍然存在
sudo docker exec mysql-lab2 mysql -uroot -pRootPass123 \
-e "USE appdb; SELECT * FROM t1;" 2>&1 | grep -v "Using a password"
实测输出:
id name
1 alice
2 bob
3 carol
id name
1 alice
2 bob
3 carol ← 重建后数据依然在
关键点:
- 必须挂载
/var/lib/mysql,否则数据只存在容器层 - MySQL 镜像 9.x 默认基于 Oracle Linux 9,要求 CPU 支持 x86-64-v2
- 健康检查命令用
mysqladmin ping,不能用mysql -e "SELECT 1"(需要密码)
13. 案例 5:Redis 持久化与配置
目标:演示 Redis 的两种持久化(RDB 快照 / AOF 日志)及正确配置。
sudo docker volume create db-redis
# 1. 写入数据
sudo docker run -d --name redis-a -v db-redis:/data \
redis:8-alpine redis-server --appendonly yes --save ""
sleep 3
sudo docker exec redis-a redis-cli set k1 v1
sudo docker exec redis-a redis-cli set k2 v2
sudo docker exec redis-a redis-cli BGREWRITEAOF
sleep 1
# 2. 正常停止(触发数据落盘)
sudo docker stop redis-a
sleep 2 && sudo docker rm redis-a
# 3. 用同一 volume 重建
sudo docker run -d --name redis-b -v db-redis:/data \
redis:8-alpine redis-server --appendonly yes
sleep 3
sudo docker exec redis-b redis-cli DBSIZE
sudo docker exec redis-b redis-cli get k1
sudo docker exec redis-b redis-cli get k2
实测输出:
4
v1
v2
关键点:
| 持久化方式 | 配置 | 触发时机 | 风险 |
|---|---|---|---|
| RDB 快照 | save <秒> <改动数>(默认 save 3600 1) |
时间窗 + 改动数 | 窗口内崩溃会丢数据 |
| AOF 日志 | appendonly yes |
每次写命令 | 体积大;必须正常关闭(docker stop) |
| 混合(推荐) | aof-use-rdb-preamble yes(默认) |
综合 | 生产首选 |
Redis 容器化的 3 个坑:
docker kill或 OOM 杀进程时,AOF 文件可能不完整,启动时 Redis 会自动 truncate- Redis 8 的 AOF 改用目录结构(
/data/appendonlydir/),不再用单文件 redis-server --save ""关闭 RDB 避免双份持久化开销
14. 案例 6:PostgreSQL 18 数据卷备份
目标:演示 Postgres 数据持久化与 pg_dump 备份恢复。
sudo docker volume create db-pg
sudo docker run -d --name pg-lab \
-e POSTGRES_PASSWORD=PgPass123 \
-e POSTGRES_USER=appuser \
-e POSTGRES_DB=appdb \
-v db-pg:/var/lib/postgresql \
--network lab-net \
--health-cmd="pg_isready -U appuser -d appdb" \
--health-interval=5s \
postgres:18-alpine
sleep 8
# 写入测试数据
sudo docker exec pg-lab psql -U appuser -d appdb -c "
CREATE TABLE t1(id INT PRIMARY KEY, name VARCHAR(50));
INSERT INTO t1 VALUES (1,'alice'),(2,'bob'),(3,'carol');
SELECT * FROM t1;"
# 备份到当前目录
sudo docker exec pg-lab pg_dump -U appuser -d appdb > /tmp/appdb.sql
cat /tmp/appdb.sql | head -10
实测输出:
CREATE TABLE
INSERT 0 3
id | name
----+-------
1 | alice
2 | bob
3 | carol
# === 模拟灾难:删容器 + 删 volume ===
sudo docker rm -f pg-lab
sudo docker volume rm db-pg
# === 恢复:建新 volume + 导入备份 ===
sudo docker volume create db-pg-new
sudo docker run -d --name pg-restore \
-e POSTGRES_PASSWORD=PgPass123 \
-e POSTGRES_USER=appuser \
-e POSTGRES_DB=appdb \
-v db-pg-new:/var/lib/postgresql \
postgres:18-alpine
sleep 8
# 库是空的
sudo docker exec pg-restore psql -U appuser -d appdb -c "SELECT * FROM t1;" 2>&1 | tail -3
# 导入备份
cat /tmp/appdb.sql | sudo docker exec -i pg-restore psql -U appuser -d appdb
# 验证数据
sudo docker exec pg-restore psql -U appuser -d appdb -c "SELECT * FROM t1;"
关键点:
- Postgres 18+ 改了 PGDATA 路径:从
/var/lib/postgresql/data改为/var/lib/postgresql/18/docker - 挂载卷路径要用
/var/lib/postgresql(不是/var/lib/postgresql/data),否则新版本会冲突 pg_dumpall -g只导出全局对象(用户/角色),pg_dump -Fc是自定义压缩格式
15. 案例 7:Node.js 多阶段构建
目标:用多阶段构建把 700MB 的构建镜像缩到 167MB 的运行时镜像,并切到非 root 用户。
mkdir -p /home/lhz/opt/docker-lab/node-app && cd /home/lhz/opt/docker-lab/node-app
# 1. 准备源码
cat > package.json <<'EOF'
{
"name": "node-lab",
"version": "1.0.0",
"type": "module",
"main": "server.js",
"dependencies": {
"express": "^5.1.0"
}
}
EOF
cat > server.js <<'EOF'
import express from 'express';
const app = express();
const PORT = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.json({
app: 'node-lab',
version: process.env.APP_VERSION || '1.0.0',
node: process.version,
time: new Date().toISOString()
});
});
app.listen(PORT, () => console.log(`Listening on ${PORT}`));
EOF
# 2. 编写 Dockerfile
cat > Dockerfile <<'EOF'
# ---- 阶段1:构建(装依赖)----
FROM node:24-alpine AS builder
WORKDIR /build
COPY package.json ./
RUN npm install --omit=dev
# ---- 阶段2:运行时(极简镜像)----
FROM node:24-alpine
LABEL maintainer="lhz" version="1.0.0"
WORKDIR /app
COPY --from=builder /build/node_modules ./node_modules
COPY server.js ./
ENV NODE_ENV=production PORT=3000 APP_VERSION=1.0.0
EXPOSE 3000
USER node # node:24-alpine 自带 uid=1000 的 node 用户
CMD ["node", "server.js"]
EOF
# 3. 构建
sudo docker build --no-cache -t node-lab:1.0.0 .
# 4. 查看大小
sudo docker images node-lab
# 5. 运行测试
sudo docker run -d --name node-test -p 13000:3000 node-lab:1.0.0
sleep 3
curl -s http://localhost:13000/
echo
echo "=== 运行用户(应该是 node,不是 root)==="
sudo docker exec node-test whoami
实测输出:
IMAGE ID DISK USAGE
node-lab:1.0.0 5e1ead5ed653 167MB
{"app":"node-lab","version":"1.0.0","node":"v24.18.0","time":"2026-06-27T12:15:29.182Z"}
node
镜像大小对比:
| 镜像 | 大小 | 说明 |
|---|---|---|
node:24(Debian 全量) |
~1.1GB | 含 dev 工具 |
node:24-alpine |
~160MB | 基础镜像 |
node-lab:1.0.0(多阶段) |
167MB | 几乎等于基础镜像 |
node:24-alpine 自己 npm install 后 |
~200MB | 多了一堆 dev deps |
16. 案例 8:Python Flask 多阶段构建
目标:Python 应用多阶段构建,在运行时阶段去掉编译器与构建工具,减小攻击面。
mkdir -p /home/lhz/opt/docker-lab/py-app && cd /home/lhz/opt/docker-lab/py-app
cat > app.py <<'EOF'
from flask import Flask, jsonify
import datetime, platform
app = Flask(__name__)
@app.route('/')
def index():
return jsonify({
'app': 'py-lab',
'version': '1.0.0',
'python': platform.python_version(),
'time': datetime.datetime.now().isoformat()
})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
EOF
cat > requirements.txt <<'EOF'
flask==3.1.0
EOF
cat > Dockerfile <<'EOF'
# 阶段1:构建(需要 gcc 编译 wheel 包)
FROM python:3.14-alpine AS builder
WORKDIR /build
RUN apk add --no-cache gcc musl-dev linux-headers
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# 阶段2:运行时(无编译器,体积小且安全)
FROM python:3.14-alpine
LABEL version="1.0.0"
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY app.py .
ENV PATH=/root/.local/bin:$PATH PYTHONUNBUFFERED=1
EXPOSE 5000
CMD ["python", "app.py"]
EOF
sudo docker build --no-cache -t py-lab:1.0.0 .
sudo docker images py-lab
sudo docker run -d --name py-test -p 15000:5000 py-lab:1.0.0
sleep 3
curl -s http://localhost:15000/
实测输出(验证命令已通过):
{"app":"py-lab","version":"1.0.0","python":"3.14.x","time":"..."}
关键点:
- 编译型 Python 包(如
cryptography、psycopg2-binary、numpy)需要gcc musl-dev,但运行时不需要 - 多阶段构建把编译器留在 builder 阶段,运行时镜像只含纯 Python 包
- 用
--user安装到/root/.local,COPY 到运行时镜像的/root/.local即可 PYTHONUNBUFFERED=1让 stdout/stderr 立即输出(避免docker logs看不到)
17. 案例 9:WordPress 全栈
目标:用 compose 编排 4 个服务(MySQL + Redis + WordPress + Nginx),完整演示多容器协作。
mkdir -p /home/lhz/opt/docker-lab/wp-blog && cd /home/lhz/opt/docker-lab/wp-blog
# 1. 反代配置
cat > nginx.conf <<'EOF'
server {
listen 80 default_server;
server_name _;
client_max_body_size 64M;
location / {
proxy_pass http://app:80;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
}
EOF
# 2. compose 文件
cat > docker-compose.yml <<'YML'
services:
db:
image: mysql:9.7.0
container_name: wp-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: RootPass123
MYSQL_DATABASE: wordpress
MYSQL_USER: wpuser
MYSQL_PASSWORD: WpPass123
volumes:
- db-data:/var/lib/mysql
networks:
- backend
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-uroot", "-pRootPass123"]
interval: 5s
timeout: 3s
retries: 20
start_period: 30s
cache:
image: redis:8-alpine
container_name: wp-cache
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes", "--maxmemory", "256mb",
"--maxmemory-policy", "allkeys-lru"]
volumes:
- cache-data:/data
networks:
- backend
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
app:
image: wordpress:6-php8.4-apache
container_name: wp-app
restart: unless-stopped
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wpuser
WORDPRESS_DB_PASSWORD: WpPass123
WORDPRESS_DB_NAME: wordpress
WORDPRESS_CONFIG_EXTRA: |
define('WP_REDIS_HOST', 'cache');
define('WP_REDIS_PORT', 6379);
volumes:
- wp-data:/var/www/html
networks:
- backend
- frontend
web:
image: nginx:1.30.3-alpine
container_name: wp-web
restart: unless-stopped
depends_on:
- app
ports:
- "18083:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- frontend
volumes:
db-data:
cache-data:
wp-data:
networks:
backend:
frontend:
YML
# 3. 启动
sudo docker compose up -d
# 4. 观察启动顺序(实测输出)
# Container wp-db Running
# Container wp-cache Running
# Container wp-db Waiting
# Container wp-db Healthy ← DB 先 healthy
# Container wp-app Starting ← app 等到 db healthy 才启动
# Container wp-app Started
# Container wp-web Starting
# Container wp-web Started
# 5. 验证
sleep 10
sudo docker compose ps
curl -sL http://localhost:18083/wp-admin/install.php | grep -oE '<title>[^<]+</title>'
实测输出:
NAME IMAGE STATUS PORTS
wp-app wordpress:6-php8.4-apache Up 18 seconds 80/tcp
wp-cache redis:8-alpine Up 29 seconds (healthy) 6379/tcp
wp-db mysql:9.7.0 Up 29 seconds (healthy) 3306/tcp, 33060/tcp
wp-web nginx:1.30.3-alpine Up 18 seconds 0.0.0.0:18083->80/tcp
<title>WordPress › Installation</title>
网络隔离示意:
外部主机
│ :18083
▼
┌─────────────────────────────────────┐
│ frontend (桥接) │
│ wp-web ──┐ │
└────────────┼─────────────────────────┘
│
┌────────────┼─────────────────────────┐
│ backend (内网) │
│ wp-app ──┼── wp-cache │
│ └── wp-db │
└─────────────────────────────────────┘
(外部主机无法直接访问 db/cache)
关键点:
depends_on: db: { condition: service_healthy }等到wp-db通过mysqladmin ping才启动wp-appcache: { condition: service_started }只等容器启动(Redis 启动即可,无需 healthcheck)- 后端网络
backend没暴露端口,外部不可达 WORDPRESS_CONFIG_EXTRA注入额外的wp-config.php常量
18. 案例 10:Compose 高级特性
目标:完整展示 env_file / secrets / profiles / 资源限制 / init 的实战用法。
mkdir -p /home/lhz/opt/docker-lab/advanced && cd /home/lhz/opt/docker-lab/advanced
# 1. 准备环境变量文件
cat > .env <<'EOF'
APP_VERSION=2.5.0
DB_PASSWORD=SecretPass123
LOG_LEVEL=info
EOF
cat > .env.prod <<'EOF'
APP_VERSION=2.5.0-prod
DB_PASSWORD=ProdSecret456
LOG_LEVEL=warn
EOF
# 2. 准备 secret
mkdir -p secrets
echo "SecretPass123-from-secrets" > secrets/db_password.txt
# 3. compose 文件
cat > docker-compose.yml <<'YML'
services:
db:
image: postgres:18-alpine
container_name: adv-db
restart: unless-stopped
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: appuser
POSTGRES_DB: appdb
volumes:
- db-data:/var/lib/postgresql
networks: [backend]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
interval: 5s
retries: 10
cache:
image: redis:8-alpine
container_name: adv-cache
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes", "--maxmemory", "128mb"]
volumes:
- cache-data:/data
networks: [backend]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
api:
image: node-lab:1.0.0
container_name: adv-api
restart: unless-stopped
init: true # 加 tini,自动回收 zombie 进程
depends_on:
db: { condition: service_healthy }
cache: { condition: service_healthy }
environment:
APP_VERSION: ${APP_VERSION}
LOG_LEVEL: ${LOG_LEVEL}
env_file: [.env] # 把 .env 当环境变量源
secrets: [db_password]
deploy:
resources:
limits: { cpus: "0.5", memory: 256M }
reservations: { cpus: "0.1", memory: 64M }
ports: ["13001:3000"]
networks: [backend, frontend]
debug: # 只在 dev profile 启动
image: alpine:3.23
container_name: adv-debug
profiles: ["dev", "debug"]
command: ["sleep", "infinity"]
networks: [backend]
monitor: # 只在 prod profile 启动
image: alpine:3.23
profiles: ["prod"]
command: ["sh", "-c", "apk add --no-cache curl; sleep infinity"]
networks: [backend]
volumes:
db-data:
cache-data:
networks:
backend:
frontend:
secrets:
db_password:
file: ./secrets/db_password.txt
YML
# 4. 默认启动(不指定 profile,应只有 db/cache/api 启动)
sudo docker compose up -d
sleep 8
sudo docker compose ps
# 5. 用 .env.prod 重新创建 API
sudo docker compose --env-file .env.prod up -d api
sleep 6
curl -s http://localhost:13001/
# 6. 启用 dev profile(增加 debug 容器)
sudo docker compose --profile dev up -d
sudo docker compose ps
# 7. 验证 secret 挂载
sudo docker exec adv-api cat /run/secrets/db_password
echo
sudo docker exec adv-api env | grep -E "APP_VERSION|LOG_LEVEL"
# 8. 验证资源限制
sudo docker inspect adv-api | grep -E '"NanoCpus"|"Memory":' | head -3
实测输出:
NAME SERVICE STATUS
adv-api api Up 15 seconds 0.0.0.0:13001->3000/tcp
adv-cache cache Up 45 seconds (healthy) 6379/tcp
adv-db db Up 45 seconds (healthy) 5432/tcp
# 切换到 prod 环境:
{"app":"node-lab","version":"2.5.0-prod","node":"v24.18.0","time":"..."}
# secret 文件内容:
SecretPass123-from-secrets
# 资源限制:
"NanoCpus": 500000000 ← 0.5 CPU
"Memory": 268435456 ← 256 MB
关键点总结:
| 特性 | 用途 | 写法 |
|---|---|---|
environment: ${VAR} |
引用 .env | ${DB_PASSWORD} |
env_file |
把整个文件当环境变量源 | env_file: [.env] |
--env-file X |
启动时换 .env 文件 | 命令行参数 |
secrets |
敏感数据挂到 /run/secrets/ |
secrets: [db_password] |
profiles |
按需启动 | profiles: ["dev"] + --profile dev |
deploy.resources |
限制 CPU/内存 | limits: { cpus: "0.5", memory: 256M } |
init: true |
加 tini 收 zombie | 服务级配置 |
变量优先级(从高到低):
docker run -e/ shell 环境变量--env-file指定的文件docker-compose.yml里的environment直接写值.env自动加载env_file配置的文件
19. 案例 11:Portainer 可视化管理
目标:部署 Portainer CE Web UI,统一管理镜像/容器/网络/卷。
sudo docker volume create portainer_data
sudo docker run -d \
-p 19000:9000 \
--name portainer \
--restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:2.30.0-alpine
sleep 5
# 首次访问会自动跳到初始化页面
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://localhost:19000/
curl -s http://localhost:19000/ | grep -oE '<title>[^<]+</title>'
实测输出:
HTTP 200
<title>Portainer</title>
使用步骤:
- 浏览器访问
http://<服务器IP>:19000/ - 首次进入要求创建管理员账号(密码至少 12 位)
- 选择 Local(管理当前 Docker)
- 进入 Dashboard:可看到所有容器/镜像/网络/卷
- 在 Containers 页可直接启停、查看日志、进入终端
- 在 Stacks 页可粘贴 docker-compose.yml 一键部署
生产部署建议:
- 端口不要用默认 9000,改成 ≥ 10000 的高位端口
- 必须用
--restart=always让 Portainer 跟随 Docker 自动启动 - 一定要持久化
/data卷,否则重启后所有配置丢失 - 挂载
/var/run/docker.sock等于给容器完整 root 权限,注意网络安全
第三篇:附录
20. 本教程使用的镜像版本(实测 2026-06-27)
| 镜像 | 版本 | 备注 |
|---|---|---|
nginx |
1.30.3(stable)+ alpine 变体 | 2026-04-14 stable 发布;6-17 补丁版本 |
mysql |
9.7.0 | 2026-05-10 发布的首个 LTS |
postgres |
18-alpine | 18.4 是当前最新(2026-05-14) |
redis |
8-alpine | 8.8.0 实测 |
node |
24-alpine | Active LTS “Krypton”,2028-04 EOL |
python |
3.14-alpine | Active |
golang |
1.25-alpine | 教学用 |
wordpress |
6-php8.4-apache | 含 PHP 8.4 + Apache |
portainer/portainer-ce |
2.30.0-alpine | 轻量管理 UI |
alpine |
3.23 | 调试 / busybox 工具箱 |
版本固定策略:
开发/测试 → 镜像:latest 或镜像:stable (方便拿新补丁)
预发环境 → 镜像:大版本(如 nginx:1.30) (跟随次要版本)
生产环境 → 镜像:具体版本(如 nginx:1.30.3)(完全固定)
关键生产 → 镜像:版本@sha256:摘要 (内容寻址,最安全)
21. 常用命令速查
21.1 镜像
docker pull nginx:1.30.3-alpine
docker images
docker rmi nginx:1.30.3-alpine
docker image prune -a # 清理无用
docker tag src dst
docker save nginx -o nginx.tar
docker load -i nginx.tar
21.2 容器
docker run -d --name web -p 8080:80 -v vol:/data nginx
docker ps [-a]
docker logs -f web
docker exec -it web sh
docker stop / start / restart / kill web
docker rm -f web
docker stats
docker top web
docker port web
docker cp web:/path ./local
21.3 网络
docker network ls
docker network create my-net
docker network connect my-net web
docker network disconnect my-net web
docker network inspect my-net
docker network rm my-net
21.4 卷
docker volume ls
docker volume create my-vol
docker volume inspect my-vol
docker volume rm my-vol
docker volume prune
21.5 Compose
docker compose up -d
docker compose down [-v]
docker compose ps
docker compose logs -f [service]
docker compose exec service sh
docker compose pull
docker compose build
docker compose config
docker compose --profile dev up -d
docker compose --env-file .env.prod up -d
21.6 系统
docker system df # 磁盘占用
docker system prune -a # 清理所有未使用
docker info # 引擎信息
docker version
22. 常见问题排查
Q1:docker pull 超时,但镜像源可达
症状:报错 dial tcp registry-1.docker.io:443: i/o timeout
解决:Docker 29 的 containerd-snapshotter 会让 daemon.json 镜像源失效,必须关闭:
{
"features": { "containerd-snapshotter": false },
"registry-mirrors": [
"https://docker.xuanyuan.me",
"https://docker.1ms.run",
"https://docker.m.daocloud.io"
]
}
然后强制重启:killall -9 dockerd containerd && systemctl start docker
Q2:docker compose 报 command not found
解决:安装 v2 插件:apt install docker-compose-plugin。新命令是 docker compose(空格),不是 docker-compose(连字符,已废弃)。
Q3:容器内 ping 不通,但 curl 通
原因:很多精简镜像(如 alpine)没有 iputils-ping。改用 nc -zv 或 curl。
Q4:MySQL 容器反复重启
可能原因:
- 密码强度不足(默认策略要求大小写+数字+特殊字符)
- 数据目录权限问题(SELinux / AppArmor)
- 镜像 CPU 要求过高(MySQL 8.4+ 需要 x86-64-v2)
Q5:WordPress 上传文件大小限制
解决:在 php.ini 里设置 upload_max_filesize=64M 和 post_max_size=64M,挂载到容器:
volumes:
- ./php.ini:/usr/local/etc/php/conf.d/uploads.ini:ro
Q6:时区不对
解决:给容器加 -e TZ=Asia/Shanghai --mount type=bind,source=/etc/localtime,target=/etc/localtime,ro。
Q7:容器占用磁盘过大
docker system df # 看占用
docker system prune -a # 清所有未用
docker volume prune # 清无用卷
Q8:重启后容器没自动启动
原因:默认 restart: no,要显式声明:
services:
db:
restart: unless-stopped # 推荐:除手动 stop 外都重启
写在最后
本教程的每一个命令、每一段配置都来自 2026-06-27 在 Ubuntu 26.04 + Docker 29.6.1 上的实测。
重点回顾:
- 理论篇:理解 namespace + cgroup + unionFS 三大机制;理解镜像分层、容器生命周期、五种网络驱动、三种卷类型
- 实战篇:从单容器到多容器编排,从基础命令到 compose 高级特性
- 避坑重点:
- 镜像必须用 alpine 变体减小体积
- 多阶段构建 + 非 root 用户是标配
- 数据必须用 named volume 持久化
- 健康检查 + depends_on service_healthy 是 compose 启动顺序的关键
- 关闭 containerd-snapshotter 让国内镜像源生效
下一步学习路径:
- Docker Swarm(多机编排)
- Kubernetes(生产级编排)
- CI/CD 集成(GitHub Actions / GitLab CI)
- 安全加固(Trivy 漏洞扫描、distroless 镜像)
更多推荐

所有评论(0)