随记
Docker 从零到实践:完整学习笔记
面向零基础读者的 Docker 系统学习笔记,覆盖跨平台安装、镜像与容器、Dockerfile、存储、网络、Compose、Python + Redis 实战、排障、安全与生产实践。
发布于 2026年7月29日
Docker 从零到实践:完整学习笔记
Docker 解决的核心问题可以概括为一句话:把应用及其运行所需的环境一起交付,并用一致的方法构建、运行和管理它。 本文面向第一次接触容器的读者,从安装和第一条命令开始,逐步讲清镜像、容器、Dockerfile、存储、网络、Compose、排障、安全与单机生产实践,最后完成一个 Python Flask + Redis 多容器项目。
文中的命令默认使用当前主流的 Docker CLI 与 Compose v2。Compose 命令统一写成 docker compose,配置文件统一使用 compose.yaml。标有“会删除数据”的命令不要直接用于重要环境;在执行任何清理操作前,先用 docker ps -a、docker image ls、docker volume ls 确认目标。
1. 学习目标、前置知识与路线图
1.1 学完后应该具备的能力
完成本文后,你应该能够:
- 解释镜像、容器、仓库、卷和网络分别解决什么问题。
- 在 Windows、macOS 或 Ubuntu 上安装并验证 Docker。
- 使用 CLI 创建、查看、停止、删除和调试容器。
- 编写 Dockerfile,把一个应用构建为镜像。
- 使用 volume 保存数据,使用自定义网络连接多个容器。
- 使用
compose.yaml管理多容器应用。 - 看懂日志、健康状态和资源使用情况,并按步骤排查常见故障。
- 避免把密码写进镜像、暴露不必要端口、滥用
--privileged等常见安全问题。 - 理解单机 Compose 的适用范围,并知道什么时候需要 Swarm、Kubernetes 或云容器平台。
1.2 前置知识
不要求有容器经验,但最好了解以下基础:
- 会使用终端进入目录并执行命令。
- 知道文件、目录、端口和进程是什么。
- 能看懂少量 YAML;不熟悉也可以边做边学。
- 综合项目使用 Python,但代码很短,不要求掌握 Flask 或 Redis。
1.3 推荐学习顺序
| 阶段 | 章节 | 目标 |
|---|---|---|
| 入门 | 1~5 | 安装 Docker,运行并访问第一个容器 |
| 核心 | 6~10 | 理解镜像和容器,能够独立编写 Dockerfile |
| 组合 | 11~15 | 掌握配置、存储、网络和 Compose,完成综合项目 |
| 实战 | 16~18 | 学会排障、安全加固、发布与后续进阶 |
不要只背命令。每学完一节,尝试回答三个问题:这个对象解决什么问题?它的数据保存在哪里?删除它会产生什么影响?
1.4 练习
建立一个专门的练习目录,并记录每次实验使用的容器名、镜像名和端口。保持命名明确,会让后续清理和排障轻松很多。
2. Docker 是什么:容器与虚拟机的区别
2.1 Docker 解决了什么问题
传统部署常遇到“在我的电脑上可以运行”的问题:开发机和服务器的系统库、运行时、配置或依赖版本不同,应用换一台机器就失败。Docker 允许开发者把应用、运行时和依赖制作成镜像,再从同一个镜像创建容器。
容器不是一份正在运行的源代码目录,而是由镜像启动的隔离进程。它拥有自己的文件系统视图、网络接口和进程空间,同时仍与宿主机共享内核。
2.2 容器和虚拟机
| 对比项 | 容器 | 虚拟机 |
|---|---|---|
| 隔离对象 | 一组进程 | 一整套客体操作系统 |
| 内核 | 与宿主机共享内核 | 每台虚拟机有自己的内核 |
| 启动速度 | 通常为秒级或更快 | 通常更慢 |
| 镜像体积 | 常见为几十到数百 MB | 常见为数 GB |
| 隔离强度 | 进程级隔离,依赖内核安全能力 | 虚拟硬件边界通常更强 |
| 典型用途 | 应用交付、开发环境、服务拆分 | 运行不同操作系统、强隔离工作负载 |
容器轻量并不表示它“没有操作系统”。镜像通常仍包含用户空间文件,例如 /bin、动态库和包管理器;只是它不再携带并启动一个独立内核。
2.3 Windows 和 macOS 上为什么也能运行 Linux 容器
Linux 容器需要 Linux 内核。Docker Desktop 在 Windows 和 macOS 上通过一个受管理的轻量 Linux 虚拟机提供该内核。Windows 上常使用 WSL 2 后端。因此:
- Windows/macOS 的路径挂载、文件权限和网络行为可能与原生 Linux 略有差异。
- Linux 镜像不能直接共享 Windows 内核运行,它实际运行在 Docker Desktop 管理的 Linux 环境中。
- “容器比虚拟机轻”与“Docker Desktop 内部完全没有虚拟机”是两件不同的事。
2.4 容器不是什么
- 容器不是自动备份:删除容器可能丢失可写层中的数据。
- 容器不是绝对安全边界:错误挂载宿主目录或 Docker Socket 仍可能危及宿主机。
- 容器不是编排平台:Docker Engine 管理一台主机,Compose 主要管理一个项目;跨主机调度要使用其他方案。
- 容器不是必须拆成微服务:一个结构清楚的单体应用同样可以容器化。
2.5 小练习
用自己的话解释“镜像是模板,容器是实例”。再思考:同一个 Nginx 镜像能否同时启动三个容器?三个容器的运行状态是否彼此独立? 可以。同一个 Nginx 镜像完全可以同时启动多个容器,并且这些容器的运行状态默认是彼此独立的。
例如:
docker run -d --name nginx1 -p 8081:80 nginx
docker run -d --name nginx2 -p 8082:80 nginx
docker run -d --name nginx3 -p 8083:80 nginx
这里三个容器:
nginx镜像
|
---------------------
| | |
nginx1 nginx2 nginx3
容器 容器 容器
它们都是基于同一个 nginx 镜像创建出来的独立实例。
| 项目 | 是否独立 |
|---|---|
| 容器启动/停止 | ✅独立 |
| 进程 | ✅独立 |
| 文件系统 | ✅独立 |
| 网络 | ✅默认独立 |
| CPU/内存限制 | ✅独立 |
| 镜像 | ❌共享 |
| 宿主机内核 | ❌共享 |
| 挂载的数据卷 | ⚠️可能共享 |
3. Docker 的组成:CLI、Engine、containerd 与仓库
3.1 一条命令背后发生了什么
当你执行下面的命令:
docker run nginx:alpine
大致会发生以下过程:
- Docker CLI 把请求发送给 Docker Engine 中的守护进程
dockerd。 - Engine 检查本地是否存在
nginx:alpine镜像。 - 若本地没有,Engine 从配置的镜像仓库拉取镜像及各个层。
- Engine 准备容器配置、可写层、网络和挂载。
- Engine 通过 containerd 和底层 OCI 运行时创建容器进程。
- 容器的主进程开始运行;主进程退出后,容器也随之停止。
初学阶段不需要直接操作 containerd。日常工作主要使用 Docker CLI、Dockerfile 和 Compose。
3.2 核心组件
- Docker CLI:
docker命令,负责接收用户输入并调用 Engine API。 - Docker Engine:管理镜像、容器、网络和卷。
- Docker Registry:保存和分发镜像,Docker Hub 是常用公共仓库。
- BuildKit:负责现代 Docker 镜像构建、缓存和多阶段构建。
- Compose:用一个 YAML 文件描述并管理多容器应用。
3.3 客户端和服务端
执行:
docker version
输出通常分为 Client 和 Server 两部分:
- 只有 Client:CLI 已安装,但守护进程没有运行、当前 context 错误,或无权访问 Socket。
- Client 和 Server 都有:CLI 已成功连接 Engine。
查看更完整的环境信息:
docker info
docker context ls
docker context show
docker info 会显示存储驱动、可用资源、容器数量和安全选项。不要在公开工单中无检查地粘贴完整输出,因为其中可能包含内部仓库地址、代理或主机信息。
3.4 Docker Socket 为什么敏感
Linux 默认通过 /var/run/docker.sock 访问守护进程。能控制这个 Socket 的用户通常可以创建高权限容器、挂载宿主文件并间接取得宿主机 root 权限。因此:
- 不要为了省事把 Socket 暴露到公网。
- 不要随意将
/var/run/docker.sock挂载进第三方容器。 - 加入
docker用户组不是普通的低权限授权,应按 root 级权限管理。
3.5 小练习
执行 docker version 和 docker info,找出当前 Engine 版本、操作系统、CPU 数量和总内存。若使用 Docker Desktop,再观察它分配给 Linux 虚拟机的资源是否与宿主机总资源相同。
4. 在 Windows、macOS 与 Ubuntu 上安装 Docker
安装步骤可能随版本和操作系统支持范围变化。开始前应查看 Docker 官方安装文档;不要从不明网站下载安装包或直接执行来源不明的一键脚本。
4.1 Windows:Docker Desktop 与 WSL 2
推荐流程:
- 在 BIOS/UEFI 中启用硬件虚拟化。
- 启用并更新 WSL 2。
- 从 Docker Desktop 官方页面下载安装程序。
- 在 Docker Desktop 设置中使用 WSL 2 后端和 Linux containers。
- 根据需要为具体 WSL 发行版启用集成。
在 PowerShell 中检查 WSL:
wsl --status
wsl --version
wsl --list --verbose
安装完成后重新打开终端:
docker version
docker compose version
docker run --rm hello-world
常见问题:
docker命令不存在:重新打开终端,确认 Docker Desktop 已完成启动。- WSL 版本过旧:运行
wsl --update后重启。 - 路径挂载很慢:项目尽量放在 WSL 的 Linux 文件系统中,而不是跨文件系统频繁访问大量小文件。
- 端口无法访问:确认命令发布了端口,并检查 Windows 防火墙和端口占用。
4.2 macOS:Docker Desktop
从 Docker Desktop for Mac 官方页面选择与芯片匹配的版本:
- Apple Silicon 选择 Arm 架构版本。
- Intel Mac 选择 x86_64 版本。
安装并启动后验证:
docker version
docker compose version
docker run --rm hello-world
镜像需要支持当前 CPU 架构。多数官方镜像提供多架构清单,Docker 会自动选择匹配版本;若强制运行其他架构,可能使用模拟并导致性能下降。
4.3 Ubuntu:使用官方 apt 仓库
下面以官方仓库方式安装 Docker Engine。支持的 Ubuntu 版本会变化,应先核对 Docker Engine on Ubuntu。
移除可能冲突的发行版包:
sudo apt remove docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc
配置官方签名密钥和软件源:
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
检查服务:
sudo systemctl status docker
sudo docker run --rm hello-world
官方便捷安装脚本更适合测试和开发,不应在不了解其行为时用于生产主机。生产环境还应制定版本升级、回滚和防火墙检查流程。
4.4 是否允许普通用户直接运行 Docker
若把当前用户加入 docker 组:
sudo usermod -aG docker "$USER"
需要重新登录才能生效。再次强调:docker 组通常等价于授予 root 级控制能力。多人服务器应评估 Rootless mode、远程受控构建器或其他隔离方案,而不是无差别加入该组。
4.5 统一验收
无论使用哪个系统,都执行:
docker version
docker info
docker compose version
docker run --rm hello-world
验收标准:
- Client 和 Server 信息都能正常显示。
- Compose 返回 v2 版本。
hello-world能拉取、运行并自动删除。- 没有 Socket 权限、代理、证书或磁盘空间错误。
4.6 小练习
记录自己的系统、CPU 架构、Docker Engine 版本、Compose 版本和当前 context。以后遇到“别人能运行、我不能”的问题,这些信息是最先要比较的环境基线。
5. 第一个容器:hello-world 与 Nginx
5.1 docker run 做了什么
运行一次性验证容器:
docker run --rm hello-world
docker run 相当于“必要时拉取镜像 + 创建容器 + 启动容器”。--rm 表示主进程退出后自动删除容器,适合一次性任务,不适合需要保留停止状态或可写层的场景。
5.2 在后台运行 Nginx
docker run --name beginner-nginx \
--detach \
--publish 127.0.0.1:8080:80 \
nginx:alpine
参数说明:
--name beginner-nginx:设置易识别的容器名。--detach或-d:后台运行。--publish 127.0.0.1:8080:80:将宿主机本地地址的 8080 端口映射到容器的 80 端口。nginx:alpine:使用 Nginx 官方镜像的 Alpine 变体。
浏览器访问 http://127.0.0.1:8080,或执行:
curl http://127.0.0.1:8080
端口格式是:
宿主机地址:宿主机端口:容器端口
只写 8080:80 通常会监听宿主机所有接口。学习和本地管理工具优先绑定 127.0.0.1,避免无意暴露给局域网或公网。
5.3 查看和管理
docker ps
docker logs beginner-nginx
docker inspect beginner-nginx
docker stop beginner-nginx
docker start beginner-nginx
docker rm -f beginner-nginx
docker ps只显示运行中的容器。docker ps -a包括已经停止的容器。docker stop先发送正常终止信号,超时后才强制结束。docker rm -f会强制删除运行中的容器;生产环境应优先正常停止。
5.4 常见错误
端口已被占用
bind: address already in use
换一个宿主机端口,例如 127.0.0.1:8081:80,或先找出占用 8080 的进程/容器。
容器名冲突
Conflict. The container name ... is already in use
执行 docker ps -a --filter name=beginner-nginx,确认旧容器是否可以删除,不要盲目使用强制命令。
容器启动后立即退出
容器的主进程已经结束。使用:
docker ps -a
docker logs <容器名>
docker inspect <容器名>
5.5 小练习
启动两个 Nginx 容器,分别命名为 nginx-a 和 nginx-b,映射到本机 8081、8082 端口。确认两者都可访问,然后正常停止并删除它们。
6. 镜像、容器、仓库、标签、摘要与分层文件系统
6.1 镜像名称
完整镜像引用通常可以表示为:
registry/namespace/repository:tag
例如:
docker.io/library/nginx:alpine
- Registry:
docker.io - Namespace:
library,Docker Hub 官方镜像常省略 - Repository:
nginx - Tag:
alpine
写成 nginx:alpine 时,省略部分由默认规则补齐。
6.2 Tag 不是不可变版本
Tag 是一个可移动的名字。维护者可能用同一个 Tag 发布更新后的镜像。生产环境若要求精确复现,可记录或部署镜像摘要:
nginx@sha256:...
摘要根据内容生成,指向确定的镜像清单。常见做法是:
- 开发环境使用清晰的版本 Tag。
- CI 构建后记录 Registry 返回的摘要。
- 生产部署使用经过验证的摘要。
- 定期构建和升级,而不是永久停留在旧摘要。
latest 只是默认 Tag 名,不表示“本地永远自动更新到最新版本”。
6.3 镜像层
Dockerfile 中的部分指令会产生只读层。多个镜像可以共享相同层,从而减少下载和磁盘占用。创建容器时,Docker 在镜像层上增加容器自己的可写层。
docker image history nginx:alpine
docker image inspect nginx:alpine
容器可写层的特点:
- 只属于当前容器。
- 删除容器时随容器一起删除。
- 不适合保存数据库等重要数据。
- 不应通过进入容器手工修改来代替重新构建镜像。
查看容器相对镜像发生的文件变化:
docker diff <容器名>
6.4 镜像、容器和卷的生命周期
| 对象 | 创建方式 | 删除后主要影响 |
|---|---|---|
| 镜像 | docker pull、docker build |
不能再从本地创建该镜像的新容器,可重新拉取或构建 |
| 容器 | docker run、docker create |
运行状态和容器可写层消失 |
| Named volume | docker volume create 或自动创建 |
持久化数据消失,通常不可自动恢复 |
| 网络 | docker network create 或 Compose 创建 |
已连接容器的通信关系受影响 |
6.5 为什么不推荐 docker commit
docker commit 可以把容器当前文件系统保存为镜像,但它难以审查、复现和自动化。正常项目应把变更写入 Dockerfile 和依赖清单,再执行可重复构建。commit 更适合临时诊断或取证,不应成为正式发布流程。
6.6 小练习
拉取 alpine 镜像,查看它的 ID、Tag、摘要、架构和层历史。启动一个容器创建临时文件,再用 docker diff 观察变化,最后删除容器并确认镜像仍然存在。
7. 容器生命周期与常用 CLI
7.1 create、start 和 run
docker create --name demo alpine:latest sleep 300
docker start demo
docker stop demo
docker rm demo
docker create 只创建不启动;docker start 启动已有容器;docker run 把两步组合起来。大多数时候直接使用 run,但拆开有助于理解生命周期。
7.2 前台、后台和交互式运行
前台运行:
docker run --rm alpine:latest echo "hello"
交互式终端:
docker run --rm -it alpine:latest sh
-i保持标准输入。-t分配伪终端。- 输入
exit结束 shell,容器主进程结束后容器退出。
后台运行:
docker run -d --name sleeper alpine:latest sleep 300
7.3 查看状态和筛选
docker ps
docker ps -a
docker ps --filter status=exited
docker ps --filter name=sleeper
docker inspect sleeper
docker top sleeper
docker stats --no-stream sleeper
inspect 返回底层 JSON,适合确认端口、挂载、环境变量、网络、退出码和健康状态。可以用格式化输出提取字段:
docker inspect --format '{{.State.Status}} {{.State.ExitCode}}' sleeper
7.4 日志
docker logs sleeper
docker logs --tail 100 sleeper
docker logs --since 10m sleeper
docker logs -f sleeper
Docker 默认收集容器主进程写到标准输出和标准错误的内容。应用若只写容器内部日志文件,docker logs 可能看不到,需要调整应用日志方式或配置日志采集。
7.5 在运行中的容器执行命令
docker exec sleeper ps
docker exec -it sleeper sh
exec 不会创建新容器,而是在现有容器中启动额外进程。极简镜像可能没有 bash、curl、ps 甚至 shell;这不是 Docker 故障。不要为了方便排障就把大量工具永久塞进生产镜像,可以使用专门的调试容器或临时调试镜像。
7.6 复制文件
docker cp sleeper:/etc/alpine-release ./alpine-release
docker cp ./local-file.txt sleeper:/tmp/local-file.txt
docker cp 适合临时诊断,不是配置发布和数据备份的首选方法。正式配置应通过镜像、挂载或配置管理系统交付。
7.7 停止、终止和删除
docker stop --time 20 sleeper
docker kill sleeper
docker rm sleeper
stop 给应用一个优雅退出窗口;kill 默认立即发送 SIGKILL。数据库和正在写文件的应用应优先正常停止,并让主进程正确处理终止信号。
7.8 小练习
启动一个后台 Alpine 容器,分别使用 ps、logs、inspect、exec、stats 查看它。记录主进程 PID、状态和退出码,再比较 stop 与 kill 后的结果。
8. 镜像管理与 Docker Hub
8.1 拉取和查看
docker pull nginx:alpine
docker image ls
docker image inspect nginx:alpine
docker image history nginx:alpine
显式 pull 可以提前发现网络、认证和架构问题。docker run 只会在本地缺少所需引用时自动拉取;本地已有同名 Tag 时不会因为远端更新就自动替换。
8.2 为本地镜像打标签
docker tag my-app:1.0 yourname/my-app:1.0
Tag 不会复制镜像层,只是增加一个引用。推送到私有仓库时通常要加仓库域名:
docker tag my-app:1.0 registry.example.com/team/my-app:1.0
8.3 登录与推送
交互式登录:
docker login
自动化环境不要把密码直接放进命令参数。优先使用短期令牌和标准输入:
printf '%s' "$REGISTRY_TOKEN" | docker login \
--username "$REGISTRY_USER" \
--password-stdin registry.example.com
推送:
docker push registry.example.com/team/my-app:1.0
docker logout registry.example.com
CI 中的令牌应放在受保护的秘密存储中,限制为所需仓库和最小权限,并设置轮换或过期时间。
8.4 选择基础镜像
优先考虑:
- Docker Official Image 或可信发布者维护的镜像。
- 与运行环境匹配的 CPU 架构。
- 明确、受支持的版本范围。
- 尽可能小但仍便于安全更新和排障的基础镜像。
- 能够定期重新构建,而不是把旧镜像永久保存为“稳定”。
alpine 体积小,但使用 musl libc,某些二进制依赖或 Python 包可能不兼容。slim 镜像通常在体积和兼容性之间更平衡。选择应基于实际依赖和测试,而不是只比较 MB 数。
8.5 删除与清理
docker image rm nginx:alpine
docker image prune
docker system df
docker image prune 默认清理悬空镜像。以下命令影响范围更大:
docker system prune -a --volumes
它可能删除未运行容器、未使用镜像、网络、构建缓存和卷。不要把它当作常规“修复 Docker”命令,更不要在未核对数据和回退方式时执行。
8.6 多架构镜像
同一个 Tag 可以指向多架构清单。查看:
docker buildx imagetools inspect nginx:alpine
构建多架构镜像通常使用 Buildx:
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag registry.example.com/team/my-app:1.0 \
--push .
这条命令会直接推送,执行前必须确认仓库、Tag、登录身份和构建上下文。
8.7 小练习
选择一个官方镜像,查看它支持哪些架构。比较两个不同 Tag 的体积和基础系统,并写下选择其中一个用于开发环境的理由。
9. Dockerfile 基础:把应用构建为镜像
Dockerfile 是构建镜像的声明式文件。它应该进入版本控制,让构建过程可以审查、重复和自动化。
9.1 一个最小示例
新建 app.py:
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
body = b"Hello from Docker!\n"
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
新建 Dockerfile:
# syntax=docker/dockerfile:1
FROM python:3.13-slim
WORKDIR /app
COPY app.py .
RUN useradd --system --uid 10001 appuser
USER appuser
EXPOSE 8000
CMD ["python", "app.py"]
构建和运行:
docker build --tag hello-docker:1.0 .
docker run --rm --publish 127.0.0.1:8000:8000 hello-docker:1.0
另开终端访问:
curl http://127.0.0.1:8000
9.2 常用指令
| 指令 | 用途 | 注意点 |
|---|---|---|
FROM |
指定基础镜像 | 尽量使用可信、明确版本 |
WORKDIR |
设置后续指令工作目录 | 使用绝对路径 |
COPY |
从构建上下文复制文件 | 不要复制密钥和无关文件 |
RUN |
构建时执行命令 | 结果进入镜像层 |
ENV |
设置镜像环境变量 | 不要写秘密 |
ARG |
构建参数 | 也不能用于安全隐藏秘密 |
USER |
设置运行用户 | 服务尽量非 root |
EXPOSE |
记录容器监听端口 | 不会自动发布到宿主机 |
CMD |
默认启动命令或参数 | 运行时可覆盖 |
ENTRYPOINT |
固定主程序入口 | 与 CMD 的组合要明确 |
HEALTHCHECK |
定义容器健康探测 | 探测应轻量、可靠 |
9.3 RUN、CMD 和 ENTRYPOINT
RUN在构建镜像时执行,例如安装依赖。CMD在容器启动时提供默认命令或默认参数。ENTRYPOINT常用于固定可执行程序,让用户传入参数。
优先使用 JSON 数组形式:
CMD ["python", "app.py"]
相比 shell 形式 CMD python app.py,exec 形式减少额外 shell,并让主进程更直接地接收终止信号。
9.4 构建上下文
命令末尾的点:
docker build -t hello-docker:1.0 .
表示当前目录是构建上下文。COPY 只能访问上下文中的文件。上下文过大会导致上传慢、缓存失效和秘密泄漏风险。
新建 .dockerignore:
.git
.venv
__pycache__/
*.py[cod]
.env
.env.*
!.env.example
*.log
即使 .dockerignore 排除了秘密,也不应把生产密钥长期放在项目目录中。
9.5 COPY 与 ADD
普通文件复制优先使用 COPY。ADD 具有自动解压本地 tar 等额外行为,只有明确需要这些语义时再使用。远程文件应在 RUN 中使用校验和明确的下载工具,便于验证来源和失败处理。
9.6 常见错误
- 把整个仓库先
COPY . .,导致改一行文档就让依赖安装缓存失效。 - 把
.git、本地虚拟环境、构建产物或.env复制进镜像。 - 使用 root 运行并开放不必要权限。
- 用
latest作为唯一版本策略。 - 在镜像中保存 SSH 私钥、云密钥或 Registry Token。
- 为了“容器不退出”而在应用后面添加无意义的
tail -f /dev/null。
9.7 小练习
修改响应文字并重新构建 hello-docker:1.1。同时保留 1.0 和 1.1 两个 Tag,分别运行在不同端口,确认镜像版本可以并存。
10. BuildKit、构建缓存、多阶段构建与镜像瘦身
10.1 缓存顺序
Docker 从上到下执行 Dockerfile。某一层输入发生变化后,后续层通常需要重新构建。以 Python 为例,应先复制依赖文件并安装依赖,再复制变化频繁的源码:
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN python -m pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
如果先 COPY . .,任何源码变化都可能让依赖安装层失去缓存。
查看详细构建过程:
docker build --progress=plain -t my-app:dev .
强制检查新基础镜像:
docker build --pull -t my-app:dev .
完全忽略构建缓存:
docker build --no-cache -t my-app:dev .
--pull 和 --no-cache 解决的问题不同:前者检查基础镜像更新,后者重新执行构建步骤。
10.2 多阶段构建
构建工具不一定要留在最终镜像中。下面先构建 Python wheel,再复制到运行阶段:
# syntax=docker/dockerfile:1
FROM python:3.13-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN python -m pip wheel --wheel-dir /wheels -r requirements.txt
FROM python:3.13-slim
WORKDIR /app
COPY --from=builder /wheels /wheels
COPY requirements.txt .
RUN python -m pip install \
--no-cache-dir \
--no-index \
--find-links=/wheels \
-r requirements.txt \
&& rm -rf /wheels
COPY . .
CMD ["python", "app.py"]
优势:
- 最终镜像不包含编译器和构建缓存。
- 构建阶段与运行阶段职责清楚。
- 通常能减少体积和攻击面。
10.3 BuildKit 缓存挂载
对支持的包管理器,可以使用构建缓存挂载:
RUN --mount=type=cache,target=/root/.cache/pip \
python -m pip install -r requirements.txt
缓存只帮助构建,不会自动进入最终镜像层。是否使用应结合 CI 构建器、缓存持久化方式和依赖锁定策略测试。
10.4 构建秘密
不要使用下面的方式传秘密:
ARG TOKEN
RUN some-command --token "$TOKEN"
构建参数和历史层可能泄露信息。BuildKit 支持 secret mount:
RUN --mount=type=secret,id=package_token \
some-command --token-file /run/secrets/package_token
构建命令通过受控来源提供秘密:
docker build --secret id=package_token,src=./temporary-token.txt .
临时文件仍需严格权限并在使用后安全处理;CI 中应直接连接秘密存储。
10.5 镜像瘦身原则
- 使用满足兼容性的较小可信基础镜像。
- 只安装运行所需包,避免推荐包和调试工具。
- 用多阶段构建移除编译器。
- 清理同一
RUN中产生且不再需要的包索引。 - 使用
.dockerignore缩小上下文。 - 不为了减少层数而写出难以维护的巨大命令。
- 定期重建并安装安全更新;小镜像不等于安全镜像。
查看体积:
docker image ls my-app
docker history my-app:dev
docker system df
10.6 小练习
分别用单阶段和多阶段 Dockerfile 构建同一个应用,比较镜像体积、层历史和重新构建时间。修改源码但不修改依赖,观察哪些层复用了缓存。
11. 容器运行配置:环境、端口、资源和健康检查
11.1 环境变量
docker run --rm \
--env APP_ENV=development \
--env PORT=8000 \
my-app:dev
也可以使用文件:
docker run --rm --env-file .env my-app:dev
.env 方便本地开发,但不是秘密管理系统。环境变量可被进程、调试工具或 docker inspect 看到。生产密码应通过平台秘密存储、受限文件或专用 Secret 机制注入。
11.2 覆盖默认命令
Dockerfile:
CMD ["python", "app.py"]
运行时覆盖:
docker run --rm my-app:dev python --version
如果镜像定义了 ENTRYPOINT,命令行内容通常作为参数传给它;可用 --entrypoint 临时覆盖,但应先理解镜像的设计。
11.3 EXPOSE 与 --publish
EXPOSE 8000 是镜像元数据,表达应用预计监听 8000;它不会让宿主机自动开放端口。真正的端口映射由运行参数完成:
docker run -p 127.0.0.1:8080:8000 my-app:dev
容器中的应用必须监听 0.0.0.0:8000,只监听容器内部的 127.0.0.1 时,端口映射通常无法从容器外访问。
11.4 CPU 和内存限制
docker run --rm \
--memory 256m \
--cpus 1.0 \
my-app:dev
没有限制的容器可能占用宿主机大量资源。限制过小又会造成 OOM、请求超时或性能异常。先监控真实使用,再设置合理的请求、限制和告警。
查看:
docker stats
docker inspect --format '{{.State.OOMKilled}}' <容器名>
11.5 重启策略
常见策略:
no:默认,不自动重启。on-failure[:次数]:异常退出时重启。unless-stopped:除非用户明确停止,否则在守护进程恢复后重启。always:总是尝试重启。
示例:
docker run -d \
--name web \
--restart unless-stopped \
my-app:dev
重启策略不能修复错误配置。若容器不断重启,应查看日志和退出码,而不是提高重试频率掩盖故障。
11.6 健康检查
“容器进程存在”不代表“服务可用”。健康检查可以探测关键接口:
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2)"]
查看状态:
docker ps
docker inspect --format '{{json .State.Health}}' <容器名>
健康检查应:
- 快速且不会修改业务数据。
- 设置超时,避免检查进程堆积。
- 检查服务真正依赖的最小关键路径。
- 区分“进程活着”和“可以接收流量”。
11.7 其他常用参数
docker run --rm \
--hostname demo-host \
--workdir /app \
--user 10001:10001 \
--read-only \
--tmpfs /tmp \
--init \
my-app:dev
--read-only将容器根文件系统设为只读。--tmpfs为确实需要写入的临时目录提供内存文件系统。--init使用轻量 init 转发信号并回收孤儿进程。--user在运行时覆盖用户,需要同时处理文件权限。
11.8 小练习
为前面的 Python HTTP 服务增加 /health,配置健康检查、128 MB 内存限制和 unless-stopped。观察健康状态从 starting 变为 healthy。
12. 数据持久化:volume、bind mount 与 tmpfs
12.1 为什么不能依赖容器可写层
容器应该可以被停止、删除并重新创建。数据库、上传文件和需要长期保存的数据必须放在容器生命周期之外。
Docker 常用三种挂载:
| 类型 | 数据位置 | 适用场景 | 主要风险 |
|---|---|---|---|
| Named volume | Docker 管理 | 数据库、应用持久数据 | 容易因错误清理命令被删除 |
| Bind mount | 指定宿主路径 | 本地源码、明确的宿主配置 | 路径、权限和跨平台差异 |
| tmpfs | 宿主内存 | 临时缓存、短期敏感文件 | 容器停止或主机重启即丢失 |
12.2 Named volume
创建并查看:
docker volume create app-data
docker volume ls
docker volume inspect app-data
挂载:
docker run --rm \
--mount type=volume,source=app-data,target=/data \
alpine:latest \
sh -c 'date > /data/created-at.txt && cat /data/created-at.txt'
再启动一个新容器读取:
docker run --rm \
--mount type=volume,source=app-data,target=/data,readonly \
alpine:latest \
cat /data/created-at.txt
即使第一个容器已经删除,volume 仍然存在。
12.3 Bind mount
Linux/macOS:
docker run --rm \
--mount type=bind,source="$PWD",target=/workspace \
alpine:latest \
ls -la /workspace
PowerShell:
docker run --rm `
--mount "type=bind,source=$($PWD.Path),target=/workspace" `
alpine:latest `
ls -la /workspace
Bind mount 让容器直接访问宿主路径。若容器以 root 运行并获得可写挂载,它可能修改或删除宿主文件。只需读取时加 readonly:
--mount type=bind,source="$PWD/config",target=/app/config,readonly
12.4 tmpfs
docker run --rm \
--tmpfs /run/secrets:rw,noexec,nosuid,size=1m \
alpine:latest \
sh -c 'echo temporary > /run/secrets/demo && cat /run/secrets/demo'
tmpfs 不写入镜像层或宿主磁盘的普通目录,但内存、交换分区和宿主安全策略仍需评估。它不能替代完整的秘密管理方案。
12.5 备份 volume
以下示例把 app-data 打包到当前目录:
docker run --rm \
--mount type=volume,source=app-data,target=/data,readonly \
--mount type=bind,source="$PWD",target=/backup \
alpine:latest \
tar -czf /backup/app-data.tar.gz -C /data .
恢复到一个新 volume:
docker volume create app-data-restored
docker run --rm \
--mount type=volume,source=app-data-restored,target=/data \
--mount type=bind,source="$PWD",target=/backup,readonly \
alpine:latest \
tar -xzf /backup/app-data.tar.gz -C /data
对数据库直接复制正在变化的数据文件可能得到不一致备份。应优先使用数据库原生备份工具、事务快照或停写窗口,并定期做恢复演练。只有“成功恢复并验证”的备份才值得信任。
12.6 删除数据
docker volume rm app-data
会删除 volume 及其中数据。Compose 的:
docker compose down --volumes
同样会删除项目声明的 named volumes。执行前必须明确是否真的要清空数据库。
12.7 小练习
在 named volume 中写入文件,删除容器后用新容器读取。完成一次备份和恢复,并比较原 volume 与恢复 volume 中的文件校验和。
13. Docker 网络:bridge、DNS、端口与隔离
13.1 容器为什么能访问网络
Docker Engine 默认创建 bridge 网络。容器通常可以主动访问外部网络,但外部无法直接访问容器端口,除非发布端口或使用其他网络模式。
查看:
docker network ls
docker network inspect bridge
13.2 使用自定义 bridge
docker network create learning-net
docker run -d \
--name learning-redis \
--network learning-net \
redis:8-alpine
docker run --rm \
--network learning-net \
redis:8-alpine \
redis-cli -h learning-redis ping
预期返回:
PONG
同一自定义网络中的容器可以通过容器名解析和通信。不要把容器 IP 写进配置,因为容器重建后 IP 可能变化;使用服务名或受管理的 DNS 名称。
清理:
docker rm -f learning-redis
docker network rm learning-net
13.3 容器间通信不需要发布端口
如果 Web 容器和 Redis 容器在同一网络:
Web -> redis:6379
不需要把 Redis 的 6379 发布到宿主机。只有宿主机或网络外部的客户端需要访问时才发布。减少端口暴露可以降低误访问和攻击面。
13.4 127.0.0.1、0.0.0.0 与容器名
- 容器内的
127.0.0.1指当前容器自己,不是宿主机,也不是其他容器。 - 应用若要接收来自容器外的连接,通常监听容器内
0.0.0.0。 - 容器访问同网络服务时使用服务名,例如
redis:6379。 - 宿主机访问容器时使用发布的宿主机地址和端口,例如
127.0.0.1:8000。
13.5 其他网络模式
bridge:单机最常用,提供网络隔离和端口发布。host:容器直接使用宿主网络栈,隔离更少,跨平台行为不同。none:不给容器常规网络连接。overlay:用于多节点环境,例如 Swarm。macvlan、ipvlan:面向特定网络集成需求,配置前需理解物理网络限制。
初学和大多数单机项目优先使用自定义 bridge。
13.6 网络操作与排障
docker network inspect learning-net
docker network connect learning-net <容器名>
docker network disconnect learning-net <容器名>
docker port <容器名>
docker inspect --format '{{json .NetworkSettings.Networks}}' <容器名>
排障顺序:
- 服务进程是否真的监听预期端口。
- 两个容器是否在同一网络。
- 客户端是否使用服务名而不是
localhost。 - 端口是容器间访问还是需要发布给宿主机。
- 宿主防火墙、云安全组和反向代理是否允许连接。
Docker 的端口发布会修改宿主机防火墙规则。在部分 Linux 防火墙组合中,公开端口可能绕过预期的 ufw/firewalld 规则。正式主机应按 Docker 防火墙文档验证,而不是只看应用层配置。
13.7 小练习
创建一个自定义网络,运行 Redis 和一个临时 redis-cli 容器。先通过服务名得到 PONG,再把客户端放到另一个网络,观察失败并解释原因。
14. Docker Compose v2:管理多容器应用
14.1 Compose 解决什么问题
命令行参数越来越多时,手工 docker run 难以维护。Compose 使用 compose.yaml 声明:
- 有哪些服务。
- 使用镜像还是本地构建。
- 环境变量、端口和启动命令。
- 网络、卷、健康检查和依赖。
- 重启策略和资源配置。
Compose 让配置可审查和重复执行,但它不会自动把单机项目变成跨节点高可用平台。
14.2 最小 compose.yaml
services:
web:
image: nginx:alpine
ports:
- "127.0.0.1:8080:80"
启动:
docker compose config
docker compose up -d
docker compose ps
docker compose logs web
docker compose down
Compose 会根据项目名创建资源。默认项目名通常来自目录名,也可使用顶层 name、--project-name 或环境变量设置。
14.3 常用命令
docker compose config
docker compose build
docker compose pull
docker compose up -d --build
docker compose ps
docker compose logs -f --tail 100
docker compose exec web sh
docker compose run --rm web <一次性命令>
docker compose stop
docker compose start
docker compose restart web
docker compose down
区别:
exec:在已经运行的服务容器中执行命令。run:为某个服务创建一次性容器,默认不发布服务端口。stop:停止但保留容器。down:删除 Compose 创建的容器和默认网络。down -v:额外删除 named volumes,可能清空数据库。
14.4 服务发现
Compose 默认给项目创建网络。服务可以使用服务名通信:
services:
web:
environment:
REDIS_HOST: redis
redis:
image: redis:8-alpine
Web 应使用 redis:6379,而不是把 Redis 地址写成 127.0.0.1。
14.5 depends_on 与健康检查
普通 depends_on 控制创建和启动顺序,不保证依赖服务已经可以处理请求。需要等待健康状态时:
services:
web:
depends_on:
redis:
condition: service_healthy
redis:
image: redis:8-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
应用自身仍应实现连接超时和有限重试,因为运行期间依赖服务也可能重启。
14.6 变量插值和容器环境变量
Compose 可以从 shell 或项目 .env 文件读取插值变量:
services:
web:
image: "example/web:${IMAGE_TAG:-dev}"
这决定 Compose 配置中的值。容器环境变量则通过 environment 或 env_file 提供:
services:
web:
environment:
APP_ENV: development
env_file:
- .env.runtime
两者不要混淆。查看最终配置:
docker compose config
注意:该输出可能展开环境变量,不要把包含秘密的完整结果上传到公开日志。
14.7 多个 Compose 文件和 Profiles
开发环境可以叠加配置:
docker compose \
-f compose.yaml \
-f compose.dev.yaml \
up -d
后面的文件按 Compose 规则合并或覆盖前面的配置。路径通常以第一个 Compose 文件为基准,合并数组的行为也可能与直觉不同,必须用 docker compose config 验证。
可选服务可以使用 profiles:
services:
debug:
image: busybox
profiles: ["debug"]
启用:
docker compose --profile debug up
14.8 小练习
把前面的两个 docker run 实验改写为 Compose:一个 Nginx 服务、一个 Redis 服务、一个 named volume 和一个自定义网络。执行 config、up、logs、exec、down,观察资源名称。
15. 综合项目:Flask + Redis + Compose
这个项目包含:
web:Flask API,由 Gunicorn 运行。redis:保存页面访问计数。- Compose 默认网络:让
web通过服务名redis连接。 redis-data:让计数在容器重建后继续存在。- 健康检查:区分容器启动与服务可用。
- 非 root Web 进程、只读根文件系统和最小能力集。
15.1 项目结构
docker-study-demo/
├── app.py
├── compose.yaml
├── Dockerfile
├── requirements.txt
├── .dockerignore
└── .env.example
15.2 app.py
import os
from flask import Flask, jsonify
from redis import Redis
from redis.exceptions import RedisError
app = Flask(__name__)
redis_client = Redis(
host=os.getenv("REDIS_HOST", "redis"),
port=int(os.getenv("REDIS_PORT", "6379")),
decode_responses=True,
socket_connect_timeout=2,
socket_timeout=2,
)
@app.get("/")
def index():
try:
visits = redis_client.incr("visits")
except RedisError:
return jsonify(message="Redis 暂时不可用"), 503
return jsonify(message="Hello from Docker!", visits=visits)
@app.get("/health")
def health():
try:
redis_client.ping()
except RedisError:
return jsonify(status="unhealthy"), 503
return jsonify(status="ok")
关键点:
- Redis 主机名默认是 Compose 服务名
redis。 - 设置连接和读取超时,避免依赖失效时请求永久等待。
/health同时检查 Web 和 Redis 连接。- 业务接口在 Redis 不可用时返回明确的 503。
15.3 requirements.txt
Flask>=3.1,<4
gunicorn>=23,<24
redis>=6,<7
范围约束便于教程在兼容版本内安装。正式项目应通过锁文件或带哈希的依赖清单固定经过测试的准确版本,并由自动化工具定期提出升级。
15.4 Dockerfile
# syntax=docker/dockerfile:1
FROM python:3.13-slim AS builder
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /build
COPY requirements.txt .
RUN python -m pip wheel --wheel-dir /wheels -r requirements.txt
FROM python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
RUN groupadd --system app \
&& useradd --system --gid app --uid 10001 --home-dir /app app
WORKDIR /app
COPY --from=builder /wheels /wheels
COPY requirements.txt .
RUN python -m pip install \
--no-cache-dir \
--no-index \
--find-links=/wheels \
-r requirements.txt \
&& rm -rf /wheels
COPY --chown=app:app app.py .
USER app
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "--access-logfile", "-", "app:app"]
15.5 .dockerignore
.git
.venv
__pycache__/
*.py[cod]
.env
.env.*
!.env.example
*.log
15.6 .env.example
WEB_PORT=8000
复制为本地 .env 后可调整宿主机端口。示例文件不能包含真实密码。
15.7 compose.yaml
name: docker-study-demo
services:
web:
build:
context: .
ports:
- "127.0.0.1:${WEB_PORT:-8000}:8000"
environment:
REDIS_HOST: redis
REDIS_PORT: "6379"
depends_on:
redis:
condition: service_healthy
healthcheck:
test:
- CMD
- python
- -c
- >-
import urllib.request;
urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2)
interval: 10s
timeout: 3s
retries: 5
start_period: 10s
restart: unless-stopped
read_only: true
tmpfs:
- /tmp:size=16m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
redis:
image: redis:8-alpine
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
volumes:
redis-data:
没有显式声明网络时,Compose 会创建项目默认网络。web 和 redis 都加入该网络,因此 web 可以解析服务名 redis。Redis 没有发布端口,宿主机和公网不能直接通过 6379 访问它。
15.8 构建与启动
先渲染并检查最终配置:
docker compose config
构建和启动:
docker compose up -d --build
docker compose ps
docker compose logs -f --tail 100
访问:
curl http://127.0.0.1:8000/
curl http://127.0.0.1:8000/health
多次访问 /,visits 应逐渐增加。
15.9 验证网络、数据和进程
docker compose exec web id
docker compose exec web python -c \
"import os; print(os.environ['REDIS_HOST'])"
docker compose exec redis redis-cli GET visits
docker compose top
删除并重新创建服务容器,但保留 volume:
docker compose down
docker compose up -d
curl http://127.0.0.1:8000/
计数应继续存在,因为 redis-data 没有被 down 删除。
15.10 修改和更新
修改 app.py 后:
docker compose build web
docker compose up -d web
docker compose ps
docker compose logs --tail 100 web
Compose 会按配置重新创建需要更新的容器。正式环境应使用 Registry 中经过 CI 测试的不可变镜像,而不是在生产主机现场构建。
15.11 故障实验
停止 Redis:
docker compose stop redis
curl -i http://127.0.0.1:8000/
docker compose ps
应看到 Web 返回 503,健康状态随后变为不健康。恢复:
docker compose start redis
docker compose ps
curl http://127.0.0.1:8000/health
这说明 depends_on 主要处理启动阶段;应用运行期间仍必须正确处理依赖暂时不可用。
15.12 清理
保留数据:
docker compose down
删除项目数据,会清空访问计数:
docker compose down --volumes
执行第二条命令前,先确认项目名和 volume:
docker compose ls
docker volume ls
15.13 进一步改进
- 为依赖生成锁文件并固定哈希。
- 把 Web 镜像推送到 Registry,并在 Compose 中使用摘要。
- 在 Web 前增加反向代理和 TLS 终止。
- 使用 Redis 原生备份策略并做恢复演练。
- 将日志发送到集中式系统。
- 配置资源限制、监控、告警和部署回滚。
16. 日志、监控、调试与高频故障排查
16.1 固定排障顺序
遇到问题时按层检查,避免一上来就删除所有资源:
- 客户端能否连接 Engine:
docker version。 - 资源是否足够:磁盘、内存、CPU、inode。
- 容器状态:
docker ps -a。 - 退出原因和日志:
docker logs、inspect。 - 最终配置:
docker compose config。 - 端口和网络:监听地址、发布端口、服务名、网络成员。
- 挂载和权限:宿主路径、UID/GID、只读设置。
- 应用依赖:数据库、Redis、DNS、证书和外部 API。
16.2 常用诊断命令
docker version
docker info
docker ps -a
docker logs --tail 200 <容器名>
docker inspect <容器名>
docker stats --no-stream
docker top <容器名>
docker events --since 10m
docker system df
docker network ls
docker volume ls
docker compose config
docker compose ps
docker compose logs --tail 200
16.3 容器不断退出
检查:
docker inspect --format \
'status={{.State.Status}} exit={{.State.ExitCode}} error={{.State.Error}} oom={{.State.OOMKilled}}' \
<容器名>
docker logs <容器名>
常见原因:
- 启动命令写错或文件不存在。
- 应用只做一次性任务,正常执行完毕。
- 必需环境变量缺失。
- 配置文件路径错误或没有读取权限。
- 内存不足被 OOM Kill。
- 主进程转到后台,前台进程退出。
- CPU 架构不匹配导致
exec format error。
16.4 端口无法访问
检查:
docker port <容器名>
docker ps
docker logs <容器名>
重点确认:
- 应用在容器内监听
0.0.0.0,而非只监听容器的127.0.0.1。 - 映射方向是“宿主端口:容器端口”。
- 宿主机端口未被占用。
- 绑定
127.0.0.1的端口只能从宿主机本地访问。 - 云安全组、防火墙、反向代理和 TLS 配置是否正确。
16.5 容器之间无法连接
- 不要把其他容器地址写成
localhost。 - 使用 Compose 服务名或自定义网络中的容器名。
- 检查两个容器是否加入同一网络。
- 检查目标进程监听端口。
- 检查应用是否把宿主映射端口误当成容器间端口。
docker network inspect <网络名>
docker inspect --format '{{json .NetworkSettings.Networks}}' <容器名>
16.6 Bind mount 权限错误
容器用户的 UID/GID 与宿主文件权限不匹配时会出现 Permission denied。解决方向:
- 明确容器实际用户:
docker compose exec web id。 - 只读配置使用
readonly。 - 构建时用
COPY --chown设置镜像内文件所有权。 - 对共享开发目录制定一致 UID/GID 方案。
- 不要用
chmod 777作为默认答案。
启用 SELinux 的主机还需按平台规则设置挂载标签,不能通过关闭安全机制长期绕过。
16.7 磁盘空间不足
docker system df
docker image ls
docker ps -a
docker volume ls
先识别大对象和数据归属,再有目标地删除。数据库 volume、构建缓存和未推送镜像可能都很重要。生产环境应配置磁盘监控、日志轮转和明确的保留策略,而不是定期盲跑全局 prune。
16.8 构建结果像是旧代码
- 确认编辑的是构建上下文中的文件。
- 检查
.dockerignore是否误排除文件。 - 用
docker build --progress=plain观察缓存命中。 - 确认运行容器使用的新镜像 ID。
- 必要时针对性使用
--no-cache,不要把它当作永久方案。
docker compose build --no-cache web
docker compose up -d web
docker compose images
16.9 Compose 变量异常
docker compose config
docker compose config --environment
检查当前目录、使用的 Compose 文件、项目 .env、shell 环境和 --env-file。输出可能包含敏感值,分享前必须脱敏。
16.10 小练习
故意制造并修复三个错误:占用 Web 端口、把 Redis 主机名改成 localhost、把 Web 内存限制设得过小。每次只根据状态、日志和配置定位,不使用全局清理命令。
17. 安全、CI/CD 与单机生产实践
17.1 先理解信任边界
容器提升了隔离和交付一致性,但安全仍依赖:
- 宿主机内核、Docker Engine 和运行时是否及时更新。
- 镜像来源和依赖是否可信。
- 容器获得了哪些用户、能力、设备、挂载和网络权限。
- Docker Socket 和 Registry 凭据是否得到保护。
- 数据、备份、日志和秘密是否正确管理。
17.2 最小权限
Dockerfile 中使用非 root 用户:
RUN useradd --system --uid 10001 appuser
USER appuser
运行时可进一步限制:
services:
web:
read_only: true
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
如果应用确实需要某项 Linux capability,再逐项添加并记录理由。不要默认使用:
--privileged
--privileged 几乎移除容器的常规设备和能力限制,只应在充分理解风险、无法采用更窄权限且经过评审时使用。
17.3 保护 Docker Socket
不要在普通 Web 应用中挂载:
/var/run/docker.sock
拿到 Socket 的容器通常可以控制宿主 Docker,风险接近宿主 root。需要构建或运维自动化时,优先使用隔离构建器、受限代理、短期凭据和明确授权。
17.4 镜像与依赖
- 使用可信基础镜像。
- 固定经过测试的版本;生产发布记录摘要。
- 定期用
--pull重新构建,吸收基础镜像安全更新。 - 删除不需要的软件包和调试工具。
- 在 CI 中执行依赖审计、镜像漏洞扫描和策略检查。
- 扫描结果要结合可利用性、运行配置和修复版本评估,不能只看数量。
- 为镜像生成软件物料清单并保存构建来源信息。
17.5 秘密管理
不要把秘密放在:
- Dockerfile 的
ENV或ARG。 - 镜像层、Git 仓库或
.env.example。 - Compose 文件明文。
- 命令行参数和公开 CI 日志。
环境变量也可能通过 inspect、进程环境或错误日志暴露。正式环境优先使用平台 Secret、权限受限的临时文件或专用秘密管理系统,并制定最小权限、轮换和撤销流程。
17.6 网络安全
- 只发布确实需要从宿主机外访问的端口。
- 本地工具默认绑定
127.0.0.1。 - 数据库和 Redis 等内部服务只加入内部网络,不发布端口。
- 公开服务前放置受维护的反向代理或负载均衡器,统一 TLS、请求限制和安全头。
- 同时检查 Docker 防火墙规则、宿主防火墙和云安全组。
17.7 资源、日志和可观测性
生产环境至少应具备:
- CPU、内存、PID、磁盘和日志大小限制。
- 健康检查、就绪判断和依赖超时。
- 标准输出日志或受管理的日志驱动。
- 主机和应用指标、告警与容量趋势。
- Docker 日志轮转,避免单个 JSON 日志无限增长。
- 明确的维护窗口和升级流程。
17.8 数据和备份
- 数据写入受管理的 volume 或外部数据服务。
- 数据库使用原生一致性备份。
- 备份加密、限制访问并保留异地副本。
- 定期恢复到隔离环境,验证可读性和业务完整性。
- 删除 volume、迁移存储或升级数据库前建立可验证恢复点。
17.9 推荐的 CI/CD 流程
一个稳健的基本流程:
- 检出确定的 Git 提交。
- 运行格式检查、静态分析和测试。
- 使用 Dockerfile 构建一次镜像。
- 扫描依赖和镜像,生成物料清单与来源证明。
- 推送带提交号/版本号的镜像到 Registry。
- 记录 Registry 返回的不可变摘要。
- 在测试环境部署同一摘要并执行烟测。
- 获得批准后把同一摘要部署到生产。
- 检查健康状态、日志和关键业务指标。
- 失败时回到上一已验证摘要,不在服务器上临时修改容器。
核心原则是 build once, promote the same artifact:不要在测试和生产分别重新构建“同一个版本”。
17.10 Compose 用于生产时
Compose 可以用于明确边界内的单机部署,但要自行补齐:
- 镜像构建和 Registry。
- TLS、入口流量与防火墙。
- 备份、监控、告警和日志。
- 主机故障恢复。
- 滚动更新和回滚流程。
- 密钥、权限和配置管理。
它不提供跨主机自动调度和完整高可用控制平面。如果业务需要多节点容错、自动调度和大规模滚动发布,应评估 Swarm、Kubernetes、Nomad 或云托管容器平台。
17.11 上线检查清单
- 镜像来自可信构建,版本和摘要已记录。
- 容器使用非 root,未使用无理由的
privileged或 Docker Socket。 - 只开放必要端口,内部数据服务不直接暴露。
- 秘密不在镜像、仓库、Compose 明文和日志中。
- 健康检查、超时、重启策略和优雅终止已验证。
- 资源限制、日志轮转、监控和告警已配置。
- 数据卷、数据库备份和恢复演练已完成。
- 更新、烟测和回滚命令已经过测试。
17.12 小练习
审查综合项目:确认 Web 进程不是 root、根文件系统只读、没有额外 capability、Redis 没有发布端口。再写出如果新镜像健康检查失败时的回滚步骤。
18. 命令速查、练习验收与后续路线
18.1 容器速查
docker run --rm IMAGE COMMAND
docker run -d --name NAME IMAGE
docker ps
docker ps -a
docker logs -f --tail 100 NAME
docker exec -it NAME sh
docker inspect NAME
docker stats --no-stream
docker stop NAME
docker rm NAME
18.2 镜像速查
docker pull IMAGE:TAG
docker image ls
docker image inspect IMAGE:TAG
docker history IMAGE:TAG
docker build -t IMAGE:TAG .
docker tag SOURCE TARGET
docker push TARGET
docker image rm IMAGE:TAG
docker system df
18.3 网络和卷速查
docker network ls
docker network create NAME
docker network inspect NAME
docker volume ls
docker volume create NAME
docker volume inspect NAME
18.4 Compose 速查
docker compose config
docker compose build
docker compose pull
docker compose up -d --build
docker compose ps
docker compose logs -f --tail 100
docker compose exec SERVICE COMMAND
docker compose run --rm SERVICE COMMAND
docker compose stop
docker compose down
只有确定要删除项目持久数据时才执行:
docker compose down --volumes
18.5 完整练习任务
- 安装 Docker,并保存
docker version、docker info和docker compose version的环境基线。 - 运行两个 Nginx 容器,分别绑定不同的本地端口。
- 编写一个非 root Dockerfile,并使用
.dockerignore。 - 比较单阶段和多阶段镜像的体积、层与缓存。
- 使用 named volume 保存数据,完成备份和恢复。
- 使用自定义网络连接 Redis 与临时客户端,不发布 Redis 端口。
- 独立完成 Flask + Redis 综合项目。
- 制造端口冲突、错误服务名和依赖停止三种故障,并按固定顺序排查。
- 把镜像推送到自己的测试仓库,记录 Tag 和摘要的区别。
- 为项目写一份上线检查、备份、烟测和回滚清单。
18.6 学习验收
如果不看本文,你应该能够回答:
- 为什么删除容器不应该删除数据库数据?
EXPOSE 8000和-p 8000:8000有什么区别?- 为什么容器 A 不能用
localhost访问容器 B? - Tag 和 digest 的差异是什么?
- Dockerfile 中为什么先复制依赖清单再复制源码?
docker compose down与down -v有什么不同?- 为什么
depends_on不能代替应用的重试和超时? - 为什么 Docker Socket、
docker用户组和--privileged风险很高? - 如何证明备份真的可用?
- 如何把同一个经过测试的镜像安全推广到生产?
18.7 Swarm 与 Kubernetes 的定位
掌握本文后,再根据需求选择:
- Docker Swarm:与 Docker Engine 集成紧密,概念相对少,提供服务、overlay 网络和滚动更新。
- Kubernetes:生态广、控制面和资源模型更完整,学习与运维成本也更高。
- 云托管容器平台:减少控制面维护,但需要理解云网络、身份、计费和平台约束。
不要因为“流行”就过早引入编排平台。单机开发、CI 任务和小型内部服务可能只需要 Docker + Compose;当确实需要多节点调度、自愈、弹性和标准化发布时,再进入编排学习。
18.8 官方资料
- Docker Get Started
- Docker Engine 安装
- Docker CLI Reference
- Dockerfile Reference
- Docker 构建最佳实践
- Docker Storage
- Docker Networking
- Docker Compose
- Compose Specification
- Docker Engine Security
- Docker Rootless mode
Docker 变化很快。学习具体参数时优先查官方文档;升级 Engine、Desktop、Compose 或基础镜像后,重新运行项目测试、备份恢复和上线烟测,而不是假定旧行为永远不变。