从下面的 Nginx 实验开始:启动容器,访问页面,再停止并删除它。完成一次运行之后,再看镜像、数据卷和网络各自解决什么问题。
本文使用 Linux 容器、现代 docker compose 命令和 Bash/Zsh 语法。macOS、Windows 通常使用 Docker Desktop,Linux 可以安装 Docker Engine 与 Compose 插件;Windows 用户可在 WSL 中执行示例。示例假设 CLI 连接本机 Docker,首次拉取镜像需要网络。
| 阶段 | 要解决的问题 | 完成标志 |
|---|---|---|
| 1. 运行容器 | 怎样启动一个现成的服务? | 浏览器显示 Nginx 欢迎页 |
| 2. 理解对象 | 镜像、容器和宿主机是什么关系? | 分清创建、停止与删除 |
| 3. 构建镜像 | 怎样交付自己的应用? | Go 服务返回主机名和健康状态 |
| 4. 管理数据与网络 | 数据放哪里,服务怎么互相访问? | 新容器读到旧数据,通过名称访问应用 |
| 5. 使用 Compose | 怎样用一份配置启动多个服务? | 请求经 Caddy 转发到 Go 服务 |
顺序阅读可完成整套实验;日常使用时可直接跳到命令速查或常见问题。
先运行一个容器
检查环境
安装入口见 Docker 官方安装文档。安装后执行:
|
|
docker version 应同时显示 Client 和 Server;hello-world 应输出 Hello from Docker!。如果无法连接 daemon,先启动 Docker Desktop 或 Linux 上的 Docker 服务。docker info 可以进一步查看服务端环境。
启动 Nginx
|
|
打开 http://127.0.0.1:8080,应看到 Nginx 欢迎页,也可以用下面的命令检查:
|
|
预期 HTTP 状态为 200 OK,容器状态为 Up。如果宿主机的 8080 已被占用,把发布参数改成 127.0.0.1:18080:80,访问地址也改为 18080。后面的独立容器实验同样只修改宿主机端口,Compose 示例则通过 APP_PORT 调整。
| 片段 | 含义 |
|---|---|
docker run |
根据镜像创建并启动一个新容器;本地没有镜像时自动拉取 |
--detach / -d |
在后台运行 |
--name docker-web |
为容器指定名称,后续命令可直接引用 |
--publish 127.0.0.1:8080:80 / -p |
把宿主机本地的 8080 转发到容器的 80 |
nginx:alpine |
镜像仓库名与标签 |
命令结构是 docker run [Docker 选项] 镜像 [命令及参数]。-p、--name 等 Docker 选项必须写在镜像名之前。
查看日志,再结束实验
|
|
此时容器仍然存在,只是状态变为 Exited。重新启动和删除是两个不同操作:
|
|
删除后可以再次使用 docker-web 这个名称。镜像仍保留在本机,下次运行可直接复用。
理解镜像、容器与运行环境
Docker 用统一的工具构建、分发和运行容器化应用。它把应用、用户空间依赖和运行时打包成镜像,减少“换一台机器就无法启动”的环境差异;端口、数据和密钥等运行配置仍需要单独提供。
从构建到运行
flowchart LR
A[源代码与 Dockerfile] -->|docker build| B[镜像]
B -->|docker push| C[镜像仓库 Registry]
C -->|docker pull| D[另一台机器上的镜像]
B -->|docker run| E[容器]
D -->|docker run| F[容器]
| 对象 | 保存什么 | 生命周期 |
|---|---|---|
| Dockerfile | 基础镜像、复制文件和构建命令等配方 | 随源代码维护 |
| 镜像(image) | 应用、依赖、只读文件系统层与默认配置 | 可用于创建多个容器 |
| 容器(container) | 运行配置、进程状态和独立可写层 | 停止后仍存在,删除时可写层消失 |
| 数据卷(volume) | 需要跨容器保留的数据 | 命名数据卷可独立于容器存在 |
| 网络(network) | 容器之间的连接、地址与服务发现 | 可供多个容器加入 |
Registry 是存储和分发镜像的服务,例如 Docker Hub 或私有镜像仓库。日常命令由 Docker CLI 发出,经 API 交给 Docker daemon;daemon 管理这些对象,并通过 containerd、OCI runtime 等组件启动容器进程。
镜像不会随着容器一起改变
容器运行时,在镜像的只读层之上使用自己的可写层。写入这个可写层的文件不会修改原镜像,也不会自动进入其他容器。
| 操作 | 实际发生的事 |
|---|---|
docker run IMAGE |
创建一个新容器并启动 |
docker stop CONTAINER |
停止容器进程,保留容器及其可写层 |
docker start CONTAINER |
再次启动已有容器 |
docker rm CONTAINER |
删除容器及其可写层;命名数据卷通常保留 |
docker pull IMAGE |
下载镜像,不会替换已经运行的容器 |
更新应用需要使用新镜像重新创建容器。 单纯拉取镜像或重启旧容器,不会把新镜像内容应用进去。
nginx:alpine 中的 alpine 是标签。标签可以被发布者重新指向另一个镜像;省略标签时默认使用 latest,它也只是普通标签,不保证最新或最稳定。本文使用便于练习的版本或变体标签,正式交付应选择经过测试的版本;严格复现时使用 digest,并安排后续更新。
容器与虚拟机的边界
| 维度 | Linux 容器 | 虚拟机 |
|---|---|---|
| 内核 | 共享宿主 Linux 内核 | 每台虚拟机运行自己的内核 |
| 隔离方式 | 进程与资源隔离 | 硬件虚拟化 |
| 启动与开销 | 通常较快、较轻 | 通常需要启动完整操作系统 |
| 常见用途 | 应用交付、开发环境、CI/CD | 不同内核、完整系统环境、较强隔离 |
Linux 通过 namespaces 隔离进程、网络和挂载等资源视图,通过 cgroups 统计和限制资源,结合 capabilities、seccomp 等机制约束权限。容器默认不会自动获得合适的 CPU、内存限制。
在 macOS 和 Windows 的 Docker Desktop 中,Linux 容器通常运行在其管理的 Linux 虚拟机里。镜像仍受操作系统、CPU 架构和内核能力约束;linux/amd64 与 linux/arm64 之间的运行需要兼容架构或模拟支持。
用 Dockerfile 构建自己的服务
这一节创建一个只依赖 Go 标准库的 HTTP 服务。编译器放在构建镜像中,本机不需要安装 Go。
准备应用文件
创建目录,后续构建和 Compose 命令都在这里执行:
|
|
目录中先准备四个文件:
|
|
go.mod:
|
|
main.go:
|
|
/ 返回问候语和容器主机名,/health 返回 ok。程序监听 :8080,可通过容器网络接口访问;如果只监听容器内的 127.0.0.1,端口映射通常无法访问它。
编写多阶段 Dockerfile
Dockerfile:
|
|
build 阶段包含编译器和源码;runtime 阶段只复制编译结果,并以非 root 用户运行。这个纯 Go 示例关闭了 CGO,避免依赖构建阶段的动态链接库。使用 CGO 的应用需要另行匹配运行时依赖。
先理解构建时执行的五条指令:
| 指令 | 作用 |
|---|---|
FROM |
选择基础镜像,也可以开始一个新阶段 |
WORKDIR |
设置后续指令的工作目录 |
COPY |
复制构建上下文中的文件;--from 可从其他阶段复制 |
RUN |
在构建过程中执行命令 |
ARG |
声明构建参数,不会自动成为容器环境变量 |
再区分会影响运行默认值的指令:
| 指令 | 作用 |
|---|---|
ENV |
设置环境变量,容器启动时可覆盖 |
USER |
设置后续构建指令和容器进程的用户 |
EXPOSE |
记录应用端口,不会自动发布宿主机端口 |
ENTRYPOINT |
设置容器入口程序 |
CMD |
提供默认命令;配合 ENTRYPOINT 时通常提供默认参数 |
这里的 exec form ENTRYPOINT ["./server"] 让服务直接接收容器信号。应用仍需自行处理信号,才能完成连接排空等优雅退出逻辑。
控制构建上下文与缓存
.dockerignore:
|
|
执行 docker build ... . 时,最后的 . 指定构建上下文。普通 COPY 从上下文中取文件,不能越过它读取父目录;.dockerignore 排除的文件也不能被普通 COPY 复制。
Dockerfile 先复制 go.mod 并下载依赖,再复制 main.go。这样只修改源码时,通常可以复用依赖下载层。当前示例没有第三方依赖,因此没有 go.sum;实际项目应一起复制并维护 go.mod 和 go.sum。
BuildKit 还支持缓存挂载。需要加速 Go 编译时,可以把构建指令替换为:
|
|
缓存挂载用于复用下载或编译结果,其目录内容不会自动写入镜像层。构建凭据应使用 secret mount,避免通过 ARG、ENV 或“复制后删除”留在镜像配置或历史层中。
构建、运行并验证
确认第一节的 Nginx 已停止,释放 8080 端口:
|
|
预期分别返回 hello from <容器主机名> 和 ok。--pull 会检查基础镜像更新;它不等于禁用构建缓存。这里使用 --rm,停止后会自动删除实验容器:
|
|
把数据与网络接到容器上
验证数据卷的生命周期
停止容器会保留可写层,删除容器才会删除可写层。需要跨容器保存的数据,应放到挂载中。
| 挂载方式 | 数据在哪里 | 典型用途 |
|---|---|---|
| 命名数据卷(volume) | 由 Docker 管理,独立于容器 | 数据库、上传文件 |
| 绑定挂载(bind mount) | Docker 宿主机上的指定路径 | 开发源码、配置文件 |
| tmpfs | Linux 宿主机内存,停止后消失 | 临时文件;仍可能受交换空间配置影响 |
先用一个容器写入数据,再用另一个容器读取:
|
|
看到 persisted 就说明数据不依赖第一个容器。--rm 不会删除显式命名的数据卷。
绑定挂载适合把已有文件交给容器,例如只读查看当前目录:
|
|
--mount 默认会在绑定挂载源路径不存在时报错;-v 则可能自动创建目录,掩盖路径拼写错误。挂载会遮蔽目标路径的原有内容;首次挂载空数据卷时,Docker 默认还可能把目标目录原有内容复制进卷。绑定挂载需要额外处理宿主机路径与 UID/GID 权限,Docker Desktop 还受目录共享设置影响。
练习完成后,只删除本节创建的卷;这会永久删除其中的实验数据:
|
|
数据卷本身不是备份。数据库应优先使用自身的备份工具并验证恢复;复制数据文件时需要保证写入一致性。
通过容器名称访问服务
先建立用户自定义桥接网络,再把第三节的 Go 镜像接进去:
|
|
预期输出 ok。这次没有 -p:临时客户端与应用在同一用户自定义网络中,可以通过 Docker DNS 解析容器名,直接访问容器端口。默认 bridge 网络不提供相同的容器名自动解析体验。
| 谁访问谁 | 应使用的地址 |
|---|---|
| 宿主机访问发布后的服务 | 127.0.0.1:宿主机端口 |
| 同一网络中的容器访问应用 | docker-demo-app:8080;Compose 中使用服务名 |
| 容器访问自己 | 127.0.0.1:容器端口 |
| 容器访问 Docker Desktop 宿主机 | 通常使用 host.docker.internal:服务端口 |
Linux Engine 上访问宿主机时,可在 docker run 中添加 --add-host host.docker.internal:host-gateway;宿主机服务也必须监听容器可达的接口并允许访问。容器内的 localhost 不会指向另一个容器或宿主机。
发布端口时,-p 8080:80 默认通常绑定宿主机所有接口;-p 127.0.0.1:8080:80 用于本机开发。服务间通信通常只需要共享网络,不必给每个服务发布端口。
结束本节实验,下一节交给 Compose 管理网络:
|
|
用 Compose 组织完整应用
现在把 Go 服务放在 Caddy 反向代理后面,由一份 compose.yaml 声明构建、网络、挂载和启动依赖。现代 Compose 使用 docker compose 命令并遵循 Compose Specification,顶层不需要 version: "3"。
flowchart LR
A[浏览器] -->|127.0.0.1:8080| B[proxy:80 / Caddy]
B -->|app:8080 / 项目网络| C[app / Go 服务]
D[Caddyfile] -.只读绑定挂载.-> B
E[Caddy 命名数据卷] --- B
添加 Caddy 与 Compose 配置
在第三节的 docker-demo 目录里增加 Caddyfile 和 compose.yaml:
|
|
Caddyfile:
|
|
compose.yaml:
|
|
这里使用 Compose 自动创建的项目默认网络,proxy 可以通过服务名 app 找到 Go 服务,无需硬编码 IP 或设置 container_name。只有 proxy 向宿主机发布端口,app 通过项目网络接收请求。
Caddyfile 以只读方式挂载,源文件不存在时直接报错。/data 和 /config 使用命名数据卷保存 Caddy 的持久状态;本例只配置 HTTP,不涉及域名与自动 HTTPS 证书签发。
app 的健康检查在它自己的容器中执行,因此使用 127.0.0.1:8080。运行镜像 Alpine 自带的 BusyBox wget 可用于本例检查;换成 distroless 等镜像时,必须同步调整检查方式。
启动并检查整条请求链路
|
|
预期 app 显示 healthy、proxy 处于运行状态,请求返回 hello from <容器主机名> 与 ok。可用 docker compose logs --tail 50 排查启动问题。
depends_on: condition: service_healthy 让 Caddy 在创建启动时等待应用健康;普通 depends_on 只保证依赖先启动,不保证可接受请求。--wait 对有健康检查的服务等待健康,对没有健康检查的服务只等待运行,因此仍需用 curl 验证入口。
健康检查失败不会单独触发 restart: unless-stopped。重启策略主要处理进程退出,启动依赖也不会持续修复运行期间的依赖故障;实际应用仍需实现超时、重试和恢复。
修改配置:区分插值与容器环境变量
在同一目录新建可选的 .env:
|
|
保持 compose.yaml 使用前面的完整配置,然后执行:
|
|
预期返回 hello-compose from <容器主机名>。后续从宿主机验证服务时,使用新端口 18080。
| 配置机制 | 发生在哪一步 | 本例中的作用 |
|---|---|---|
Shell 环境或 .env 中的变量 |
Compose 解析配置时 | 替换 ${APP_PORT:-8080} 等表达式;Shell 同名变量优先 |
environment |
创建容器时 | 将展开后的 APP_MESSAGE 注入应用容器 |
服务级 env_file |
创建容器时 | 把指定文件中的变量传给该服务容器 |
.env 被 Compose 读取,不代表其中所有变量自动进入容器。 本例的 APP_PORT 只改变宿主机发布端口,Go 服务仍监听容器端口 8080。APP_MESSAGE 能被 Go 程序读取,是因为配置中写了 environment。
docker compose config 会显示展开后的配置,不应把包含真实密钥的输出直接贴入公共日志。密钥应使用未提交到 Git 的文件或专门的密钥管理系统;Compose secrets 可将文件挂到 /run/secrets/<名称>,但本地源文件仍是明文。
更新与停止
| 需求 | 命令 |
|---|---|
| 修改源码或 Dockerfile 后更新 | docker compose up -d --build --wait |
| 修改环境变量或 Compose 服务配置后更新 | docker compose up -d --wait |
| 拉取 Caddy 镜像更新并应用 | docker compose pull --ignore-buildable,再执行 docker compose up -d --wait |
| 暂停环境,保留容器 | docker compose stop;恢复用 docker compose start |
| 结束实验,删除容器与项目网络 | docker compose down;默认保留命名数据卷 |
docker compose restart 只重启已有容器,不会应用新的环境变量、镜像或服务配置。若只修改了绑定挂载的 Caddyfile 内容,可执行 docker compose exec proxy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile,让 Caddy 重新加载配置。
完整重置本例时,docker compose down --volumes 还会删除声明的非 external 命名数据卷和附属匿名卷。它会永久删除卷内数据,且不会额外询问确认,应与日常 down 区分使用。
命令速查
下面的 IMAGE、CONTAINER、NETWORK 和 registry.example.com/team 都是占位值,执行前需要替换。这些是按需查阅的命令,不是一段需要顺序执行的脚本。
镜像与分发
| 目的 | 命令 |
|---|---|
| 拉取、列出镜像 | docker pull IMAGE / docker image ls(或 docker images) |
| 检查配置与构建历史 | docker image inspect IMAGE / docker image history IMAGE |
| 添加引用、推送镜像 | docker tag IMAGE registry.example.com/team/app:1.0 / docker push registry.example.com/team/app:1.0 |
| 离线导出与载入 | docker save -o app.tar IMAGE / docker load -i app.tar |
| 删除本地镜像引用 | docker image rm IMAGE(或 docker rmi IMAGE) |
推送前使用 docker login registry.example.com 登录;自动化中使用 --password-stdin 从标准输入接收凭据,避免把令牌直接写在命令参数中。
save/load 保存镜像配置和镜像层,不包含数据卷或容器可写层。export/import 处理容器文件系统,无法完整保留原镜像的分层、历史与配置,也不会备份挂载卷,不能与镜像归档或数据备份混用。
需要发布多架构镜像时,可使用 Buildx:
|
|
跨架构 RUN 需要原生 worker 或 QEMU/binfmt 模拟支持。非默认 builder 的单平台本地构建通常需要 --load 才会载入本地镜像列表,多平台结果通常直接推送到仓库。
容器与日志
| 目的 | 命令 |
|---|---|
| 查看全部容器及状态 | docker ps -a |
| 跟踪最近日志 | docker logs --follow --tail 100 CONTAINER |
| 在已有容器中执行命令 | docker exec -it CONTAINER sh |
| 检查配置与端口 | docker inspect CONTAINER / docker port CONTAINER |
| 查看资源与进程 | docker stats CONTAINER / docker top CONTAINER |
logs --follow 和 stats 会持续运行,按 Ctrl+C 退出查看;exec 启动的 shell 用 exit 退出。精简镜像可能没有 Bash,distroless 或 scratch 镜像甚至没有 shell,应通过日志、指标或专门的调试工具排查。
需要复制文件时,使用 docker cp ./file CONTAINER:/path 或反向复制;需要查看可写层变化时,使用 docker diff CONTAINER。这些操作不会把修改自动写回 Dockerfile。
Compose 与资源限制
| 目的 | 命令 |
|---|---|
| 检查配置与状态 | docker compose config / docker compose ps |
| 跟踪一个服务的日志 | docker compose logs --follow --tail 100 app |
| 在应用容器内执行命令 | docker compose exec app sh |
| 创建一次性容器并覆盖入口 | docker compose run --rm --entrypoint sh app -c 'id' |
| 查看项目使用的镜像 | docker compose images |
给独立容器设置资源上限的示例,使用第三节已经构建的镜像:
|
|
命令在前台运行,按 Ctrl+C 结束。Compose 服务可以配置对应的 mem_limit、cpus、pids_limit,再用 up -d 应用,并通过 docker inspect、docker stats 核对效果。
磁盘占用与清理
先用 docker system df --verbose 确认空间用在哪里,再选择清理范围:
| 命令 | 删除范围 |
|---|---|
docker container prune |
所有已停止的容器及其可写层 |
docker image prune |
悬空镜像;加 --all 扩大到未被容器引用的镜像 |
docker builder prune |
未使用的构建缓存,默认主要清理悬空缓存 |
docker network prune |
未被容器使用的网络 |
docker volume prune |
现代 Engine 默认仅清理未被容器引用的匿名本地卷;--all 包括命名卷 |
未被引用,不代表数据已经没有价值。 prune 的确认提示主要说明删除范围,并不是完整的待删除对象预览。先查看 docker ps -a、docker volume ls 和相关 inspect 输出,确认后再清理;能明确定位的对象,优先按名称删除。
docker system prune --all --volumes 会合并清理停止的容器、未使用网络、未被容器引用的镜像、构建缓存与未使用匿名卷,影响范围很大,不适合作为日常实验的收尾命令。
常见问题
容器启动后立即退出
容器主进程退出,容器就会停止。先查退出码和日志:
|
|
检查入口命令、缺失配置与应用错误。服务应以前台进程运行;在容器里把服务放到后台后退出入口脚本,会导致容器结束。排障期间可暂时省略 --rm,保留退出后的状态与日志。
端口已经发布,却无法访问
- 用
docker ps -a和日志确认服务没有退出。 - 用
docker port CONTAINER核对“宿主机端口 → 容器端口”的映射。 - 确认应用监听容器网络接口,而不是只监听容器内的
127.0.0.1。 - 如果从另一台机器访问,确认端口没有仅绑定宿主机
127.0.0.1,并检查防火墙与安全组。
如果报 port is already allocated,是宿主机端口冲突:单容器修改 -p 左侧端口,Compose 示例修改 APP_PORT。容器内部的监听端口不必跟着改变。
服务名解析失败,或代理返回 502
先用 docker compose ps 确认两个服务都存在,再检查 docker compose logs --tail 100 app proxy。同一网络内使用服务名与容器端口,不能用宿主机映射端口或把 localhost 当作其他服务。
对本文示例,可从 Caddy 容器中直接请求应用:
|
|
返回 ok 说明代理到应用的网络路径正常,再检查 Caddy 配置与宿主机入口。独立容器使用 docker network inspect NETWORK 核对网络成员。
修改代码或配置后没有生效
| 修改内容 | 应执行的操作 |
|---|---|
main.go、Dockerfile 或构建依赖 |
docker compose up -d --build --wait |
.env、environment 或端口等服务配置 |
docker compose up -d --wait |
绑定挂载的 Caddyfile |
在 Caddy 容器中执行 caddy reload |
先检查构建上下文、.dockerignore 和 COPY 是否包含目标文件。只有仍怀疑缓存时,才用 docker compose build --no-cache app 后重新 up -d;--no-cache 不需要用于每次构建,也不会自动拉取更新的基础镜像。
重建容器后数据不见了
先确认数据是否曾写入命名卷,以及新容器是否挂载了同一个卷。docker inspect CONTAINER 的 Mounts 和 docker volume inspect VOLUME 可帮助核对。
Compose 默认把项目名作为卷名的一部分,例如本例的 docker-demo_caddy-data。改变项目名可能创建一组新卷,看起来像“数据丢失”,旧卷可能仍在。不要先运行清理命令;如果已执行 down --volumes,则需要从备份恢复。
用到真实项目时
本文示例用于本地练习。投入实际服务前,补齐与应用相关的五件事:
- 管理交付版本:使用可信基础镜像,固定经过测试的版本或 digest,持续更新与扫描漏洞。
- 限制运行权限:使用非 root 用户,按需配置资源上限,谨慎开放 Docker socket、宿主机目录或
--privileged。 - 分开配置与密钥:普通配置可以版本化,真实密钥放到独立的密钥管理流程中,只发布必要端口。
- 验证服务恢复:实现依赖超时与重试、优雅退出,接入健康监测和日志。
- 验证数据恢复:为持久数据建立备份,实际演练恢复,确认重建容器后仍能读取原数据。
默认以 root 运行的 Linux Docker daemon 具有很高的宿主机权限,访问其 socket 或加入 docker 用户组通常也意味着接近 root 的能力。共享容器网络只提供连接与一定的隔离,不会自动提供服务认证或流量加密。