Docker 与 Docker Compose 从理论到实战教程

适用环境:Ubuntu 26.04 LTS(resolute)/ Docker 29.6.1 / Docker Compose v5.2.0

是否实测:所有镜像版本、命令、配置均已实测

覆盖范围:镜像/容器/网络/卷/Dockerfile 理论 + 11 个实战案例(全部跑通)


目录

第一篇:理论基础

  1. Docker 是什么
  2. 核心架构与名词
  3. 镜像(Image)详解
  4. 容器(Container)生命周期
  5. 网络(Network)模型
  6. 卷(Volume)持久化
  7. Dockerfile 指令全景
  8. Docker Compose 概念

第二篇:实战案例

  1. 案例 1:单容器运行 Nginx
  2. 案例 2:自定义静态站点 + 端口映射
  3. 案例 3:Nginx 反向代理多站点
  4. 案例 4:MySQL 持久化与健康检查
  5. 案例 5:Redis 持久化与配置
  6. 案例 6:PostgreSQL 18 数据卷备份
  7. 案例 7:Node.js 多阶段构建
  8. 案例 8:Python Flask 多阶段构建
  9. 案例 9:WordPress 全栈(MySQL + Redis + WP + Nginx)
  10. 案例 10:Compose 高级特性(profile / secrets / 资源限制)
  11. 案例 11:Portainer 可视化管理

第三篇:附录

  1. 本教程使用的镜像版本(实测 2026-06-27)
  2. 常用命令速查
  3. 常见问题排查

第一篇:理论基础

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 个坑

  1. docker kill 或 OOM 杀进程时,AOF 文件可能不完整,启动时 Redis 会自动 truncate
  2. Redis 8 的 AOF 改用目录结构(/data/appendonlydir/),不再用单文件
  3. 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 包(如 cryptographypsycopg2-binarynumpy)需要 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-app
  • cache: { 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 服务级配置

变量优先级(从高到低):

  1. docker run -e / shell 环境变量
  2. --env-file 指定的文件
  3. docker-compose.yml 里的 environment 直接写值
  4. .env 自动加载
  5. 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>

使用步骤

  1. 浏览器访问 http://<服务器IP>:19000/
  2. 首次进入要求创建管理员账号(密码至少 12 位)
  3. 选择 Local(管理当前 Docker)
  4. 进入 Dashboard:可看到所有容器/镜像/网络/卷
  5. Containers 页可直接启停、查看日志、进入终端
  6. 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 composecommand not found

解决:安装 v2 插件:apt install docker-compose-plugin。新命令是 docker compose(空格),不是 docker-compose(连字符,已废弃)。

Q3:容器内 ping 不通,但 curl

原因:很多精简镜像(如 alpine)没有 iputils-ping。改用 nc -zvcurl

Q4:MySQL 容器反复重启

可能原因

  • 密码强度不足(默认策略要求大小写+数字+特殊字符)
  • 数据目录权限问题(SELinux / AppArmor)
  • 镜像 CPU 要求过高(MySQL 8.4+ 需要 x86-64-v2)

Q5:WordPress 上传文件大小限制

解决:在 php.ini 里设置 upload_max_filesize=64Mpost_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 上的实测

重点回顾

  1. 理论篇:理解 namespace + cgroup + unionFS 三大机制;理解镜像分层、容器生命周期、五种网络驱动、三种卷类型
  2. 实战篇:从单容器到多容器编排,从基础命令到 compose 高级特性
  3. 避坑重点
    • 镜像必须用 alpine 变体减小体积
    • 多阶段构建 + 非 root 用户是标配
    • 数据必须用 named volume 持久化
    • 健康检查 + depends_on service_healthy 是 compose 启动顺序的关键
    • 关闭 containerd-snapshotter 让国内镜像源生效

下一步学习路径

  • Docker Swarm(多机编排)
  • Kubernetes(生产级编排)
  • CI/CD 集成(GitHub Actions / GitLab CI)
  • 安全加固(Trivy 漏洞扫描、distroless 镜像)

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐