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-v4.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注意:是否忽略dist、compose*.yaml、README.md要根据项目情况决定。如果构建阶段需要这些文件,就不能忽略。
7.2 构建、运行和验证
dockerbuild-tmy-api:dev.dockerrun--rm-p3000:3000--namemy-api my-api:devdockerlogs-fmy-api查看镜像层和元数据:
dockerimage inspect my-api:devdockerhistorymy-api:dev8. 进阶构建: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 - 环境标签:
staging、production - 镜像摘要:
sha256:...
不要只依赖latest。latest可以用于演示或本地体验,但生产发布应该能追溯到具体源码和构建产物。
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_only、cap_drop、tmpfs,取决于应用是否需要写本地文件、绑定低端口、调用系统能力。安全配置不能机械套用,必须压测和验证。
9.3 可观测
- 日志输出到标准输出和标准错误,由平台采集。
- 健康检查接口不要依赖过重逻辑,避免把数据库慢查询变成健康检查雪崩。
- 指标至少包含请求量、错误率、延迟、资源使用、依赖连接状态。
- 保留镜像标签、摘要、构建时间、commit,便于回滚和审计。
9.4 数据
- 数据库存储使用命名卷、云盘或外部托管服务。
- 建立备份策略,并定期做恢复演练。
- 明确哪些数据随容器删除,哪些数据随卷保留。
- 谨慎使用
docker compose down -v、docker volume prune。
10. 故障排查手册
10.1 先看容器是否在运行
dockerpsdockerps-adockerinspect<container>重点看:
StatusExitCodeRestartCountMountsNetworkSettingsHealth
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.20sh10.5 排查资源
dockerstatsdockereventsdockersystemdf如果容器频繁退出,关注:
- 是否 OOM。
- 是否健康检查失败后被平台重启。
- 是否启动命令使用了前台进程。容器里的主进程退出,容器就会退出。
- 是否磁盘写满或日志无限增长。
10.6 常见问题速查
| 现象 | 高概率原因 | 排查方向 |
|---|---|---|
| 浏览器访问不到服务 | 端口映射错误、服务未监听0.0.0.0、宿主机防火墙 | docker ps、docker port、应用监听地址 |
| 容器一启动就退出 | 主进程退出、命令写错、缺环境变量 | docker logs、docker inspect的ExitCode |
| API 访问不到数据库 | 不在同一网络、服务名写错、数据库未就绪 | Compose 服务名、健康检查、应用重试 |
| 数据库数据丢失 | 数据写在容器层、执行了down -v或volume prune | 查看volumes配置与备份 |
| 镜像太大 | 构建工具链进入运行镜像、上下文过大、没有.dockerignore | 多阶段构建、docker history |
| 构建很慢 | 缓存顺序不合理、上下文包含大目录 | 先复制依赖清单、优化.dockerignore |
| 权限错误 | 非 root 用户无法写目录、挂载目录 UID/GID 不匹配 | USER、目录权限、卷权限 |
11. 一套推荐学习路线
如果你刚开始学 Docker,建议按这个顺序练习:
- 用
docker run跑通 Nginx、Redis、PostgreSQL。 - 学会
ps、logs、exec、inspect、stop、rm。 - 给自己的一个小项目写 Dockerfile。
- 使用
.dockerignore和多阶段构建优化镜像。 - 用 Volume 保存数据库数据。
- 用 Docker Network 或 Compose 服务名连接 API、数据库、缓存。
- 写
compose.yaml管理完整开发环境。 - 学 Build secrets,不再把密钥写进镜像。
- 学
buildx,构建多平台镜像。 - 用生产检查清单复盘安全、备份、健康检查和可观测性。
12. 最终最佳实践清单
- 新项目使用
docker compose,不要继续传播旧式docker-compose作为默认命令。 - 新项目的 Compose 文件通常不需要顶层
version:。 - 生产镜像不要依赖
latest,要使用明确版本,并记录镜像摘要。 - Dockerfile 中变化少的步骤放前面,业务代码放后面。
- 使用
.dockerignore减少构建上下文。 - 使用多阶段构建,最终镜像只保留运行必需文件。
- 不用
ARG、ENV、代码仓库传递密钥,构建时使用 Build secrets,运行时使用 secrets 或平台密钥管理。 - 容器内应用监听
0.0.0.0,外部访问通过-p或 Composeports发布。 - 服务间通信优先用服务名,不硬编码容器 IP。
- 数据库等长期数据使用命名卷或外部持久化服务。
- 上线前验证健康检查、重启策略、资源限制、日志采集和备份恢复。
- 不随意执行
docker volume prune、docker system prune、docker compose down -v。 - 不随意挂载 Docker socket 到业务容器。
13. 参考资料
- Docker Docs