Featured image of post Docker 入门与实战:从容器到 Compose

Docker 入门与实战:从容器到 Compose

系统介绍 Docker 的核心概念、常用命令、Dockerfile、数据卷、网络与 Docker Compose,并通过完整示例掌握容器化应用的基本工作流

引言

开发一个应用时,经常会遇到这样的情况:代码在自己的电脑上可以运行,换到另一台机器后,可能因为操作系统、运行时版本、系统库或配置不同而启动失败。

Docker 解决的核心问题,就是把应用和它依赖的运行环境一起打包,并用统一的方式完成构建、分发和运行。

1
2
3
4
5
源代码 + 依赖 + 运行时 + 配置约定
                ↓ docker build
              Docker 镜像
                ↓ docker run
              Docker 容器

本文从 Docker 的基本概念开始,依次介绍常用命令、Dockerfile、数据卷、网络和 Docker Compose,最后把这些能力串成一套完整的容器化工作流。

Docker 是什么

Docker 是一套用于构建(build)、分发(ship)和运行(run)容器化应用的工具与平台。

在 Linux 上,容器主要利用多种内核能力实现隔离和限制:

  • namespaces 隔离进程、网络、挂载点、主机名等资源视图。
  • cgroups 限制和统计 CPU、内存、进程数等资源。
  • 分层文件系统复用镜像层,减少重复存储和传输。
  • Linux capabilities、seccomp 等机制缩小容器进程的权限范围。

容器并不是一台缩小版虚拟机。Linux 容器与宿主机共享内核,隔离边界通常弱于拥有独立内核的虚拟机。在 macOS 和 Windows 上使用 Docker Desktop 时,Linux 容器一般运行在 Docker Desktop 管理的 Linux 虚拟机中。

Docker 的核心对象

对象 作用 可以类比为
Dockerfile 声明镜像如何构建 构建配方
镜像(image) 包含应用、运行时和文件系统的只读模板 安装包
容器(container) 镜像的一次运行实例,并拥有独立可写层 运行中的进程
镜像仓库(registry) 存储和分发镜像 软件制品仓库
数据卷(volume) 独立于容器生命周期保存数据 持久化磁盘
网络(network) 连接容器并提供隔离和服务发现 虚拟局域网

同一个镜像可以启动多个彼此隔离的容器。删除容器会删除该容器的可写层,但不会删除镜像,也不会自动删除显式创建的命名数据卷。

Docker 的工作架构

1
2
3
4
5
6
7
8
9
Docker CLI
    │  Docker API
Docker daemon(守护进程)── pull / push ── Registry
    ├── 管理镜像、容器、网络和数据卷
containerd → OCI runtime → Linux kernel

日常输入的 docker 命令由 Docker CLI 发出,Docker 守护进程(daemon)接收请求并管理对象。创建容器时,Docker Engine 会继续调用底层容器运行时启动隔离进程。

容器与虚拟机的区别

维度 容器 虚拟机
内核 与宿主机或宿主 VM 共享内核 每台虚拟机拥有独立内核
启动速度 通常较快 通常较慢
制品体积 通常为 MB 到数 GB 通常为数 GB 以上
隔离边界 进程级隔离 硬件虚拟化隔离
适用场景 应用交付、微服务、CI/CD 强隔离、不同内核、完整操作系统

Docker 镜像也不是在所有机器上都能无条件运行。操作系统类型、CPU 架构和内核能力仍需兼容,例如 linux/amd64 镜像不能在不具备兼容或模拟能力的 linux/arm64 环境中直接运行。

准备环境

Linux 可以安装 Docker Engine,macOS 和 Windows 通常使用 Docker Desktop。本文使用 Compose v2 的现代命令 docker compose,不再使用旧的 docker-compose 独立命令。

安装后先检查客户端、服务端和 Compose:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 查看客户端与服务端版本
docker version

# 查看存储驱动、运行时、资源等信息
docker info

# 查看 Compose 版本
docker compose version

# 运行官方测试镜像,结束后自动删除容器
docker run --rm hello-world

docker version 只显示客户端信息或提示无法连接 daemon 时,需要先启动 Docker 服务或 Docker Desktop。

Docker 常用命令

Docker 命令大多采用 docker <对象> <操作> 的结构,例如 docker image lsdocker container rm。Docker 同时保留了 docker imagesdocker ps 等传统短命令,日常使用中两种写法都很常见。

常用短命令 对象式命令 作用
docker images docker image ls 查看本地镜像
docker ps docker container ls 查看运行中的容器
docker ps -a docker container ls -a 查看全部容器
docker rmi IMAGE docker image rm IMAGE 删除镜像引用
docker rm CONTAINER docker container rm CONTAINER 删除已停止的容器

镜像管理

下面的 registry.example.com/team 是私有仓库地址占位值,登录和推送前需要替换为自己的仓库与命名空间。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 在 Docker Hub 搜索官方镜像
docker search nginx --filter is-official=true --limit 10

# 拉取镜像;标签省略时默认使用 latest
docker pull nginx:alpine

# 查看本地镜像
docker images

# 查看镜像详细信息
docker image inspect nginx:alpine

# 查看镜像层历史
docker image history nginx:alpine

# 为镜像添加新标签
docker tag nginx:alpine registry.example.com/team/nginx:1.0

# 推送镜像到仓库
docker push registry.example.com/team/nginx:1.0

# 删除本地镜像标签或引用
docker image rm nginx:alpine

标签是镜像的可读引用。latest 只是一个普通且可变的标签,不保证它一定是最新或最稳定版本。生产环境应固定经过测试的版本标签;需要严格复现时,可以进一步固定镜像 digest,同时建立主动升级和漏洞修复流程。

docker search 只搜索 Docker Hub 仓库,不会列出私有仓库内容或完整标签。选镜像时仍需核对发布者、具体标签、支持架构和更新时间。

镜像离线保存与载入

无法直接访问镜像仓库时,可以把镜像及其配置、标签和镜像层保存为归档文件:

1
2
3
4
5
# 将镜像保存为 tar 归档
docker save --output nginx-alpine.tar nginx:alpine

# 从归档载入镜像
docker load --input nginx-alpine.tar

docker save/load 不包含容器可写层和数据卷。docker export/import 导出的是容器文件系统,会丢失镜像层、构建历史和部分配置,不适合作为常规镜像迁移方案。

创建并运行容器

下面启动一个 Nginx 容器,并只允许从宿主机本地访问:

1
2
3
4
5
docker run \
  --name web \
  --detach \
  --publish 127.0.0.1:8080:80 \
  nginx:alpine

访问 http://127.0.0.1:8080 时,请求会从宿主机的 8080 端口转发到容器的 80 端口。

参数 简写 作用
--name web 指定容器名称
--detach -d 在后台运行
--publish 8080:80 -p 映射宿主机端口到容器端口
--env KEY=value -e 设置容器环境变量
--interactive --tty -it 保持标准输入并分配终端
--rm 容器退出后自动删除
--network NAME 连接指定网络
--mount ... 挂载数据卷、目录或 tmpfs

镜像名之后的内容会被当作容器内要执行的命令,因此 Docker 自身的选项必须放在镜像名之前。下面两行只用于对比参数位置,错误示例不要执行:

1
2
3
4
5
# 正确:-p 是 docker run 的参数
docker run --rm -p 127.0.0.1:8081:80 nginx:alpine

# 错误:-p 会被当作传给 nginx 镜像入口的参数
docker run nginx:alpine -p 127.0.0.1:8081:80

查看与管理容器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 只查看运行中的容器
docker ps

# 查看全部容器,包括已经退出的容器
docker ps -a

# 停止、启动和重启容器
docker stop web
docker start web
docker restart web

# 重命名容器
docker rename web web-server

# 删除已停止的容器
docker stop web-server
docker container rm web-server

docker run 会基于镜像创建一个新容器,而 docker start 只是重新启动已经存在的容器。docker pull 也不会自动更新正在运行的容器;更新镜像后,需要用新镜像重新创建容器。

日志与排障

下面用 CONTAINER 表示实际的容器名称或 ID,执行时需要替换。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 持续查看最后 100 行日志
docker logs --follow --tail 100 CONTAINER

# 在运行中的容器内执行命令
docker exec -it CONTAINER sh

# 查看容器的完整配置和状态
docker inspect CONTAINER

# 实时查看资源占用
docker stats

# 查看容器内运行的进程
docker top CONTAINER

# 在宿主机和容器之间复制文件
docker cp ./nginx.conf CONTAINER:/etc/nginx/nginx.conf
docker cp CONTAINER:/var/log/nginx ./nginx-logs

精简镜像不一定包含 Bash,所以排障时通常先尝试 sh。使用 scratch 或 distroless 构建的镜像甚至可能完全没有 shell,此时应通过日志、指标、docker inspect 或专门的调试容器排查。

按需使用的容器命令

命令 作用 注意事项
docker pause CONTAINER / docker unpause CONTAINER 暂停或恢复容器内全部进程 暂停不是优雅停机,连接可能超时
docker wait CONTAINER 等待容器停止并输出退出码 容器运行期间会阻塞当前终端
docker diff CONTAINER 查看容器可写层中变化的路径 不包含数据卷中的变化
docker events --since 10m 实时查看 Docker 对象事件 持续运行,使用 Ctrl+C 退出
docker kill CONTAINER 立即向容器主进程发送信号 默认发送 SIGKILL,优先使用 docker stop

docker create 只创建容器但不启动,docker run 相当于创建后立即启动。docker attach 连接容器主进程的标准输入输出,可能把 Ctrl+C 等信号传给主进程,普通排障优先使用 docker logsdocker execdocker commit 会把人工修改固化成难以复现的镜像,应优先维护 Dockerfile;容器文件系统迁移也优先使用镜像的 save/load,而不是会丢失镜像历史与配置的 export/import

资源限制

容器默认可能使用宿主机的大部分资源。运行不可信或容易突发占用的服务时,应设置边界。下面的 example/app:1.0 是占位镜像名,使用时需要替换:

1
2
3
4
5
6
docker run --detach \
  --name limited-app \
  --memory 512m \
  --cpus 1.0 \
  --pids-limit 200 \
  example/app:1.0

具体限制是否生效取决于宿主系统和 Docker 环境,可以通过 docker inspect limited-appdocker stats limited-app 验证。

运行中的容器也可以调整部分资源限制:

1
docker update --memory 768m --cpus 1.5 limited-app

Compose 管理的容器应优先修改 compose.yaml 后重新创建,避免运行状态与声明配置不一致。

查看占用与清理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 查看 Docker 对象占用的磁盘空间
docker system df

# 删除所有已停止的容器
docker container prune

# 删除悬空镜像
docker image prune

# 删除未被容器使用的网络
docker network prune

# 删除未被容器使用的匿名数据卷
docker volume prune

如需把未使用的命名数据卷也纳入清理,使用 docker volume prune --all。清理命令会要求确认,但仍应先检查删除列表。下面的命令删除范围很大,可能同时清除未使用镜像、构建缓存和匿名数据卷,不应作为日常无脑清理命令:

1
docker system prune --all --volumes

Dockerfile

Dockerfile 是构建镜像的声明式文件。Docker 从上到下执行指令,并将文件变更组织为可复用的镜像层。

构建上下文

执行下面的命令时,最后的 . 表示构建上下文:

1
docker build --tag docker-demo:1.0 .

Dockerfile 中的 COPY 只能读取构建上下文内的文件,不能读取它的父目录。构建上下文过大会拖慢构建,还可能把 Git 历史、日志、密钥或本地依赖发送给 builder,因此应配合 .dockerignore 使用。

常用指令

指令 作用 示例
FROM 指定基础镜像或构建阶段 FROM golang:1.25-alpine AS build
WORKDIR 设置后续指令的工作目录 WORKDIR /src
COPY 从构建上下文复制文件 COPY go.mod ./
RUN 构建镜像时执行命令 RUN go build -o /out/server .
ARG 声明构建参数 ARG VERSION=dev
ENV 设置镜像和容器环境变量 ENV APP_ENV=production
EXPOSE 声明应用监听的容器端口 EXPOSE 8080
USER 指定后续指令和容器进程用户 USER app
ENTRYPOINT 设置固定的容器入口程序 ENTRYPOINT ["./server"]
CMD 设置默认命令或默认参数 CMD ["--port", "8080"]

EXPOSE 只记录镜像元数据,不会自动开放宿主机端口。真正发布端口仍要使用 docker run -p 或 Compose 的 ports

一个完整的 Go 示例

目录结构如下:

1
2
3
4
5
docker-demo/
├── .dockerignore
├── Dockerfile
├── go.mod
└── main.go

go.mod

1
2
3
module example.com/docker-demo

go 1.25

main.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
)

func main() {
	hostname, err := os.Hostname()
	if err != nil {
		hostname = "unknown"
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
		fmt.Fprintf(w, "hello from %s\n", hostname)
	})
	mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
		fmt.Fprintln(w, "ok")
	})

	log.Println("listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", mux))
}

Dockerfile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# syntax=docker/dockerfile:1

FROM golang:1.25-alpine AS build

WORKDIR /src
COPY go.mod ./
RUN go mod download
COPY main.go ./
RUN CGO_ENABLED=0 GOOS=linux go build \
    -trimpath \
    -ldflags="-s -w" \
    -o /out/server .

FROM alpine:3.22 AS runtime

RUN addgroup -S app && adduser -S -G app app
WORKDIR /app
COPY --from=build --chown=app:app /out/server ./server

USER app
EXPOSE 8080
ENTRYPOINT ["./server"]

.dockerignore

1
2
3
4
5
6
7
8
9
.git
.gitignore
.env
*.log
tmp/
bin/
Dockerfile*
compose*.yaml
README.md

构建并运行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 构建镜像,并检查是否有更新的基础镜像
docker build --pull --tag docker-demo:1.0 .

# 后台运行,只将端口开放给本机
docker run --detach \
  --rm \
  --name docker-demo \
  --publish 127.0.0.1:8080:8080 \
  docker-demo:1.0

curl http://127.0.0.1:8080/
curl http://127.0.0.1:8080/health

# 停止后,因为使用了 --rm,容器会被自动删除
docker stop docker-demo

为什么使用多阶段构建

上面的 Dockerfile 包含两个阶段:

1
2
3
build 阶段:编译器 + 源代码 → 可执行文件
                         ↓ COPY --from=build
runtime 阶段:最小运行环境 + 可执行文件

最终镜像不会包含 Go 编译器、模块缓存和源代码,因此镜像更小,攻击面也更少。多阶段构建同样适用于前端构建、Java 打包和原生程序编译。

镜像层与构建缓存

Docker 会复用没有变化的构建层。通常应先复制变化较少的依赖清单并安装依赖,再复制频繁变化的源代码:

1
2
3
4
5
6
7
# 依赖清单不变时,可以复用下载依赖的缓存层
COPY go.mod ./
RUN go mod download

# 当前示例只有 main.go,源码变化只会让这里之后的层失效
COPY main.go ./
RUN go build -o /out/server .

BuildKit 还支持缓存挂载。例如 Go 模块与编译缓存可以写成:

1
2
3
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go build -o /out/server .

这些缓存用于加速后续构建,不会自动进入最终镜像。

Dockerfile 最佳实践

  1. 使用可信、尽量精简的基础镜像,并定期更新和扫描漏洞。
  2. 最终阶段使用非 root 用户,只复制运行所需的文件。
  3. 使用 JSON exec form 的 ENTRYPOINTCMD,让信号直接传给应用进程。
  4. 使用 .dockerignore 缩小构建上下文,避免把密钥和无关文件送入构建器。
  5. 不要用 ARGENV 或“复制后删除”的方式保存密钥;密钥可能留在镜像配置或历史层中。

需要在构建过程中临时使用私有仓库凭据时,可以使用 BuildKit secret mount。下面以私有 Go 模块所需的 .netrc 为例:

1
2
RUN --mount=type=secret,id=netrc,target=/root/.netrc,required=true \
    go mod download
1
docker build --secret id=netrc,src="$HOME/.netrc" --tag app:1.0 .

数据卷

容器的可写层适合保存临时运行状态,但容器被删除时,这一层也会被删除。数据库文件、上传文件和需要跨容器复用的数据应放在容器可写层之外。

Docker 主要提供三种挂载方式:

类型 数据位置 生命周期 适用场景
命名数据卷(named volume) Docker 管理 独立于容器 数据库、应用持久化数据
绑定挂载(bind mount) 指定宿主机路径 由宿主机文件决定 源代码、配置文件、开发环境
tmpfs 宿主机内存 容器停止后消失 临时文件、敏感的短期数据

命名数据卷

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 创建和查看数据卷
docker volume create demo-data
docker volume ls
docker volume inspect demo-data

# 写入数据;--rm 不会删除显式命名的数据卷
docker run --rm \
  --mount type=volume,src=demo-data,dst=/data \
  alpine:3.22 sh -c 'date > /data/created-at && cat /data/created-at'

# 在另一个容器中读取同一份数据
docker run --rm \
  --mount type=volume,src=demo-data,dst=/data,readonly \
  alpine:3.22 cat /data/created-at

绑定挂载

绑定挂载把宿主机文件或目录直接映射到容器中:

1
2
3
docker run --rm \
  --mount type=bind,src="$(pwd)",dst=/workspace,readonly \
  alpine:3.22 ls -la /workspace

绑定挂载适合开发时同步源码,也适合以只读方式提供配置。它依赖宿主机路径和权限,可移植性弱于命名数据卷。在 Docker Desktop 中,宿主机目录还需要处于允许共享的路径范围内。

--mount 语义明确,适合脚本和文档。使用 -v 宿主路径:容器路径 时,如果宿主路径拼错,Docker 可能自动创建一个目录,从而掩盖错误。

tmpfs

1
2
3
docker run --rm \
  --mount type=tmpfs,dst=/tmp,tmpfs-size=64m \
  alpine:3.22 sh -c 'echo temporary > /tmp/value && cat /tmp/value'

tmpfs 数据不会写入容器可写层,容器停止后即消失。它适合缓存、临时文件和不需要持久化的短期敏感数据,但仍可能受到宿主机交换空间等系统配置影响。

挂载时的注意事项

  • 挂载会遮蔽容器目标目录中原有的文件,卸载后原文件才会重新可见。
  • 绑定挂载需要处理宿主机与容器之间的 UID、GID 和文件权限。
  • 配置、证书等只读内容应使用 readonly:ro,减少误写风险。
  • 命名数据卷不是备份;误删除、应用错误和数据损坏仍会影响卷内数据。
  • docker volume prunedocker compose down -v 都可能永久删除数据,执行前应确认备份。

备份与恢复数据卷

下面先暂停应用写入,再创建专用备份目录并执行备份,避免给临时容器整个项目目录的写权限。示例使用固定文件名,重复执行会覆盖原文件;正式备份应使用带时间戳的唯一文件名:

1
2
3
4
5
6
mkdir -p backup

docker run --rm \
  --mount type=volume,src=demo-data,dst=/data,readonly \
  --mount type=bind,src="$(pwd)/backup",dst=/backup \
  alpine:3.22 tar -czf /backup/demo-data.tar.gz -C /data .

恢复时先创建一个新卷,避免覆盖原数据:

1
2
3
4
5
6
docker volume create demo-data-restored

docker run --rm \
  --mount type=volume,src=demo-data-restored,dst=/data \
  --mount type=bind,src="$(pwd)/backup",dst=/backup,readonly \
  alpine:3.22 tar -xzf /backup/demo-data.tar.gz -C /data

恢复后应启动临时容器或应用执行完整性校验。数据库通常还应优先使用数据库自身的逻辑备份工具,例如 pg_dump,而不是只复制正在写入的数据文件。

完成备份和恢复验证后,再清理实验数据卷:

1
docker volume rm demo-data demo-data-restored

Docker 网络

容器拥有独立的网络命名空间。Docker 网络负责连接容器、分配地址、提供 DNS 服务发现,并限定容器之间以及容器与外部网络之间的通信范围。

常见网络驱动

驱动 特点 常见用途
bridge 单台 Docker 主机上的虚拟网络 本地服务、单机部署
host 容器直接使用宿主机网络栈 特定 Linux 性能或端口场景
none 只保留回环接口 完全不需要网络的任务
overlay 跨多台 Docker 主机连接服务 Docker Swarm
macvlan 为容器分配局域网中的独立 MAC/IP 需要直接接入物理网络的场景

日常单机使用时,优先创建用户自定义桥接网络(user-defined bridge network),而不是把所有容器都放进默认 bridge。用户自定义网络具有更清晰的隔离边界,并提供基于容器名和网络别名的 DNS 解析。它不会自动提供服务认证、细粒度授权或流量加密。

创建自定义网络

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 创建和查看网络
docker network create demo-net
docker network ls
docker network inspect demo-net

# Redis 容器加入 demo-net
docker run --detach \
  --name redis \
  --network demo-net \
  redis:7-alpine

# 临时客户端通过容器名 redis 访问服务
docker run --rm \
  --network demo-net \
  redis:7-alpine \
  redis-cli -h redis ping

如果返回 PONG,说明 Docker DNS 已将名称 redis 解析到对应容器。对于另一个正在运行的容器,可以将下面的 CONTAINER 替换为它的名称,动态接入或断开网络:

1
2
docker network connect demo-net CONTAINER
docker network disconnect demo-net CONTAINER

完成实验后清理:

1
2
3
docker stop redis
docker rm redis
docker network rm demo-net

端口映射

同一 Docker 网络中的容器可以直接访问对方的容器端口,不需要发布到宿主机。只有宿主机或外部客户端需要访问服务时,才使用 -p

1
2
3
4
5
6
7
浏览器
  │ 127.0.0.1:8080
宿主机端口 8080
  │ Docker 端口转发
web 容器端口 80 ───── 容器网络 ───── db:5432

下面两种发布方式按访问范围二选一。命令以前台方式运行,按 Ctrl+C 停止后,--rm 会删除实验容器:

1
2
3
4
5
# 默认通常绑定宿主机所有接口,局域网也可能访问
docker run --rm -p 8080:80 nginx:alpine

# 只允许宿主机本地访问,更适合开发环境
docker run --rm -p 127.0.0.1:8080:80 nginx:alpine

排查已有容器的端口映射时,可以直接查询全部映射或指定容器端口:

1
2
docker port CONTAINER
docker port CONTAINER 80/tcp

应用进程通常需要监听容器内的 0.0.0.0,才能通过端口映射访问。如果只监听容器内的 127.0.0.1,外部请求通常无法进入该进程。

localhost 与服务发现

容器内的 localhost 只指向当前容器本身。应用容器访问数据库容器时,不应连接 localhost:5432,而应使用同一网络中的服务名:

1
2
错误:DB_HOST=localhost
正确:DB_HOST=db

容器访问宿主机服务时,Docker Desktop 通常可以使用 host.docker.internal。Linux Docker Engine 常常需要显式添加映射。下面的 example/app:1.0 需要替换为实际镜像:

1
2
3
docker run \
  --add-host host.docker.internal:host-gateway \
  example/app:1.0

--network host 的行为具有平台差异,不应作为通用跨平台方案。

Docker Compose

手动管理一个容器时,docker run 足够直接。但一个应用通常包含 Web 服务、数据库、缓存、消息队列等多个容器。如果每次都手写网络、数据卷、环境变量和启动顺序,命令会迅速变得难以维护。

Docker Compose 使用一个 compose.yaml 文件声明多容器应用:

1
2
3
4
5
6
compose.yaml
    ├── services:要运行的服务
    ├── networks:服务使用的网络
    ├── volumes:需要持久化的数据卷
    ├── configs:非敏感配置
    └── secrets:敏感文件的挂载声明

现代 Compose 遵循 Compose Specification,顶层不再需要 version: "3"

完整 Compose 示例

沿用前面的 Go 应用,再添加一个 Caddy 反向代理。目录结构如下:

1
2
3
4
5
6
7
docker-demo/
├── .dockerignore
├── Caddyfile
├── Dockerfile
├── compose.yaml
├── go.mod
└── main.go

Caddyfile

1
2
3
:80 {
    reverse_proxy app:8080
}

compose.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
name: docker-demo

services:
  app:
    build:
      context: .
      target: runtime
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/health"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 5s
    networks:
      - app-net

  proxy:
    image: caddy:2.9-alpine
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - type: bind
        source: ./Caddyfile
        target: /etc/caddy/Caddyfile
        read_only: true
      - caddy-data:/data
      - caddy-config:/config
    depends_on:
      app:
        condition: service_healthy
    networks:
      - app-net

volumes:
  caddy-data:
  caddy-config:

networks:
  app-net:
    driver: bridge

这个配置体现了前面几节的核心概念:

  • app 使用当前目录的 Dockerfile 构建镜像。
  • proxy 通过 Compose DNS 使用 app:8080 访问应用。
  • 只有 proxy 向宿主机发布端口,app 只在 app-net 中被其他服务访问。
  • Caddyfile 通过只读绑定挂载提供,Caddy 状态保存在命名数据卷中。
  • proxy 等待 app 健康检查通过后再启动。

depends_on 和健康检查可以改善启动顺序,但不能替代应用自身的重试与故障恢复。依赖服务在运行中重启时,应用仍应能够重新连接。

Compose 常用命令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 展开变量、合并配置并验证语法
docker compose config

# 拉取使用 image 的服务,跳过由 Dockerfile 构建的服务
docker compose pull --ignore-buildable

# 只构建服务镜像,不启动容器
docker compose build

# 构建镜像并在后台启动全部服务
docker compose up --detach --build

# 查看服务与容器状态
docker compose ps

# 查看当前项目的服务镜像
docker compose images

# 持续查看全部服务日志
docker compose logs --follow --tail 100

# 只查看 app 服务日志
docker compose logs --follow app

# 在现有 app 容器内执行命令
docker compose exec app sh

# 创建一个一次性服务容器,并覆盖默认入口执行检查命令
docker compose run --rm --entrypoint sh app -c 'id'

# 停止服务,但保留容器
docker compose stop

# 启动已经停止的服务
docker compose start

# 只重启 app 服务,不应用配置文件变更
docker compose restart app

# 停止并删除容器与项目网络,默认保留命名数据卷
docker compose down

启动后访问:

1
2
curl http://127.0.0.1:8080/
curl http://127.0.0.1:8080/health

修改 compose.yaml、环境变量或镜像后,docker compose restart 不会应用这些变化。应使用 docker compose up -d 让 Compose 按需重新创建容器;Dockerfile 或源码变化时再加 --build

删除服务和命名数据卷时需要显式使用 -v/--volumes。这会删除 Compose 项目声明的非 external 命名数据卷和匿名卷,应在确认备份后执行:

1
docker compose down --volumes

环境变量

Compose 支持从 shell 或项目目录中的 .env 文件读取插值变量:

1
2
APP_PORT=8080
APP_IMAGE_TAG=1.0
1
2
3
4
5
services:
  app:
    image: example/app:${APP_IMAGE_TAG:-latest}
    ports:
      - "127.0.0.1:${APP_PORT:-8080}:8080"

${VAR} 是 Compose 在解析 YAML 时进行的变量插值,而 environmentenv_file 用于设置容器内部的环境变量,这几个概念不要混淆。

密码和令牌不应提交到 .env。本地开发可以使用不进 Git 的独立密钥文件,并通过 Compose secrets 以文件形式挂载:

1
2
3
4
5
6
7
8
services:
  app:
    secrets:
      - db-password

secrets:
  db-password:
    file: ./secrets/db-password.txt

应用可从 /run/secrets/db-password 读取内容。Compose secrets 改善了传递方式,但本地文件本身仍是明文,生产环境还需要专门的密钥管理系统和访问控制。

Compose 使用建议

  1. 文件优先命名为 compose.yaml,并提交一份不含真实密钥的默认配置。
  2. 不要无必要设置 container_name,否则容易产生项目间冲突,也会妨碍同一服务扩容。
  3. 服务之间使用服务名和容器端口通信,只有外部需要访问的服务才配置 ports
  4. 配置健康检查,同时让应用对数据库、缓存等依赖实现超时和重试。
  5. 提交前运行 docker compose config,检查变量展开、端口和挂载路径是否符合预期。

一套完整的日常工作流

从修改代码到停止环境,可以按照下面的顺序操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 1. 检查最终 Compose 配置
docker compose config

# 2. 构建、启动并等待服务进入运行或健康状态
docker compose up --detach --build --wait

# 3. 查看状态和最近 100 行日志
docker compose ps
docker compose logs --tail 100

# 4. 测试服务
curl http://127.0.0.1:8080/health

# 5. 停止并删除容器,保留命名数据卷
docker compose down

发布镜像时,再增加版本标签和镜像仓库步骤。下面的 registry.example.com、团队名和凭据变量都是占位值,需要替换为自己的仓库信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 构建带版本的镜像
docker build --tag registry.example.com/team/docker-demo:1.0.0 .

# 登录时通过标准输入传递令牌,避免把密码直接写进命令参数
printf '%s' "$REGISTRY_TOKEN" | \
  docker login registry.example.com \
  --username "$REGISTRY_USER" \
  --password-stdin

# 推送不可混淆的版本标签
docker push registry.example.com/team/docker-demo:1.0.0

# 共享设备使用结束后退出仓库
docker logout registry.example.com

多架构发布可以使用 Buildx:

1
2
3
4
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag registry.example.com/team/docker-demo:1.0.0 \
  --push .

执行多架构构建前,可以用 docker buildx inspect --bootstrap 检查 builder 支持的平台。包含跨架构 RUN 的 Dockerfile 需要对应架构的原生 worker,或预先配置 binfmt/QEMU 模拟;Docker Desktop 通常已经配置,普通 Linux Engine 不一定具备。使用非默认 builder 时,单平台本地构建通常需要 --load 才会把结果加载到本地镜像列表;多平台结果一般直接使用 --push 发布到仓库。

常见问题

容器启动后立即退出

容器的生命周期由主进程决定,主进程退出后容器就会停止。先查看状态、退出码和日志:

1
2
3
docker ps -a
docker inspect --format '{{.State.ExitCode}}' CONTAINER
docker logs CONTAINER

不要通过在后台运行一个无意义进程来“保活”,应修复入口命令、配置或应用本身。

端口映射后仍无法访问

依次检查:

  1. 容器是否仍在运行,应用日志是否报错。
  2. -p 的顺序是否为“宿主机端口:容器端口”。
  3. 应用是否监听容器内的 0.0.0.0,而不是只监听 127.0.0.1
  4. Docker Desktop 的虚拟机、宿主防火墙或云安全组是否允许访问。
  5. 服务是否实际上监听了预期的容器端口。

容器之间无法连接

先确认两个容器位于同一用户自定义网络,再使用容器名、网络别名或 Compose 服务名连接。容器内的 localhost 不能指向另一个容器。

1
2
docker network inspect NETWORK
docker inspect CONTAINER

修改代码后镜像没有变化

检查构建上下文、.dockerignore 和 Dockerfile 的 COPY 路径,然后重新构建:

1
docker build --no-cache --tag docker-demo:test .

--no-cache 适合定位缓存问题,不必用于每次日常构建。Compose 环境中使用 docker compose up -d --build 更新镜像并重新创建需要变化的服务。

磁盘空间持续增长

先观察占用来源,再按对象类型清理:

1
2
3
4
docker system df --verbose
docker builder prune
docker image prune
docker container prune

不要直接跳到 docker system prune -a --volumes。匿名数据卷、未运行环境的镜像和构建缓存都可能仍有价值;未使用的命名数据卷只有在单独执行 docker volume prune --all 等命令时才会被纳入清理。

安全边界

容器带来隔离,但不会自动让应用安全。至少应遵守下面几条原则:

  1. 运行时使用非 root 用户,只授予应用真正需要的 Linux capabilities。
  2. 不把 --privileged、宿主机根目录或 /var/run/docker.sock 挂载当作普通方案。
  3. 只发布必要端口,开发数据库优先绑定到宿主机 127.0.0.1
  4. 不在 Dockerfile、镜像层、Git、命令历史或日志中保存密码和令牌。
  5. 使用可信基础镜像,持续更新依赖、扫描漏洞,并设置 CPU、内存和进程数限制。

在 Linux 上,能访问 Docker 守护进程的用户通常可以获得接近宿主机 root 的能力,因此 docker 用户组和 Docker socket 都应按高权限入口管理。

小结

Docker 的核心不是记住大量命令,而是理解几个对象之间的关系:

1
2
3
4
5
6
Dockerfile ──构建──> 镜像 ──运行──> 容器
                    ┌─────────────┴─────────────┐
                    ▼                           ▼
                 数据卷                        网络
                    └──────── Docker Compose ───┘
  • 镜像负责交付一致的应用和运行环境,容器负责运行镜像。
  • Dockerfile 把手工安装过程变成可审查、可重复的构建过程。
  • 数据卷让重要数据脱离容器的可写层,网络让服务在指定范围内互相发现和通信。
  • Docker Compose 把多个服务、网络、数据卷和配置组织成一个可复现的应用环境。
  • 生产环境还需要持续处理镜像更新、密钥管理、资源限制、日志监控、数据备份和安全加固。
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计