开发者 Docker 指南:镜像、容器与 Compose
你克隆了一个项目,按照安装说明操作,结果卡住了:缺少系统库、数据库无法启动,或者 Node 版本不匹配。Docker 的存在就是为了消除这类问题。与其直接在机器上安装各种服务,不如一次性描述它们,然后在隔离的容器中运行,无论在哪里行为都一致。
本指南讲解你真正需要的三个概念:镜像、容器和 Compose。内容注重实用,边读边跑命令即可。
镜像与容器:心智模型
镜像是一个只读模板,包含文件系统快照以及默认命令、暴露端口等元数据。容器则是该镜像的一个运行(或停止)实例,顶部带有一层薄薄的可写层。
如果你有面向对象编程背景,这个类比很贴切:镜像是类,容器是对象。你可以从一个镜像启动多个容器,而容器内的改动不会修改镜像本身。
| 概念 | 是什么 | 常用命令 |
|---|---|---|
| 镜像 | 不可变模板 | docker build、docker pull |
| 容器 | 运行实例 | docker run、docker ps |
| 卷 | 持久化存储 | docker volume create |
| Compose | 多容器定义 | docker compose up |
用 Dockerfile 构建第一个镜像
Dockerfile 是一份配方。每条指令创建一个层,Docker 会缓存这些层,所以当没有变化时重建非常快。下面是一个简单的 Node.js 示例:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]这里有两个重要习惯。第一,先复制依赖清单再复制其余源码,这样当只有应用代码变化时,npm ci 层仍能命中缓存。第二,优先使用像 alpine 这样的精简基础镜像,以减小体积和攻击面。
构建并运行:
docker build -t myapp:1.0 .
docker run --rm -p 3000:3000 myapp:1.0-p 3000:3000 将宿主机端口 3000 映射到容器端口 3000。--rm 会在容器退出时删除它,开发时能保持机器整洁。
日常容器命令
以下命令覆盖了大部分日常工作:
docker ps— 列出运行中的容器;加上-a可包含已停止的容器。docker logs -f <container>— 流式查看应用输出。docker exec -it <container> sh— 在运行中的容器内打开 shell。docker stop <container>— 优雅停止。docker images— 列出本地镜像及其大小。docker system prune— 删除未使用的容器、网络和悬空镜像。
当容器立即退出时,先看日志。最常见的原因是 CMD 错误、缺少环境变量,或者进程在容器内绑定到了 127.0.0.1 而不是 0.0.0.0。
Compose 为什么存在
真实应用通常需要多个进程:一个 API、一个数据库,可能还有一个缓存。用一长串参数运行三个 docker run 命令很容易出错。Docker Compose 让你在一个 YAML 文件中声明所有服务,并一起启动。
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:注意 API 连接的是 db,而不是 localhost。Compose 会创建一个私有网络,每个服务都可以通过其服务名访问。这是新手最常见的困惑来源。
用 docker compose up 启动所有服务,或加 -d 在后台运行。代码变更后用 docker compose up --build 重建。用 docker compose down 拆除,如果还想删除命名卷,加上 -v。
卷、绑定挂载与热重载
容器是临时的;里面的数据在删除容器后就会消失。有两种机制可以解决:
- 命名卷 — 由 Docker 管理,适合数据库文件。它们能在容器重启后保留,并在
volumes:部分声明。 - 绑定挂载 — 将宿主机目录映射到容器内,适合开发时的源代码。
services:
api:
build: .
volumes:
- ./src:/app/src使用绑定挂载后,在宿主机上编辑文件会反映到容器内。再配合文件监视工具(nodemon、uvicorn --reload、air),就能实现热重载而无需重建镜像。
实用的开发工作流
- 为应用编写 Dockerfile,保持精简。
- 添加
docker-compose.yml,包含应用及其依赖。 - 源代码用绑定挂载,数据用命名卷。
- 不要把密钥写进文件;使用
.env文件并通过${VAR}引用变量。 - 添加
.dockerignore,包含node_modules、.git和构建产物,以加快构建速度。 - 运行
docker compose up --build并迭代。
如果你的服务产生的日志之后需要检查,请记住容器日志只是文本。解析服务器日志的工具,例如 nginx 日志分析器,与容器配合得很好,因为你可以将日志管道输出或复制出来,而无需在宿主机上安装任何东西。
常见陷阱
- 镜像臃肿:使用多阶段构建,在一个阶段编译,只将产物复制到精简的运行时镜像中。
- 每次都全量重建:将 Dockerfile 指令按变化频率从低到高排序。
- 硬编码主机名:在 Compose 内使用服务名;在外部使用环境变量。
- 忽略清理:悬空镜像和已停止的容器会不断累积。定期运行
docker system prune。 - 以 root 运行:在生产镜像中添加非 root 的
USER。
常见问题
镜像和容器有什么区别?
镜像是包含文件系统和元数据的不可变模板。容器是该镜像的一个运行实例,带有一层额外的可写层。你可以从一个镜像启动多个容器。
单个服务也需要 Docker Compose 吗?
不需要。单个容器用一条 docker run 命令就够了。当你有多个服务、共享网络或想在一个地方声明环境变量时,Compose 才有价值。
容器之间如何通信?
Compose 将所有服务放在一个共享网络中,每个服务都可以通过其服务名访问。用该名称作为主机名,例如 db:5432,而不是 localhost。
Docker 值得你投入一点前期时间。一旦理解了镜像、容器和 Compose,帮助队友上手或更换机器就不再是繁琐的安装仪式,而是一条命令的事。