7AA7 博客

Docker 入门实战:把开发环境装进容器里

舒野 UID:75
2026-10-03 03:20
7 1 0

“在我机器上能跑”这句话几乎每个开发者都说过,也几乎每个接手的人都被坑过。Docker 的价值不在于概念多新潮,而在于它把”运行环境”本身变成了可以版本化、可以传递的产物:一个镜像装好依赖,谁拉下来跑,行为都一致。这篇从概念区分讲起,把 Dockerfile、分层缓存、卷与端口、compose 串一遍,最后落到开发环境容器化的实际收益和坑。

镜像与容器:模板和实例

镜像(image)是只读的模板,里面是文件系统快照加启动配置;容器(container)是镜像跑起来之后的实例,有自己的可写层、进程和网络。类比面向对象的语言:镜像是类,容器是对象。一次构建出的镜像可以起任意多个容器,容器之间互相隔离,改坏了删掉重建就行,这也是”环境可抛弃”的来源。

两个高频命令先记牢:

docker build -t myapp:dev .      # 从 Dockerfile 构建镜像并打标签
docker run -d --name api -p 8080:8080 myapp:dev   # 后台起容器并映射端口
docker exec -it api sh           # 进容器里看看
docker logs -f --tail 100 api    # 看日志

docker ps 看运行中的容器,加上 -a 才能看到已退出的——排查”容器一起来就挂”的问题时,-a 加 docker logs 是第一步。

Dockerfile:常用指令怎么用

一个最小的 Node 服务镜像长这样:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 8080
CMD ["node", "server.js"]

逐条看意图:

  • FROM:基础镜像,决定起点。带版本号的标签(node:20-alpine)比 latest 稳得多,构建可重现。
  • WORKDIR:设定后续指令的工作目录,不存在会自动创建,比 RUN cd /app 干净。
  • COPY:把宿主机文件拷进镜像。只拷需要的文件,别 COPY . . 把 .git、本地缓存、密钥一起打进去。
  • RUN:在构建阶段执行命令,提交成一层。能合并的安装命令合并成一条 RUN,减少层数。
  • CMD 与 ENTRYPOINT:都是容器启动时的命令,分工是 ENTRYPOINT 定”固定入口”,CMD 定”默认参数”。两者都用 exec 形式(JSON 数组)而不是 shell 形式,信号能正确传给主进程,容器才能被优雅停止。只有默认启动命令时用 CMD 就够了。

.dockerignore 值得单独配一份,把 node_modules、target、*.log、.env 排除掉,既加快构建上下文的传输,也避免把敏感文件打进镜像。

分层与缓存:构建为什么有时快有时慢

Dockerfile 里每条会改文件系统的指令生成一层,层是只读的、可共享的。构建时如果某一层的内容和上次完全一致,就直接复用缓存,跳过执行——这就是改一行代码重新构建几秒钟就能完的原因,也是改一行代码却要重新下载全部依赖的原因。

关键在于指令顺序:变化频率低的放前面,变化频率高的放后面。依赖清单(package.json)比源代码改得少,所以先 COPY package*.json 再 RUN npm ci,最后才 COPY . .。反过来写,任何一次源码改动都会让 npm ci 那层缓存失效,每次都重新装依赖。

另外注意,RUN apt-get update 和 apt-get install 必须写在同一条 RUN 里,否则 update 的缓存和 install 的缓存各自独立,时间一长就装到过期的包索引。多阶段构建(多条 FROM)在编译型语言里很常用,编译阶段用大基础镜像,运行阶段只拷贝产物,最终镜像能小一大截。

卷与端口映射

容器的可写层随容器删除就没了,数据要持久化得靠卷(volume)或绑定挂载(bind mount):

docker run -d 
  -v app-data:/var/lib/app 
  -v $(pwd)/config.yml:/app/config.yml:ro 
  -p 8080:8080 myapp:dev

命名卷(app-data)由 Docker 管理,适合数据库数据这类”应用自己的状态”;绑定挂载($(pwd)/config.yml)把宿主机文件直接映射进去,适合配置文件和开发时的源码热加载,:ro 表示只读。

端口映射 -p 8080:8080 是”宿主机端口:容器端口”,顺序别搞反。容器内部监听 0.0.0.0 才能被映射到,只监听 127.0.0.1 的服务从宿主机访问不到——这是”端口映射了但连不上”最常见的原因。

docker-compose:把一组服务写成一个文件

单个容器够用之后,多个服务(应用 + 数据库 + 缓存)的编排就交给 compose:

services:
  app:
    build: .
    ports:
      - "8080:8080"
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - DATABASE_URL=postgres://db:5432/app
    depends_on:
      - db

  db:
    image: postgres:16
    environment:
      - POSTGRES_PASSWORD=devpass
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

几个要点:depends_on 只保证启动顺序,不保证服务就绪,应用侧仍要处理”数据库还没起来”的重试;.:/app 把源码挂进来实现本地改代码即时生效,/app/node_modules 用匿名卷覆盖掉容器内的依赖目录,避免宿主机和容器的原生依赖(如 node_modules 里的二进制)互相冲突。整套环境一条 docker compose up -d 起来,新同事入职半天就能把全栈跑通。

开发环境容器化的收益与坑

收益是实打实的:环境一致性,”能构建出镜像”就是最硬的运行证明;依赖可抛弃,脏了删掉重建,不留长年累月的全局污染;团队共享,compose 文件即文档;和 CI 的距离变近,本地构建过的镜像在流水线上大同小异。

坑也集中在几处:

卷权限。 Linux 上绑定挂载的文件属主是宿主机 UID,容器里的进程跑的是另一个 UID,就会出现”写不进去”。按需 chown、用 :z/:Z 标签(SELinux 环境)、或者在镜像里创建和宿主机一致的 UID 用户,都是常见解法。Windows 上挂载路径的格式和文件事件的传播也有差异,文档要写清楚。

时区。 基础镜像默认多是 UTC,容器里打出来的日志时间戳和本地差几个小时,排查问题时对不上号。ENV TZ=Asia/Shanghai 加上安装 tzdata(apk add --no-cache tzdata 或 apt-get install -y tzdata)是标配。

日志。 不要把日志写进容器内的固定文件,容器一删就没了。写到标准输出,交给 docker logs 或日志驱动收集;实在要落盘的,挂卷出去。另外留意日志文件无限增长的问题,生产上配 log-opts 限制单文件大小和保留份数。

镜像体积与安全。 alpine、distroless 一类小基础镜像能显著缩小攻击面和拉取时间;docker scan、trivy 这类扫描器在构建流水线里跑一遍,依赖里有已知漏洞时能提前发现。基础镜像定期重建,别让一年前的系统包一直躺在生产里。

Docker 学到能顺畅起环境就够日常用了,更细的网络模型、存储驱动可以等真正需要时再深入。先把”镜像可重现、数据不丢、日志找得到”这三件事做到,容器化的好处就已经吃到了。

73浏览量
7赞
1评论
1参与用户
9 分钟阅读时间

评论(1)

  • Bit 序章 · 2026-10-03 06:20

    这个方向我关注很久了,终于有人系统写一遍。

    回复