
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 后,先确认命令可用:
bash
docker version
docker info
docker compose version
其中 docker compose 是现在推荐的 Compose V2 调用方式。老文章中常见的 docker-compose 是历史命令,新项目建议统一使用 docker compose。

用 Nginx 跑一个最小示例:
bash
docker pull nginx:alpine
docker run -d -p 8080:80 --name web nginx:alpine
docker ps
打开浏览器访问:
text
http://localhost:8080
这条命令中最重要的参数是:
-d:后台运行容器。-p 8080:80:把宿主机的 8080 端口映射到容器内的 80 端口。--name web:给容器起名,后续用名字管理更方便。nginx:alpine:镜像名与标签。alpine表示这个镜像基于 Alpine Linux 变体。
查看日志、进入容器、停止并删除:
bash
docker logs -f web
docker exec -it web sh
docker stop web
docker rm web
如果只是临时体验,可以使用 --rm,容器退出后自动删除:
bash
docker run --rm alpine:3.20 echo "hello docker"
常用对象查看命令:
bash
docker ps # 查看运行中的容器
docker ps -a # 查看全部容器
docker images # 查看本地镜像
docker volume ls # 查看卷
docker network ls # 查看网络
docker system df # 查看 Docker 占用空间
清理命令要谨慎:
bash
docker container prune # 删除已停止容器
docker image prune # 删除悬空镜像
docker volume prune # 删除未使用卷,可能清掉数据
docker system prune # 综合清理,发布机上务必先确认影响
3. 镜像、容器与仓库:不要把三个概念混在一起

镜像是只读模板,容器是在镜像之上增加可写层后的运行实例。删除容器不会删除镜像;删除镜像也不应该影响已经基于该镜像创建出的容器文件系统,但如果容器仍引用镜像,Docker 通常会阻止你删除该镜像。
3.1 镜像标签不是版本锁
很多人误以为 latest 表示"永远最新",这是一个危险误解。latest 只是一个普通标签,镜像作者可以随时让它指向不同内容。生产环境建议使用明确版本:
bash
docker run nginx:1.27-alpine
docker run postgres:16
对强一致性要求高的场景,可以使用镜像摘要:
bash
docker run nginx@sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
摘要能更严格地锁定内容,但会增加升级维护成本。实际工程中常见做法是:开发环境用语义化标签,生产发布系统记录标签、摘要、Git commit 和构建时间。
3.2 镜像分层为什么重要
Dockerfile 中每个关键构建步骤会形成镜像层。层可以缓存和复用,所以 Dockerfile 的顺序会直接影响构建速度:
dockerfile
# 不推荐:代码一变,依赖安装层也容易失效
COPY . .
RUN npm ci
# 推荐:先复制依赖清单,再安装依赖,最后复制业务代码
COPY package*.json ./
RUN npm ci
COPY . .
这背后的原则是:变化少的步骤放前面,变化频繁的业务代码放后面。
4. 数据持久化:容器可以删,数据不能丢
容器默认文件系统适合放临时运行状态,不适合放长期业务数据。数据库文件、上传文件、队列状态、缓存快照等需要明确挂载。

4.1 Bind Mount:适合开发热更新
Bind Mount 把宿主机某个目录直接挂进容器,适合开发时同步代码:
bash
docker run --rm -it \
--mount type=bind,source="$PWD",target=/app \
-w /app node:22-alpine sh
优点是直观,宿主机改文件,容器里立刻看到。缺点是依赖宿主机路径和权限,跨平台团队要小心 Windows、macOS、Linux 的路径差异。
4.2 Named Volume:适合数据库和长期数据
命名卷由 Docker 管理,适合数据库数据:
bash
docker volume create pg-data
docker run -d \
--name pg \
-e POSTGRES_PASSWORD=example \
-v pg-data:/var/lib/postgresql/data \
postgres:16
这里的 example 只用于本地演示。生产环境不要把真实密码直接写进命令、脚本或仓库,应使用 secrets 或平台密钥管理能力。
删除容器不会删除命名卷:
bash
docker rm -f pg
docker volume ls
但下面这类命令可能删除未使用卷,生产环境不要随手执行:
bash
docker volume prune
docker compose down -v
4.3 tmpfs:适合敏感临时文件
tmpfs 把数据放在内存中,容器停止后数据消失,适合临时缓存、敏感中间文件:
bash
docker run --rm \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
alpine:3.20 sh -c "df -h /tmp"
5. 网络与端口:服务名优先,IP 靠后

Docker 网络里最容易混淆的是"容器内部端口"和"宿主机端口":
- 容器内部端口:应用在容器里监听的端口,例如 Nginx 监听 80。
- 宿主机端口:外部用户访问宿主机时使用的端口,例如
localhost:8080。 -p 8080:80:左边是宿主机端口,右边是容器端口。
创建一个用户自定义网络:
bash
docker network create demo-net
docker run -d --name api --network demo-net my-api:dev
docker run --rm --network demo-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 的开发环境示例:
yaml
services:
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
NODE_ENV: development
DATABASE_URL: postgres://app:example@db:5432/app
REDIS_URL: redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
volumes:
- .:/app
- api-node-modules:/app/node_modules
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: example
POSTGRES_DB: app
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
volumes:
db-data:
api-node-modules:
上面的 POSTGRES_PASSWORD: example 只适合本地开发演示。团队环境和生产环境应改用 secrets、密钥管理服务或部署平台提供的安全注入方式。
启动和查看:
bash
docker compose up -d --build
docker compose ps
docker compose logs -f api
docker compose exec api sh
停止:
bash
docker compose down
重点提醒:
depends_on可以控制启动顺序,配合健康检查可以等待依赖达到健康状态;但应用代码仍应具备重试数据库、缓存等依赖的能力。docker compose down默认会删除 Compose 创建的容器和网络;加上-v会删除卷,可能造成数据丢失。- 开发环境可以挂载源码,生产镜像不建议依赖源码 Bind Mount。
6.1 Compose 中使用 secrets
密码、令牌、私钥不要写进镜像,也不要直接写进 Git 仓库。Compose 可以把 secret 作为文件挂载到容器:
yaml
services:
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_DB: app
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
这里环境变量里只写 secret 文件路径,真正密码来自容器内的 /run/secrets/db_password。
7. 写一个专业的 Dockerfile
Dockerfile 不是"把本机命令搬进去"这么简单。一个好的 Dockerfile 应该具备这些特征:
- 可重复构建:同样输入尽量得到同样输出。
- 构建快:合理利用缓存。
- 镜像小:最终镜像只包含运行必需文件。
- 安全:不把密钥、源码历史、构建工具链带进运行镜像。
- 可运维:提供健康检查、明确端口、清晰启动命令。

下面是一个 Node.js API 的多阶段构建模板。它不是唯一答案,但体现了工程上常用的结构:
dockerfile
# 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 构建器,拖慢构建甚至造成泄漏。
示例:
gitignore
.git
node_modules
dist
coverage
.env
.env.*
*.log
Dockerfile*
compose*.yaml
README.md
注意:是否忽略 dist、compose*.yaml、README.md 要根据项目情况决定。如果构建阶段需要这些文件,就不能忽略。
7.2 构建、运行和验证
bash
docker build -t my-api:dev .
docker run --rm -p 3000:3000 --name my-api my-api:dev
docker logs -f my-api
查看镜像层和元数据:
bash
docker image inspect my-api:dev
docker history my-api:dev
8. 进阶构建:BuildKit、密钥和多平台镜像

8.1 构建时不要用 ARG 或 ENV 传密钥
构建参数和环境变量不适合传递密钥,因为它们可能出现在镜像历史、构建记录或最终镜像元数据中。构建时需要访问私有包仓库、私有 Git 仓库、云凭证时,应该使用 Build secrets 或 SSH mount。
示例:构建时临时挂载 .npmrc:
bash
docker build \
--secret id=npmrc,src=.npmrc \
-t my-api:secure .
Dockerfile:
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 构建多平台镜像:
bash
docker buildx create --name multiarch --use
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t registry.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,实现优雅停止。
- 不要把业务数据只放在容器可写层。
示例:
yaml
services:
api:
image: registry.example.com/my-api:1.2.3
restart: unless-stopped
ports:
- "3000:3000"
read_only: true
tmpfs:
- /tmp:size=64m
mem_limit: 512m
cpus: 1.0
是否能启用 read_only、cap_drop、tmpfs,取决于应用是否需要写本地文件、绑定低端口、调用系统能力。安全配置不能机械套用,必须压测和验证。
9.3 可观测
- 日志输出到标准输出和标准错误,由平台采集。
- 健康检查接口不要依赖过重逻辑,避免把数据库慢查询变成健康检查雪崩。
- 指标至少包含请求量、错误率、延迟、资源使用、依赖连接状态。
- 保留镜像标签、摘要、构建时间、commit,便于回滚和审计。
9.4 数据
- 数据库存储使用命名卷、云盘或外部托管服务。
- 建立备份策略,并定期做恢复演练。
- 明确哪些数据随容器删除,哪些数据随卷保留。
- 谨慎使用
docker compose down -v、docker volume prune。
10. 故障排查手册
10.1 先看容器是否在运行
bash
docker ps
docker ps -a
docker inspect <container>
重点看:
StatusExitCodeRestartCountMountsNetworkSettingsHealth
10.2 再看日志
bash
docker logs --tail=200 <container>
docker logs -f <container>
docker compose logs -f api
如果日志为空,通常有几种可能:
- 应用根本没有启动到日志初始化阶段。
- 应用把日志写到文件而不是标准输出。
- 容器启动命令错了,进程直接退出。
10.3 排查端口
bash
docker port <container>
docker inspect <container> --format '{{json .NetworkSettings.Ports}}'
常见错误:
- 把
-p 80:8080和-p 8080:80写反。 - 宿主机端口已经被占用。
- 应用只监听
127.0.0.1,没有监听容器内的0.0.0.0。 - 只写了
EXPOSE,却没有真正发布端口。
10.4 排查网络
bash
docker network ls
docker network inspect <network>
docker compose exec api sh
进入容器后检查:
bash
getent hosts db
nc -vz db 5432
wget -qO- http://api:3000/health
不同镜像内置工具不同。Alpine 里可能需要安装 busybox-extras 才有 nc。生产镜像为了保持精简,通常不会内置太多排查工具,可以临时启动一个同网络的调试容器:
bash
docker run --rm -it --network <network> alpine:3.20 sh
10.5 排查资源
bash
docker stats
docker events
docker system df
如果容器频繁退出,关注:
- 是否 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