引言
开发一个应用时,经常会遇到这样的情况:代码在自己的电脑上可以运行,换到另一台机器后,可能因为操作系统、运行时版本、系统库或配置不同而启动失败。
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 的工作架构
|
|
日常输入的 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:
|
|
docker version 只显示客户端信息或提示无法连接 daemon 时,需要先启动 Docker 服务或 Docker Desktop。
Docker 常用命令
Docker 命令大多采用 docker <对象> <操作> 的结构,例如 docker image ls 和 docker container rm。Docker 同时保留了 docker images、docker 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 是私有仓库地址占位值,登录和推送前需要替换为自己的仓库与命名空间。
|
|
标签是镜像的可读引用。latest 只是一个普通且可变的标签,不保证它一定是最新或最稳定版本。生产环境应固定经过测试的版本标签;需要严格复现时,可以进一步固定镜像 digest,同时建立主动升级和漏洞修复流程。
docker search 只搜索 Docker Hub 仓库,不会列出私有仓库内容或完整标签。选镜像时仍需核对发布者、具体标签、支持架构和更新时间。
镜像离线保存与载入
无法直接访问镜像仓库时,可以把镜像及其配置、标签和镜像层保存为归档文件:
|
|
docker save/load 不包含容器可写层和数据卷。docker export/import 导出的是容器文件系统,会丢失镜像层、构建历史和部分配置,不适合作为常规镜像迁移方案。
创建并运行容器
下面启动一个 Nginx 容器,并只允许从宿主机本地访问:
|
|
访问 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 自身的选项必须放在镜像名之前。下面两行只用于对比参数位置,错误示例不要执行:
|
|
查看与管理容器
|
|
docker run 会基于镜像创建一个新容器,而 docker start 只是重新启动已经存在的容器。docker pull 也不会自动更新正在运行的容器;更新镜像后,需要用新镜像重新创建容器。
日志与排障
下面用 CONTAINER 表示实际的容器名称或 ID,执行时需要替换。
|
|
精简镜像不一定包含 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 logs 和 docker exec。docker commit 会把人工修改固化成难以复现的镜像,应优先维护 Dockerfile;容器文件系统迁移也优先使用镜像的 save/load,而不是会丢失镜像历史与配置的 export/import。
资源限制
容器默认可能使用宿主机的大部分资源。运行不可信或容易突发占用的服务时,应设置边界。下面的 example/app:1.0 是占位镜像名,使用时需要替换:
|
|
具体限制是否生效取决于宿主系统和 Docker 环境,可以通过 docker inspect limited-app 与 docker stats limited-app 验证。
运行中的容器也可以调整部分资源限制:
|
|
Compose 管理的容器应优先修改 compose.yaml 后重新创建,避免运行状态与声明配置不一致。
查看占用与清理
|
|
如需把未使用的命名数据卷也纳入清理,使用 docker volume prune --all。清理命令会要求确认,但仍应先检查删除列表。下面的命令删除范围很大,可能同时清除未使用镜像、构建缓存和匿名数据卷,不应作为日常无脑清理命令:
|
|
Dockerfile
Dockerfile 是构建镜像的声明式文件。Docker 从上到下执行指令,并将文件变更组织为可复用的镜像层。
构建上下文
执行下面的命令时,最后的 . 表示构建上下文:
|
|
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 示例
目录结构如下:
|
|
go.mod:
|
|
main.go:
|
|
Dockerfile:
|
|
.dockerignore:
|
|
构建并运行:
|
|
为什么使用多阶段构建
上面的 Dockerfile 包含两个阶段:
|
|
最终镜像不会包含 Go 编译器、模块缓存和源代码,因此镜像更小,攻击面也更少。多阶段构建同样适用于前端构建、Java 打包和原生程序编译。
镜像层与构建缓存
Docker 会复用没有变化的构建层。通常应先复制变化较少的依赖清单并安装依赖,再复制频繁变化的源代码:
|
|
BuildKit 还支持缓存挂载。例如 Go 模块与编译缓存可以写成:
|
|
这些缓存用于加速后续构建,不会自动进入最终镜像。
Dockerfile 最佳实践
- 使用可信、尽量精简的基础镜像,并定期更新和扫描漏洞。
- 最终阶段使用非 root 用户,只复制运行所需的文件。
- 使用 JSON exec form 的
ENTRYPOINT和CMD,让信号直接传给应用进程。 - 使用
.dockerignore缩小构建上下文,避免把密钥和无关文件送入构建器。 - 不要用
ARG、ENV或“复制后删除”的方式保存密钥;密钥可能留在镜像配置或历史层中。
需要在构建过程中临时使用私有仓库凭据时,可以使用 BuildKit secret mount。下面以私有 Go 模块所需的 .netrc 为例:
|
|
|
|
数据卷
容器的可写层适合保存临时运行状态,但容器被删除时,这一层也会被删除。数据库文件、上传文件和需要跨容器复用的数据应放在容器可写层之外。
Docker 主要提供三种挂载方式:
| 类型 | 数据位置 | 生命周期 | 适用场景 |
|---|---|---|---|
| 命名数据卷(named volume) | Docker 管理 | 独立于容器 | 数据库、应用持久化数据 |
| 绑定挂载(bind mount) | 指定宿主机路径 | 由宿主机文件决定 | 源代码、配置文件、开发环境 |
| tmpfs | 宿主机内存 | 容器停止后消失 | 临时文件、敏感的短期数据 |
命名数据卷
|
|
绑定挂载
绑定挂载把宿主机文件或目录直接映射到容器中:
|
|
绑定挂载适合开发时同步源码,也适合以只读方式提供配置。它依赖宿主机路径和权限,可移植性弱于命名数据卷。在 Docker Desktop 中,宿主机目录还需要处于允许共享的路径范围内。
--mount 语义明确,适合脚本和文档。使用 -v 宿主路径:容器路径 时,如果宿主路径拼错,Docker 可能自动创建一个目录,从而掩盖错误。
tmpfs
|
|
tmpfs 数据不会写入容器可写层,容器停止后即消失。它适合缓存、临时文件和不需要持久化的短期敏感数据,但仍可能受到宿主机交换空间等系统配置影响。
挂载时的注意事项
- 挂载会遮蔽容器目标目录中原有的文件,卸载后原文件才会重新可见。
- 绑定挂载需要处理宿主机与容器之间的 UID、GID 和文件权限。
- 配置、证书等只读内容应使用
readonly或:ro,减少误写风险。 - 命名数据卷不是备份;误删除、应用错误和数据损坏仍会影响卷内数据。
docker volume prune和docker compose down -v都可能永久删除数据,执行前应确认备份。
备份与恢复数据卷
下面先暂停应用写入,再创建专用备份目录并执行备份,避免给临时容器整个项目目录的写权限。示例使用固定文件名,重复执行会覆盖原文件;正式备份应使用带时间戳的唯一文件名:
|
|
恢复时先创建一个新卷,避免覆盖原数据:
|
|
恢复后应启动临时容器或应用执行完整性校验。数据库通常还应优先使用数据库自身的逻辑备份工具,例如 pg_dump,而不是只复制正在写入的数据文件。
完成备份和恢复验证后,再清理实验数据卷:
|
|
Docker 网络
容器拥有独立的网络命名空间。Docker 网络负责连接容器、分配地址、提供 DNS 服务发现,并限定容器之间以及容器与外部网络之间的通信范围。
常见网络驱动
| 驱动 | 特点 | 常见用途 |
|---|---|---|
bridge |
单台 Docker 主机上的虚拟网络 | 本地服务、单机部署 |
host |
容器直接使用宿主机网络栈 | 特定 Linux 性能或端口场景 |
none |
只保留回环接口 | 完全不需要网络的任务 |
overlay |
跨多台 Docker 主机连接服务 | Docker Swarm |
macvlan |
为容器分配局域网中的独立 MAC/IP | 需要直接接入物理网络的场景 |
日常单机使用时,优先创建用户自定义桥接网络(user-defined bridge network),而不是把所有容器都放进默认 bridge。用户自定义网络具有更清晰的隔离边界,并提供基于容器名和网络别名的 DNS 解析。它不会自动提供服务认证、细粒度授权或流量加密。
创建自定义网络
|
|
如果返回 PONG,说明 Docker DNS 已将名称 redis 解析到对应容器。对于另一个正在运行的容器,可以将下面的 CONTAINER 替换为它的名称,动态接入或断开网络:
|
|
完成实验后清理:
|
|
端口映射
同一 Docker 网络中的容器可以直接访问对方的容器端口,不需要发布到宿主机。只有宿主机或外部客户端需要访问服务时,才使用 -p:
|
|
下面两种发布方式按访问范围二选一。命令以前台方式运行,按 Ctrl+C 停止后,--rm 会删除实验容器:
|
|
排查已有容器的端口映射时,可以直接查询全部映射或指定容器端口:
|
|
应用进程通常需要监听容器内的 0.0.0.0,才能通过端口映射访问。如果只监听容器内的 127.0.0.1,外部请求通常无法进入该进程。
localhost 与服务发现
容器内的 localhost 只指向当前容器本身。应用容器访问数据库容器时,不应连接 localhost:5432,而应使用同一网络中的服务名:
|
|
容器访问宿主机服务时,Docker Desktop 通常可以使用 host.docker.internal。Linux Docker Engine 常常需要显式添加映射。下面的 example/app:1.0 需要替换为实际镜像:
|
|
--network host 的行为具有平台差异,不应作为通用跨平台方案。
Docker Compose
手动管理一个容器时,docker run 足够直接。但一个应用通常包含 Web 服务、数据库、缓存、消息队列等多个容器。如果每次都手写网络、数据卷、环境变量和启动顺序,命令会迅速变得难以维护。
Docker Compose 使用一个 compose.yaml 文件声明多容器应用:
|
|
现代 Compose 遵循 Compose Specification,顶层不再需要 version: "3"。
完整 Compose 示例
沿用前面的 Go 应用,再添加一个 Caddy 反向代理。目录结构如下:
|
|
Caddyfile:
|
|
compose.yaml:
|
|
这个配置体现了前面几节的核心概念:
app使用当前目录的 Dockerfile 构建镜像。proxy通过 Compose DNS 使用app:8080访问应用。- 只有
proxy向宿主机发布端口,app只在app-net中被其他服务访问。 Caddyfile通过只读绑定挂载提供,Caddy 状态保存在命名数据卷中。proxy等待app健康检查通过后再启动。
depends_on 和健康检查可以改善启动顺序,但不能替代应用自身的重试与故障恢复。依赖服务在运行中重启时,应用仍应能够重新连接。
Compose 常用命令
|
|
启动后访问:
|
|
修改 compose.yaml、环境变量或镜像后,docker compose restart 不会应用这些变化。应使用 docker compose up -d 让 Compose 按需重新创建容器;Dockerfile 或源码变化时再加 --build。
删除服务和命名数据卷时需要显式使用 -v/--volumes。这会删除 Compose 项目声明的非 external 命名数据卷和匿名卷,应在确认备份后执行:
|
|
环境变量
Compose 支持从 shell 或项目目录中的 .env 文件读取插值变量:
|
|
|
|
${VAR} 是 Compose 在解析 YAML 时进行的变量插值,而 environment 和 env_file 用于设置容器内部的环境变量,这几个概念不要混淆。
密码和令牌不应提交到 .env。本地开发可以使用不进 Git 的独立密钥文件,并通过 Compose secrets 以文件形式挂载:
|
|
应用可从 /run/secrets/db-password 读取内容。Compose secrets 改善了传递方式,但本地文件本身仍是明文,生产环境还需要专门的密钥管理系统和访问控制。
Compose 使用建议
- 文件优先命名为
compose.yaml,并提交一份不含真实密钥的默认配置。 - 不要无必要设置
container_name,否则容易产生项目间冲突,也会妨碍同一服务扩容。 - 服务之间使用服务名和容器端口通信,只有外部需要访问的服务才配置
ports。 - 配置健康检查,同时让应用对数据库、缓存等依赖实现超时和重试。
- 提交前运行
docker compose config,检查变量展开、端口和挂载路径是否符合预期。
一套完整的日常工作流
从修改代码到停止环境,可以按照下面的顺序操作:
|
|
发布镜像时,再增加版本标签和镜像仓库步骤。下面的 registry.example.com、团队名和凭据变量都是占位值,需要替换为自己的仓库信息:
|
|
多架构发布可以使用 Buildx:
|
|
执行多架构构建前,可以用 docker buildx inspect --bootstrap 检查 builder 支持的平台。包含跨架构 RUN 的 Dockerfile 需要对应架构的原生 worker,或预先配置 binfmt/QEMU 模拟;Docker Desktop 通常已经配置,普通 Linux Engine 不一定具备。使用非默认 builder 时,单平台本地构建通常需要 --load 才会把结果加载到本地镜像列表;多平台结果一般直接使用 --push 发布到仓库。
常见问题
容器启动后立即退出
容器的生命周期由主进程决定,主进程退出后容器就会停止。先查看状态、退出码和日志:
|
|
不要通过在后台运行一个无意义进程来“保活”,应修复入口命令、配置或应用本身。
端口映射后仍无法访问
依次检查:
- 容器是否仍在运行,应用日志是否报错。
-p的顺序是否为“宿主机端口:容器端口”。- 应用是否监听容器内的
0.0.0.0,而不是只监听127.0.0.1。 - Docker Desktop 的虚拟机、宿主防火墙或云安全组是否允许访问。
- 服务是否实际上监听了预期的容器端口。
容器之间无法连接
先确认两个容器位于同一用户自定义网络,再使用容器名、网络别名或 Compose 服务名连接。容器内的 localhost 不能指向另一个容器。
|
|
修改代码后镜像没有变化
检查构建上下文、.dockerignore 和 Dockerfile 的 COPY 路径,然后重新构建:
|
|
--no-cache 适合定位缓存问题,不必用于每次日常构建。Compose 环境中使用 docker compose up -d --build 更新镜像并重新创建需要变化的服务。
磁盘空间持续增长
先观察占用来源,再按对象类型清理:
|
|
不要直接跳到 docker system prune -a --volumes。匿名数据卷、未运行环境的镜像和构建缓存都可能仍有价值;未使用的命名数据卷只有在单独执行 docker volume prune --all 等命令时才会被纳入清理。
安全边界
容器带来隔离,但不会自动让应用安全。至少应遵守下面几条原则:
- 运行时使用非 root 用户,只授予应用真正需要的 Linux capabilities。
- 不把
--privileged、宿主机根目录或/var/run/docker.sock挂载当作普通方案。 - 只发布必要端口,开发数据库优先绑定到宿主机
127.0.0.1。 - 不在 Dockerfile、镜像层、Git、命令历史或日志中保存密码和令牌。
- 使用可信基础镜像,持续更新依赖、扫描漏洞,并设置 CPU、内存和进程数限制。
在 Linux 上,能访问 Docker 守护进程的用户通常可以获得接近宿主机 root 的能力,因此 docker 用户组和 Docker socket 都应按高权限入口管理。
小结
Docker 的核心不是记住大量命令,而是理解几个对象之间的关系:
|
|
- 镜像负责交付一致的应用和运行环境,容器负责运行镜像。
- Dockerfile 把手工安装过程变成可审查、可重复的构建过程。
- 数据卷让重要数据脱离容器的可写层,网络让服务在指定范围内互相发现和通信。
- Docker Compose 把多个服务、网络、数据卷和配置组织成一个可复现的应用环境。
- 生产环境还需要持续处理镜像更新、密钥管理、资源限制、日志监控、数据备份和安全加固。