把环境装进箱子:Docker 的日常开发指南
从镜像、容器、Dockerfile 到 Compose 和数据持久化,整理一份日常开发里真正用得上的 Docker 工作流。
Docker 最常见的价值,不是让人显得更“云原生”,而是把那句古老的“我机器上明明可以跑”变少一点。
它把应用运行所需的系统依赖、语言运行时、启动命令和部分配置打包到一个可复制的环境里。这样你在本地、同事电脑、CI、测试环境里运行的东西,至少有机会站在同一条起跑线上。
这篇文章先不碰 Kubernetes,也不急着谈复杂部署。我们只回答日常开发里最常遇到的问题:
- Docker 到底在管理什么。
- 镜像、容器、Dockerfile、Compose 分别是什么。
- 常用命令适合哪些场景。
- 本地开发和团队协作里怎样少踩坑。
Docker 解决的是什么问题
传统开发环境里,应用往往依赖很多本机状态:Node、Python、Java、数据库、系统库、环境变量、端口、配置文件。只要其中一处不一致,项目就可能在一个人机器上能跑,在另一个人机器上炸掉。
Docker 的思路是:不要只交付代码,也交付一份可重复创建的运行环境。
容器不是一台完整虚拟机。你可以把它理解成一个隔离的进程,它带着自己需要的文件系统、运行时和默认启动命令,同时和宿主机共享内核。因为它比虚拟机轻,所以很适合本地开发、CI 测试和应用交付。
先分清几个核心概念
镜像
镜像是一个只读模板,里面包含运行应用所需的文件、依赖和默认配置。比如 node:22-alpine、postgres:16-alpine、nginx:alpine 都是镜像。
你可以把镜像看成“打包好的运行材料”。它本身不会跑,真正跑起来的是容器。
容器
容器是镜像运行后的实例。一个镜像可以启动很多个容器,就像一个类可以创建很多个对象。
docker run --rm -p 8080:80 nginx:alpine
这条命令会用 nginx:alpine 镜像启动一个容器,把容器里的 80 端口映射到本机的 8080 端口。打开 http://localhost:8080,访问到的就是容器里的 Nginx。
Dockerfile
Dockerfile 是构建镜像的说明书。它告诉 Docker:从哪个基础镜像开始,复制哪些文件,安装哪些依赖,最后容器启动时执行什么命令。
一个极简 Node 应用 Dockerfile 可能长这样:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
这不是最终最佳实践,但它能表达基本流程:选基础镜像、设置工作目录、安装依赖、复制代码、声明端口、定义启动命令。
Registry
Registry 是存放镜像的地方。Docker Hub、GitHub Container Registry、私有镜像仓库都属于这一类。
常见动作是:
docker pull nginx:alpine
docker tag my-app:latest ghcr.io/example/my-app:latest
docker push ghcr.io/example/my-app:latest
本地构建出来的镜像只在你机器上。要给 CI、测试环境或服务器使用,就需要推到某个 registry。
Volume
容器本身应该尽量是可删除、可重建的。问题是数据库数据、上传文件、缓存这类内容不能随着容器删除一起消失,这就需要 volume。
docker volume create pg_data
docker run -d \
--name postgres \
-e POSTGRES_PASSWORD=postgres \
-v pg_data:/var/lib/postgresql/data \
postgres:16-alpine
容器删掉后,pg_data 这个 volume 还在。下次重新启动数据库容器,数据可以继续挂载回来。
Compose
单个容器用 docker run 还能接受;一旦项目里有 Web、API、数据库、Redis、消息队列,手敲命令就会很快失控。
Docker Compose 用一个 compose.yaml 描述多容器应用:
services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://postgres:postgres@db:5432/app
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: app
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- pg_data:/var/lib/postgresql/data
volumes:
pg_data:
然后一条命令启动整套本地环境:
docker compose up -d
这就是 Compose 在日常开发里的价值:把“项目怎么跑起来”写进仓库,而不是散落在每个人的聊天记录和记忆里。
常用命令和场景
看 Docker 是否可用
docker version
docker info
docker version 看客户端和服务端版本;docker info 看当前 Docker Engine、镜像、容器、存储驱动等状态。遇到 Docker Desktop 没启动、daemon 连不上时,这两个命令很快能暴露问题。
启动一个临时容器
docker run --rm -it ubuntu:24.04 bash
这里几个参数很常用:
--rm:容器退出后自动删除。-it:进入交互式终端。ubuntu:24.04:镜像名和 tag。bash:覆盖默认启动命令。
这种方式适合临时验证 Linux 命令、系统包、网络连通性,或者快速进入一个干净环境。
后台运行服务
docker run -d \
--name web \
-p 8080:80 \
nginx:alpine
常用参数:
-d:后台运行。--name:给容器起名,后面操作更方便。-p 8080:80:本机端口 8080 映射到容器端口 80。
端口映射最容易记反。规则是:宿主机端口:容器端口。
查看、进入和停止容器
docker ps
docker ps -a
docker logs -f web
docker exec -it web sh
docker stop web
docker rm web
docker ps 只看运行中的容器;docker ps -a 包括已经退出的容器。logs 看日志,exec 进入正在运行的容器,stop 停止,rm 删除。
如果你发现本机端口被占用,先看是不是某个旧容器还活着。
构建镜像
docker build -t my-app:dev .
最后的 . 是 build context,表示 Docker 会把当前目录作为构建上下文发送给构建器。这里很容易踩坑:如果没有 .dockerignore,node_modules、日志、测试截图、临时文件都可能进入上下文,让构建变慢,也让镜像层变脏。
常见 .dockerignore:
node_modules
dist
.git
.env
log
*.log
.env 一般不应该被复制进镜像。真正的密钥和环境差异,应该在运行时注入。
清理不用的资源
docker image ls
docker container ls -a
docker volume ls
docker system df
docker system prune
prune 很有用,但也要小心。它会删除未使用的资源。清 volume 前尤其要确认,因为那里可能有数据库数据。
docker volume prune
这条命令不要闭眼执行。
使用 Compose 管理本地环境
docker compose up -d
docker compose logs -f
docker compose exec app sh
docker compose down
docker compose down --volumes
down 会删除 Compose 创建的容器和网络,但默认不会删除 named volumes。加上 --volumes 才会把 volume 也删掉。这对重置本地数据库很方便,但也意味着数据会消失。
Dockerfile 怎么写得更像生产可用
使用多阶段构建
多阶段构建可以把“构建环境”和“运行环境”分开。前一阶段安装依赖、编译代码,后一阶段只拿运行需要的产物。
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM deps AS build
COPY . .
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/server.js"]
它的核心不是“写得高级”,而是避免把编译工具、开发依赖和中间文件带进最终镜像。
让缓存帮你省时间
Docker 构建是分层缓存的。Dockerfile 里越靠前的步骤越容易影响后续缓存,所以一般先复制依赖清单,再安装依赖,最后复制业务代码:
COPY package*.json ./
RUN npm ci
COPY . .
这样只改业务代码时,依赖安装层可以复用。
不要把镜像当垃圾桶
镜像里不要放不需要的东西:
- 不需要的系统包。
- 本地日志。
.git目录。- 测试产物。
- 密钥、token、私有证书。
镜像越小,构建越快,传输越快,攻击面也越小。
容器尽量保持短命、可替换
容器不应该是需要手工进去“修一修”的小服务器。更健康的方式是:配置通过环境变量和挂载传入,数据放在 volume 或外部服务,容器本身可以随时删掉重建。
如果你需要经常 docker exec 进去改文件,通常说明镜像或配置方式需要调整。
日常开发工作流
一个比较舒服的本地 Docker 工作流可以是这样:
# 第一次进入项目
docker compose up -d --build
# 看日志
docker compose logs -f app
# 进入应用容器排查问题
docker compose exec app sh
# 改了 Dockerfile 或依赖
docker compose build app
docker compose up -d
# 停止本地环境
docker compose down
如果只是改业务代码,是否需要 rebuild 取决于你的开发模式:
- 如果代码通过 bind mount 挂进容器,通常不用 rebuild。
- 如果代码被 COPY 进镜像,就需要重新 build。
- 如果改了
package.json、requirements.txt、pom.xml这类依赖清单,通常需要 rebuild。
常见坑
端口映射写反
-p 8080:80 是本机 8080 到容器 80。如果容器里应用监听的是 3000,那就应该写:
docker run -p 3000:3000 my-app:dev
数据写在容器层里
容器删除后,容器自己的可写层也会消失。数据库、本地上传文件、开发缓存要用 volume 或外部服务承接。
把 latest 当成确定版本
latest 不是“最新且安全”的承诺,它只是一个 tag。团队协作和生产镜像最好使用明确版本,比如:
FROM node:22-alpine
是否要 pin 到 digest,取决于你对可复现和安全更新的要求。至少不要在关键环境里完全依赖模糊 tag。
在镜像里放密钥
不要在 Dockerfile 里写:
ENV API_TOKEN=...
镜像层会留下历史。密钥应该通过运行时环境变量、secret 管理系统或部署平台注入。
Compose 文件越写越像生产编排
Compose 很适合本地开发和简单部署,但它不是 Kubernetes 的低配替代品。不要把所有生产复杂度都塞进本地 compose.yaml。本地文件的目标应该是让开发者可靠启动依赖,而不是模拟整个生产世界。
我会保留的一组命令
如果只记一组命令,我会记这些:
docker ps
docker ps -a
docker logs -f <container>
docker exec -it <container> sh
docker build -t <image>:<tag> .
docker run --rm -it <image> sh
docker stop <container>
docker rm <container>
docker compose up -d --build
docker compose logs -f
docker compose exec <service> sh
docker compose down
这些命令足够覆盖 80% 的日常开发排查。
最后
Docker 用得好,不是把所有东西都塞进容器,也不是为了容器化而容器化。
它真正适合解决的是环境一致性、依赖隔离、服务编排和交付可重复性。一个好的 Docker 工作流,应该让新人更快跑起项目,让 CI 更接近真实运行环境,让生产镜像更小、更明确、更容易替换。
当你能清楚地区分镜像和容器、构建和运行、临时文件和持久数据、Dockerfile 和 Compose,Docker 就不再是一组神秘命令,而会变成日常开发里很可靠的一层地板。