Docker 从入门到进阶:一篇写给工程师的系统化实战指南

1. Docker 到底解决什么问题

在没有容器之前,一个应用从开发机走到测试、预发、生产,经常会遇到这些问题:

  • 本地 Node、Java、Python、系统库版本和服务器不一致。
  • 部署文档写了十几步,少执行一步就出错。
  • 应用依赖数据库、缓存、消息队列,多人协作时环境难以复现。
  • 回滚不稳定,因为机器上的包、配置和临时文件已经被改过很多次。

Docker 的核心价值是把“应用如何运行”标准化。它把应用代码、系统依赖、运行命令、端口、环境变量等打包成镜像,再用镜像启动容器。镜像是可分发的模板,容器是运行中的实例。

需要注意:Docker 不是传统意义上的虚拟机。Linux 上的容器共享宿主机内核,通过命名空间、控制组、联合文件系统等机制实现隔离和资源控制。macOS 和 Windows 上运行 Linux 容器时,Docker Desktop 通常会在背后使用轻量 Linux 虚拟机承载 Docker Engine,所以不要把“容器”和“完整虚拟机”混为一谈。

一个简单但非常重要的心智模型:

  • Dockerfile:描述如何构建镜像的文本文件。
  • Image 镜像:只读模板,包含应用与依赖,通常按层保存。
  • Container 容器:镜像启动后的运行实例,拥有自己的可写层、进程、网络和挂载。
  • Registry 镜像仓库:存储和分发镜像,例如 Docker Hub、私有 Harbor、云厂商容器镜像仓库。
  • Volume 卷:用于保存容器之外仍需保留的数据。
  • Network 网络:让容器互相访问,也让外部流量进入容器。

2. 安装后的第一组命令

安装 Docker Desktop 或 Docker Engine 后,先确认命令可用:

dockerversiondockerinfodockercompose version

其中docker compose是现在推荐的 Compose V2 调用方式。老文章中常见的docker-compose是历史命令,新项目建议统一使用docker compose

用 Nginx 跑一个最小示例:

dockerpull nginx:alpinedockerrun-d-p8080:80--nameweb nginx:alpinedockerps

打开浏览器访问:

http://localhost:8080

这条命令中最重要的参数是:

  • -d:后台运行容器。
  • -p 8080:80:把宿主机的 8080 端口映射到容器内的 80 端口。
  • --name web:给容器起名,后续用名字管理更方便。
  • nginx:alpine:镜像名与标签。alpine表示这个镜像基于 Alpine Linux 变体。

查看日志、进入容器、停止并删除:

dockerlogs-fwebdockerexec-itwebshdockerstop webdockerrmweb

如果只是临时体验,可以使用--rm,容器退出后自动删除:

dockerrun--rmalpine:3.20echo"hello docker"

常用对象查看命令:

dockerps# 查看运行中的容器dockerps-a# 查看全部容器dockerimages# 查看本地镜像dockervolumels# 查看卷dockernetworkls# 查看网络dockersystemdf# 查看 Docker 占用空间

清理命令要谨慎:

dockercontainer prune# 删除已停止容器dockerimage prune# 删除悬空镜像dockervolume prune# 删除未使用卷,可能清掉数据dockersystem prune# 综合清理,发布机上务必先确认影响

3. 镜像、容器与仓库:不要把三个概念混在一起

镜像是只读模板,容器是在镜像之上增加可写层后的运行实例。删除容器不会删除镜像;删除镜像也不应该影响已经基于该镜像创建出的容器文件系统,但如果容器仍引用镜像,Docker 通常会阻止你删除该镜像。

3.1 镜像标签不是版本锁

很多人误以为latest表示“永远最新”,这是一个危险误解。latest只是一个普通标签,镜像作者可以随时让它指向不同内容。生产环境建议使用明确版本:

dockerrun nginx:1.27-alpinedockerrun postgres:16

对强一致性要求高的场景,可以使用镜像摘要:

dockerrun nginx@sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

摘要能更严格地锁定内容,但会增加升级维护成本。实际工程中常见做法是:开发环境用语义化标签,生产发布系统记录标签、摘要、Git commit 和构建时间。

3.2 镜像分层为什么重要

Dockerfile 中每个关键构建步骤会形成镜像层。层可以缓存和复用,所以 Dockerfile 的顺序会直接影响构建速度:

# 不推荐:代码一变,依赖安装层也容易失效 COPY . . RUN npm ci # 推荐:先复制依赖清单,再安装依赖,最后复制业务代码 COPY package*.json ./ RUN npm ci COPY . .

这背后的原则是:变化少的步骤放前面,变化频繁的业务代码放后面。

4. 数据持久化:容器可以删,数据不能丢

容器默认文件系统适合放临时运行状态,不适合放长期业务数据。数据库文件、上传文件、队列状态、缓存快照等需要明确挂载。

4.1 Bind Mount:适合开发热更新

Bind Mount 把宿主机某个目录直接挂进容器,适合开发时同步代码:

dockerrun--rm-it\--mounttype=bind,source="$PWD",target=/app\-w/app node:22-alpinesh

优点是直观,宿主机改文件,容器里立刻看到。缺点是依赖宿主机路径和权限,跨平台团队要小心 Windows、macOS、Linux 的路径差异。

4.2 Named Volume:适合数据库和长期数据

命名卷由 Docker 管理,适合数据库数据:

dockervolume create pg-datadockerrun-d\--namepg\-ePOSTGRES_PASSWORD=example\-vpg-data:/var/lib/postgresql/data\postgres:16

这里的example只用于本地演示。生产环境不要把真实密码直接写进命令、脚本或仓库,应使用 secrets 或平台密钥管理能力。

删除容器不会删除命名卷:

dockerrm-fpgdockervolumels

但下面这类命令可能删除未使用卷,生产环境不要随手执行:

dockervolume prunedockercompose down-v

4.3 tmpfs:适合敏感临时文件

tmpfs把数据放在内存中,容器停止后数据消失,适合临时缓存、敏感中间文件:

dockerrun--rm\--tmpfs/tmp:rw,noexec,nosuid,size=64m\alpine:3.20sh-c"df -h /tmp"

5. 网络与端口:服务名优先,IP 靠后

Docker 网络里最容易混淆的是“容器内部端口”和“宿主机端口”:

  • 容器内部端口:应用在容器里监听的端口,例如 Nginx 监听 80。
  • 宿主机端口:外部用户访问宿主机时使用的端口,例如localhost:8080
  • -p 8080:80:左边是宿主机端口,右边是容器端口。

创建一个用户自定义网络:

dockernetwork create demo-netdockerrun-d--nameapi--networkdemo-net my-api:devdockerrun--rm--networkdemo-net alpine:3.20\sh-c"wget -qO- http://api:3000/health"

同一 Docker 网络内,容器可以用容器名或 Compose 服务名互相访问。不要在应用配置里硬编码容器 IP,因为容器重建后 IP 可能变化。

几个常见规则:

  • EXPOSE不等于发布端口,它更像镜像元数据和文档。
  • ports-p才会把容器端口发布到宿主机。
  • Compose 中同一项目默认会创建一个网络,服务可通过服务名互访。
  • 对内服务尽量只放在 Docker 网络里,不要无必要地发布到公网或宿主机所有网卡。

6. Docker Compose:把多容器应用写成蓝图

当应用依赖数据库、缓存、队列、对象存储模拟器时,单条docker run很快会变得难维护。Compose 的价值是把多容器应用写进一个声明式 YAML 文件。

新项目建议文件名使用compose.yaml。Compose Specification 已经是滚动规范,旧式顶层version:对新项目通常没有必要,写了反而容易让读者误以为这是 Compose 文件格式版本锁。

下面是一个 API + PostgreSQL + Redis 的开发环境示例:

services:api:build:context:.dockerfile:Dockerfileports:-"3000:3000"environment:NODE_ENV:developmentDATABASE_URL:postgres://app:example@db:5432/appREDIS_URL:redis://redis:6379depends_on:db:condition:service_healthyredis:condition:service_startedvolumes:-.:/app-api-node-modules:/app/node_modulesdb:image:postgres:16environment:POSTGRES_USER:appPOSTGRES_PASSWORD:examplePOSTGRES_DB:appvolumes:-db-data:/var/lib/postgresql/datahealthcheck:test:["CMD-SHELL","pg_isready -U app -d app"]interval:10stimeout:5sretries:5redis:image:redis:7-alpinevolumes:db-data:api-node-modules:

上面的POSTGRES_PASSWORD: example只适合本地开发演示。团队环境和生产环境应改用 secrets、密钥管理服务或部署平台提供的安全注入方式。

启动和查看:

dockercompose up-d--builddockercomposepsdockercompose logs-fapidockercomposeexecapish

停止:

dockercompose down

重点提醒:

  • depends_on可以控制启动顺序,配合健康检查可以等待依赖达到健康状态;但应用代码仍应具备重试数据库、缓存等依赖的能力。
  • docker compose down默认会删除 Compose 创建的容器和网络;加上-v会删除卷,可能造成数据丢失。
  • 开发环境可以挂载源码,生产镜像不建议依赖源码 Bind Mount。

6.1 Compose 中使用 secrets

密码、令牌、私钥不要写进镜像,也不要直接写进 Git 仓库。Compose 可以把 secret 作为文件挂载到容器:

services:db:image:postgres:16environment:POSTGRES_USER:appPOSTGRES_DB:appPOSTGRES_PASSWORD_FILE:/run/secrets/db_passwordsecrets:-db_passwordsecrets:db_password:file:./secrets/db_password.txt

这里环境变量里只写 secret 文件路径,真正密码来自容器内的/run/secrets/db_password

7. 写一个专业的 Dockerfile

Dockerfile 不是“把本机命令搬进去”这么简单。一个好的 Dockerfile 应该具备这些特征:

  • 可重复构建:同样输入尽量得到同样输出。
  • 构建快:合理利用缓存。
  • 镜像小:最终镜像只包含运行必需文件。
  • 安全:不把密钥、源码历史、构建工具链带进运行镜像。
  • 可运维:提供健康检查、明确端口、清晰启动命令。

下面是一个 Node.js API 的多阶段构建模板。它不是唯一答案,但体现了工程上常用的结构:

# syntax=docker/dockerfile:1 FROM node:22-alpine AS deps WORKDIR /app COPY package*.json ./ RUN npm ci FROM deps AS build WORKDIR /app COPY . . RUN npm run build FROM node:22-alpine AS runtime ENV NODE_ENV=production WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY --from=build /app/dist ./dist COPY package*.json ./ USER node EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ CMD node -e "fetch('http://127.0.0.1:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" CMD ["node", "dist/server.js"]

几个关键点:

  • 第一行# syntax=docker/dockerfile:1让你可以使用较新的 Dockerfile 语法能力。
  • deps阶段只安装依赖,便于缓存。
  • build阶段负责构建产物。
  • runtime阶段只复制运行需要的文件,不包含完整源码和构建缓存。
  • USER node避免应用以 root 用户运行。前提是应用不需要写入没有权限的目录,日志应输出到标准输出。
  • EXPOSE 3000只是声明容器内服务端口,不会自动把端口发布到宿主机。真正发布端口仍要用-p或 Compose 的ports

7.1 一定要写.dockerignore

.dockerignore用来控制构建上下文。如果不写,node_modules、日志、测试缓存、.git、本地密钥都可能被发送给 Docker 构建器,拖慢构建甚至造成泄漏。

示例:

.git node_modules dist coverage .env .env.* *.log Dockerfile* compose*.yaml README.md

注意:是否忽略distcompose*.yamlREADME.md要根据项目情况决定。如果构建阶段需要这些文件,就不能忽略。

7.2 构建、运行和验证

dockerbuild-tmy-api:dev.dockerrun--rm-p3000:3000--namemy-api my-api:devdockerlogs-fmy-api

查看镜像层和元数据:

dockerimage inspect my-api:devdockerhistorymy-api:dev

8. 进阶构建:BuildKit、密钥和多平台镜像

8.1 构建时不要用 ARG 或 ENV 传密钥

构建参数和环境变量不适合传递密钥,因为它们可能出现在镜像历史、构建记录或最终镜像元数据中。构建时需要访问私有包仓库、私有 Git 仓库、云凭证时,应该使用 Build secrets 或 SSH mount。

示例:构建时临时挂载.npmrc

dockerbuild\--secretid=npmrc,src=.npmrc\-tmy-api:secure.

Dockerfile:

# syntax=docker/dockerfile:1 FROM node:22-alpine AS deps WORKDIR /app COPY package*.json ./ RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci

这样.npmrc不会被复制进镜像层。

8.2 构建多平台镜像

如果你的镜像要同时跑在 x86 服务器和 ARM 机器上,可以用buildx构建多平台镜像:

dockerbuildx create--namemultiarch--usedockerbuildx build\--platformlinux/amd64,linux/arm64\-tregistry.example.com/my-api:1.2.3\--push.

多平台构建常见于:

  • 云服务器与本地 Apple Silicon 开发机混用。
  • 边缘设备、树莓派、ARM 节点需要同一应用镜像。
  • 团队希望一个标签支持多个 CPU 架构。

8.3 发布标签建议

推荐同时记录:

  • 语义化版本:1.2.3
  • Git commit:git-abc1234
  • 环境标签:stagingproduction
  • 镜像摘要:sha256:...

不要只依赖latestlatest可以用于演示或本地体验,但生产发布应该能追溯到具体源码和构建产物。

9. 生产环境检查清单

上线前至少检查以下内容。

9.1 安全

  • 使用可信基础镜像,优先选择官方镜像或团队维护的基础镜像。
  • 不在 Dockerfile、镜像层、环境变量、仓库中硬编码密钥。
  • 尽量使用非 root 用户运行应用。
  • 不把 Docker socket 随意挂进容器。/var/run/docker.sock等同于授予很高的宿主机控制能力。
  • 对镜像做漏洞扫描,并建立升级基础镜像的节奏。
  • 生产容器尽量只开放必要端口。

9.2 稳定性

  • 为服务提供健康检查。
  • 设置合理的重启策略,例如restart: unless-stopped
  • 为关键服务配置 CPU、内存限制,并观察真实峰值。
  • 应用要能处理 SIGTERM,实现优雅停止。
  • 不要把业务数据只放在容器可写层。

示例:

services:api:image:registry.example.com/my-api:1.2.3restart:unless-stoppedports:-"3000:3000"read_only:truetmpfs:-/tmp:size=64mmem_limit:512mcpus:1.0

是否能启用read_onlycap_droptmpfs,取决于应用是否需要写本地文件、绑定低端口、调用系统能力。安全配置不能机械套用,必须压测和验证。

9.3 可观测

  • 日志输出到标准输出和标准错误,由平台采集。
  • 健康检查接口不要依赖过重逻辑,避免把数据库慢查询变成健康检查雪崩。
  • 指标至少包含请求量、错误率、延迟、资源使用、依赖连接状态。
  • 保留镜像标签、摘要、构建时间、commit,便于回滚和审计。

9.4 数据

  • 数据库存储使用命名卷、云盘或外部托管服务。
  • 建立备份策略,并定期做恢复演练。
  • 明确哪些数据随容器删除,哪些数据随卷保留。
  • 谨慎使用docker compose down -vdocker volume prune

10. 故障排查手册

10.1 先看容器是否在运行

dockerpsdockerps-adockerinspect<container>

重点看:

  • Status
  • ExitCode
  • RestartCount
  • Mounts
  • NetworkSettings
  • Health

10.2 再看日志

dockerlogs--tail=200<container>dockerlogs-f<container>dockercompose logs-fapi

如果日志为空,通常有几种可能:

  • 应用根本没有启动到日志初始化阶段。
  • 应用把日志写到文件而不是标准输出。
  • 容器启动命令错了,进程直接退出。

10.3 排查端口

dockerport<container>dockerinspect<container>--format'{{json .NetworkSettings.Ports}}'

常见错误:

  • -p 80:8080-p 8080:80写反。
  • 宿主机端口已经被占用。
  • 应用只监听127.0.0.1,没有监听容器内的0.0.0.0
  • 只写了EXPOSE,却没有真正发布端口。

10.4 排查网络

dockernetworklsdockernetwork inspect<network>dockercomposeexecapish

进入容器后检查:

getent hosts dbnc-vzdb5432wget-qO- http://api:3000/health

不同镜像内置工具不同。Alpine 里可能需要安装busybox-extras才有nc。生产镜像为了保持精简,通常不会内置太多排查工具,可以临时启动一个同网络的调试容器:

dockerrun--rm-it--network<network>alpine:3.20sh

10.5 排查资源

dockerstatsdockereventsdockersystemdf

如果容器频繁退出,关注:

  • 是否 OOM。
  • 是否健康检查失败后被平台重启。
  • 是否启动命令使用了前台进程。容器里的主进程退出,容器就会退出。
  • 是否磁盘写满或日志无限增长。

10.6 常见问题速查

现象高概率原因排查方向
浏览器访问不到服务端口映射错误、服务未监听0.0.0.0、宿主机防火墙docker psdocker port、应用监听地址
容器一启动就退出主进程退出、命令写错、缺环境变量docker logsdocker inspectExitCode
API 访问不到数据库不在同一网络、服务名写错、数据库未就绪Compose 服务名、健康检查、应用重试
数据库数据丢失数据写在容器层、执行了down -vvolume prune查看volumes配置与备份
镜像太大构建工具链进入运行镜像、上下文过大、没有.dockerignore多阶段构建、docker history
构建很慢缓存顺序不合理、上下文包含大目录先复制依赖清单、优化.dockerignore
权限错误非 root 用户无法写目录、挂载目录 UID/GID 不匹配USER、目录权限、卷权限

11. 一套推荐学习路线

如果你刚开始学 Docker,建议按这个顺序练习:

  1. docker run跑通 Nginx、Redis、PostgreSQL。
  2. 学会pslogsexecinspectstoprm
  3. 给自己的一个小项目写 Dockerfile。
  4. 使用.dockerignore和多阶段构建优化镜像。
  5. 用 Volume 保存数据库数据。
  6. 用 Docker Network 或 Compose 服务名连接 API、数据库、缓存。
  7. compose.yaml管理完整开发环境。
  8. 学 Build secrets,不再把密钥写进镜像。
  9. buildx,构建多平台镜像。
  10. 用生产检查清单复盘安全、备份、健康检查和可观测性。

12. 最终最佳实践清单

  • 新项目使用docker compose,不要继续传播旧式docker-compose作为默认命令。
  • 新项目的 Compose 文件通常不需要顶层version:
  • 生产镜像不要依赖latest,要使用明确版本,并记录镜像摘要。
  • Dockerfile 中变化少的步骤放前面,业务代码放后面。
  • 使用.dockerignore减少构建上下文。
  • 使用多阶段构建,最终镜像只保留运行必需文件。
  • 不用ARGENV、代码仓库传递密钥,构建时使用 Build secrets,运行时使用 secrets 或平台密钥管理。
  • 容器内应用监听0.0.0.0,外部访问通过-p或 Composeports发布。
  • 服务间通信优先用服务名,不硬编码容器 IP。
  • 数据库等长期数据使用命名卷或外部持久化服务。
  • 上线前验证健康检查、重启策略、资源限制、日志采集和备份恢复。
  • 不随意执行docker volume prunedocker system prunedocker compose down -v
  • 不随意挂载 Docker socket 到业务容器。

13. 参考资料

  • Docker Docs