故事的开始:一个"简单"的需求
事情是这样的:老板说"把咱们的项目容器化一下"。我心想,不就是 Docker 吗?装个环境而已,能有多难?
然后我从早上九点折腾到晚上九点,期间摔了三次键盘、喝了五杯咖啡、骂了十几次娘。最终成功了,但头发掉了不少。
第一关:Dockerfile 怎么写?
先搭个 Nginx + Node.js 的环境。听起来简单,但我第一次写的 Dockerfile 长这样:
FROM node:16
FROM nginx:latest
# 等等,两个 FROM 是啥意思???
好家伙,多阶段构建不是这么用的啊!查了半天才搞明白,正确的做法是分阶段:
# 第一阶段:构建
FROM node:16 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# 第二阶段:运行
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
看到没?两个 FROM 是先后关系,不是并列关系。第一个阶段编译,第二个阶段跑。这样最终镜像只有几十 MB,而不是几个 G。
第二关:Docker Compose 的毒打
单容器没问题了,但项目需要 Nginx + Node.js 后端 + 数据库。手动启动三个容器?你是原始人吗?上 docker-compose.yml!
version: '3'
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- node-app
node-app:
build: ./app
ports:
- "3000:3000"
environment:
- DB_HOST=db
depends_on:
- db
db:
image: postgres:13
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: secret
volumes:
pgdata:
这里最大的坑是什么?网络。一开始我写了 localhost:5432 连数据库,死活连不上。后来才知道,Docker Compose 里服务之间要用服务名做 hostname,也就是 db:5432。这个坑我踩了半小时。
第三关:卷挂载,数据持久化的救星
容器一重启数据就没了?那你做个寂寞。卷挂载(volume mount)要刻进 DNA 里:
volumes:
- ./data:/app/data # 绑定挂载,本地目录
- app_data:/app/uploads # 命名卷,Docker 管理
这两个的区别是:绑定挂载你在本地能直接看到文件,适合开发;命名卷 Docker 帮你管,适合生产。选错了后果很严重——生产环境用绑定挂载,迁移的时候你就哭吧。
我悟了:Docker 的核心是"不可变基础设施"
折腾了一整天,我终于理解了 Docker 的精髓:一次构建,到处运行。你本地跑起来了,到服务器上也能跑,到同事电脑上也能跑。"在我电脑上是好的"——这句话以后不再是个梗了。
当然,Docker 也有坑:镜像层缓存。不改 package.json 就别重新 npm install 啊,放过你的 CI 吧!
# 正确的 layer 缓存姿势
COPY package*.json ./ # 先复制依赖文件
RUN npm install # 缓存这一层
COPY . . # 再复制源码
顺序不对,每次构建都重新 npm install,你说气不气?
结尾
所以啊,Docker 不难,但细节多。第一次搞肯定要踩坑,但踩过一次就记住了。最后送大家一句话:容器化之前先想清楚你要解决什么问题——别为了 Docker 而 Docker,最后容器里面跑了个 curl 就完事了。