Docker进阶(Docker Compose)
目录
3-双服务协作:API + Postgres(健康检查 & 依赖启动)
3.1-src/server.js(最小 Node API)
7-进阶构建:多阶段、缓存加速、环境分离(dev/prod)
7.1-.dockerignore(演示“构建上下文”与体积差异)
7.2-package.json(用最小脚本演示 dev/prod 差异)
7.3-src/server.js(暴露可观察“模式差异”的接口)
7.4-Dockerfile(多阶段 + BuildKit 缓存)
7.5-docker-compose.yml(生产基线:使用 runner 阶段)
7.6-docker-compose.override.yml(开发覆盖:使用 deps 阶段 + 热更新)
7.7-生产模式(仅使用 docker-compose.yml,不加载 override)
7.8-开发模式(默认会合并 override:target=deps + 热更新)
7.10-.dockerignore 影响:构建上下文大小可视化
# 版本与环境
docker version
docker info
# 搜索/拉取/查看镜像
docker search nginx
docker pull nginx:alpine
docker images
docker rmi <image-id>
# 运行容器(-d 后台,-p 端口映射,--name 命名)
docker run -d -p 8080:80 --name web nginx:alpine
# 日志/进入容器/查看进程/资源使用
docker logs -f web
docker exec -it web sh
docker top web
docker stats
# 停止/启动/重启/删除
docker stop web
docker start web
docker restart web
docker rm -f web
# 导出/导入
docker save nginx:alpine > nginx.tar
docker load < nginx.tar
前置配置:创建好文件夹,方便后续学习
mkdir -p ~/compose-labs/{01-hello,02-build,03-api-db,04-multinet,05-ops,06-scale-nginx,07-build-advanced,08-final} apt install npm -y ls compose-labs/
1-单服务、最快上手:Hello Compose
目标:用 Compose 起一个最小的 Nginx 服务,学会 up/down/logs/ps。
yaml对格式要求很高,不使用tab,使用空格进行缩进
cd ~/compose-labs/01-hello
tee > docker-compose.yml << "EOF"
services:
web:
image: nginx:1.27-alpine
ports:
- "8080:80"
# 显示版本与启动参数
command: ["nginx", "-g", "daemon off;"]
EOF
docker compose up -d # 后台启动
docker compose ps # 查看状态
docker compose logs -f web # 跟随日志

curl http://localhost:8080 # 测试
docker compose down # 停止并清理容器网络

1.1-小点总结
Compose = 把“多条 docker run 命令”写进 YAML。
ports: 主机:容器,只映射需要暴露的端口。
2-用 Compose 构建镜像:本地热更新/自动重建
目标:Compose 不仅能拉镜像,还能构建镜像;开发期支持绑定挂载热更新。
cd ~/compose-labs/02-build
2.1-Dockerfile
tee > dockerfile << "EOF"
# syntax=docker/dockerfile:1.7
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
FROM base AS dev
COPY . .
CMD ["npm","run","dev"] # 假设用 nodemon 或 tsx 热启动
FROM base AS runner
COPY . .
RUN npm run build
CMD ["node","dist/server.js"]
EOF
2.2-package.json
tee > package.json << "EOF"
{
"name": "hello-compose",
"private": true,
"scripts": {
"dev": "node -e \"require('http').createServer((_,r)=>r.end('dev ok')).listen(3000)\"",
"build": "echo \"(这里替换为你的真实构建命令)\""
}
}
EOF
2.3-docker-compose.yml
tee > docker-compose.yml << "EOF"
services:
app:
build:
context: .
target: dev # 使用 dev 阶段
ports:
- "3000:3000"
volumes:
- ./:/app # 开发期热更新
environment:
- NODE_ENV=development
init: true # 使用 tini 作为 PID 1,避免僵尸进程
EOF
# 构建,检验,清理
npm install
docker compose up --build -d

curl http://localhost:3000 # dev ok
docker compose down

2.4-小点总结
build.context指向源码目录;target指明多阶段构建的目标层。
volumes绑定挂载,开发期热更新;生产一般不这样做。
3-双服务协作:API + Postgres(健康检查 & 依赖启动)
目标:用 Compose 编排 API 与数据库,健康检查、依赖启动、数据持久化。
cd ~/compose-labs/03-api-db && mkdir src
3.1-src/server.js(最小 Node API)
tee > src/server.js << "EOF"
import http from "http";
import { Client } from "pg";
const port = process.env.PORT || 3000;
const client = new Client({ connectionString: process.env.DATABASE_URL });
const server = http.createServer(async (req, res) => {
if (req.url === "/healthz") {
res.writeHead(200); res.end("ok"); return;
}
if (req.url === "/") {
try {
await client.connect();
const { rows } = await client.query("SELECT NOW() as now");
await client.end();
res.setHeader("Content-Type","application/json");
res.end(JSON.stringify({ now: rows[0].now }));
} catch (e) {
res.writeHead(500); res.end(String(e));
}
return;
}
res.writeHead(404); res.end("not found");
});
server.listen(port, () => console.log("API on", port));
EOF
3.2-Dockerfile
tee > dockerfile << "EOF"
# syntax=docker/dockerfile:1.7
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY src ./src
EXPOSE 3000
HEALTHCHECK --interval=20s --timeout=2s --retries=5 CMD node -e "fetch('http://127.0.0.1:3000/healthz').then(r=>process.exit(r.status===200?0:1)).catch(()=>process.exit(1))"
CMD ["node","src/server.js"]
EOF
3.3-package.json
tee > package.json << "EOF"
{
"name": "api-db",
"type": "module",
"dependencies": {
"pg": "^8.12.0"
}
}
EOF
3.4-.env
tee > .env << "EOF"
POSTGRES_PASSWORD=example
POSTGRES_DB=appdb
DATABASE_URL=postgres://postgres:example@db:5432/appdb
TZ=UTC
EOF
3.5-docker-compose.yml
tee > docker-compose.yml << "EOF"
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- dbdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL","pg_isready -U postgres -d ${POSTGRES_DB}"]
interval: 10s
timeout: 3s
retries: 5
api:
build: .
env_file: .env
ports:
- "8080:3000"
depends_on:
db:
condition: service_healthy
restart: always
init: true
volumes:
dbdata:
EOF
# 构建,日志,检验,清理
npm install
docker compose up --build -d

docker compose logs -f db
curl http://localhost:8080/ # 返回 { now: 时间 }
curl http://localhost:8080/healthz # ok
docker compose ps
docker compose down -v # 清理包含卷(会删数据库数据)


3.6-小点总结
.env提供变量;env_file导入。
depends_on.condition=service_healthy让 API 等 DB 真正健康后再启动。命名卷
dbdata用于数据持久化。
4-多网络隔离:前后端网络分层
目标:让 Web 只暴露对外端口,DB 隐身在后端网络里,提高安全性。
cd ~/compose-labs/04-multinet/
4.1-注意
后面的实操延用03实操的文件,仅修改docker-compose.yml文件
cp -r ~/compose-labs/03-api-db/* ./
关键点
配置 作用 networks声明了两个独立网络:frontend、backend db.networks: [backend]数据库只在后台网络中可见 api.networks: [frontend, backend]API 同时连前后端 外部访问 只能访问 api:8080;db完全对宿主机和外部隔离
4.2-docker-compose.yml
tee > docker-compose.yml << "EOF"
networks:
frontend:
backend:
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: example
POSTGRES_DB: appdb
volumes:
- dbdata:/var/lib/postgresql/data
networks: [backend]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d appdb"]
interval: 10s
timeout: 3s
retries: 5
api:
build: .
environment:
DATABASE_URL: postgres://postgres:example@db:5432/appdb
ports:
- "8080:3000"
depends_on:
db:
condition: service_healthy
networks: [frontend, backend]
restart: always
volumes:
dbdata:
EOF
4.3-环境搭建与测试
启动
npm install docker compose up --build -d docker compose ps输出:
NAME STATE HEALTH 04-multinet-db-1 running (healthy) 04-multinet-api-1 running (healthy)
网络结构验证
docker network ls输出:
NETWORK ID NAME DRIVER SCOPE xxxxxxx 04-multinet_frontend bridge local xxxxxxx 04-multinet_backend bridge local
外部访问验证
curl http://localhost:8080 # 输出时间宿主机访问数据库端口:
nc -zv localhost 5432 # 输出: # nc: connect to localhost (127.0.0.1) port 5432 (tcp) failed: Connection refused数据库隔离成功。
内部通信测试
网络拓扑分析:外部-->frontend 网络-->api-->backend 网络-->db
进入
api容器内:docker compose exec api sh ping -c 2 db exit # 退出输出:
PING db (172.18.0.2): 56 data bytes 64 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.09 ms说明:
api能解析到db。
进入
db容器内:docker compose exec db sh ping -c 2 api exit # 退出输出:
PING api (172.18.0.3): 56 data bytes 64 bytes from 172.18.0.3: seq=0 ttl=64 time=0.047 ms
docker compose ps docker compose down -v # 清理包含卷(会删数据库数据)
在docker-compose.yaml文件中
services: db: networks: [backend] api: networks: [frontend, backend]这意味着:
db在backend网络;
api在 frontend + backend;两者 共享 backend 网络。
❗重点:共享同一网络的容器一定可以互相通信。
4.4-小点总结
api同时连前后端网络;db只在backend。外部只能访问到映射端口的
api。
5-观测与治理:日志轮转、健康检查、重启策略、只读根
目标:限制日志体积、防止日志爆盘;加固容器运行安全;加健康检查。
cd ~/compose-labs/05-ops
cp -r ~/compose-labs/03-api-db/* ./
5.1-docker-compose.yml
tee > docker-compose.yml << "EOF"
x-logging: &default-logging
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
x-health: &api-health
test: ["CMD","node","-e","fetch('http://127.0.0.1:3000/healthz').then(r=>process.exit(r.status===200?0:1)).catch(()=>process.exit(1))"]
interval: 20s
timeout: 2s
retries: 5
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: example
POSTGRES_DB: appdb
volumes:
- dbdata:/var/lib/postgresql/data
logging: *default-logging
healthcheck:
test: ["CMD-SHELL","pg_isready -U postgres -d appdb"]
interval: 10s
timeout: 3s
retries: 5
restart: unless-stopped
api:
build: .
environment:
DATABASE_URL: postgres://postgres:example@db:5432/appdb
TZ: UTC
ports:
- "8080:3000"
depends_on:
db:
condition: service_healthy
healthcheck: *api-health
logging: *default-logging
restart: always
init: true
read_only: true # 根文件系统只读
tmpfs:
- /tmp # 需要写入的目录挂 tmpfs
security_opt:
- no-new-privileges:true # 降低提权风险
user: "1000:1000" # 尽量非 root 运行
volumes:
dbdata:
EOF
5.2-验证和模拟环境
1. 构建+启动
npm install
docker compose up --build -d
docker compose ps

2. 健康检查验证
apt install jq -y
docker inspect --format='{{json .State.Health}}' $(docker compose ps -q api) | jq

3. 日志轮转验证
docker-compose.yml中:
logging: driver: "json-file" options: max-size: "10m" max-file: "3"文件中的这段代码表示:
容器日志会自动分片(最大 10MB)
最多保留 3 个文件,旧日志自动删除
模拟生成日志
docker compose exec api sh -c 'for i in $(seq 1 100000); do echo "log-$i"; done'
然后进入宿主机查看日志
docker inspect $(docker compose ps -q api) | grep LogPath
# 输出路径例如:
# /var/lib/docker/containers/xxxx-json.log
ls -lh /var/lib/docker/containers/18d737cc459c7d09f76c7d8b3cd2ef0d1f0f5010691f14635840080c5989c1fc/


输出类似如下:
xxxx-json.log xxxx-json.log.1 xxxx-json.log.2超过 30MB 时旧日志会被自动清理。
4. 重启策略验证
restart: always的容器会在崩溃时自动重启。下面模拟容器崩溃
docker compose exec api pkill nodedocker compose ps
几秒内你会发现
api自动恢复为running状态(重启计数增加)。docker inspect $(docker compose ps -q api) | jq '.[0].RestartCount' # 输出为重启次数
若是
db服务(unless-stopped),它也会重启,但如果你用docker compose stop主动停止,它不会再自动启动。
5. 只读文件系统验证
read_only: true表示容器根目录(/)不能写。测试 1:尝试写根目录
docker compose exec api sh -c 'echo hi > /root/test.txt' # 报错:Read-only file system
测试 2:临时目录 /tmp 可写
docker compose exec api sh -c 'echo hi > /tmp/test.txt && cat /tmp/test.txt' # 输出:hi
因为单独挂载了:
tmpfs: - /tmp
6. 安全性验证
检查当前用户
docker compose exec api id # uid=1000 gid=1000说明容器内不是 root 用户运行。
尝试提权(会失败)
docker compose exec api sh -c 'apk add sudo' # 报错:read-only file systemdocker compose exec api sh -c 'chmod 777 /etc/passwd' # 报错:Read-only file system
no-new-privileges:true确保即使进程有漏洞,也无法提权。
7. 健康状态失效模拟
编辑
src/server.js(或健康路由)让/healthz返回错误:tee > src/server.js <<'EOF' import http from "http"; import { Client } from "pg"; const port = process.env.PORT || 3000; const client = new Client({ connectionString: process.env.DATABASE_URL }); const server = http.createServer(async (req, res) => { // 模拟健康检查失效:返回 500 if (req.url === "/healthz") { res.writeHead(500); res.end("bad"); return; } if (req.url === "/") { try { await client.connect(); const { rows } = await client.query("SELECT NOW() as now"); await client.end(); res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify({ now: rows[0].now })); } catch (e) { res.writeHead(500); res.end(String(e)); } return; } res.writeHead(404); res.end("not found"); }); server.listen(port, () => console.log("API on", port)); EOF重启容器:(重启后刚开始的状态是starting)
docker compose up -d --build
docker inspect --format='{{json .State.Health}}' $(docker compose ps -q api) | jq状态变为:
"Status": "unhealthy"
此时
depends_on.condition=service_healthy的下游服务将不会继续启动(或在依赖场景下被阻止)。docker compose ps docker compose down -v # 清理包含卷(会删数据库数据)
6-扩容与反向代理:Nginx 负载到多副本 API
目标:用
--scale扩容 API 多实例,Nginx 作为反向代理对外暴露一个端口。
cd ~/compose-labs/06-scale-nginx
cp -r ~/compose-labs/03-api-db/* ./
6.1-docker-compose.yml
tee > docker-compose.yml << "EOF"
version: "3.9"
services:
db:
image: postgres:16-alpine
container_name: db
environment:
POSTGRES_PASSWORD: example
POSTGRES_DB: appdb
volumes:
- dbdata:/var/lib/postgresql/data
networks:
- appnet
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d appdb"]
interval: 10s
timeout: 3s
retries: 5
api:
build: .
environment:
DATABASE_URL: postgres://postgres:example@db:5432/appdb
networks:
- appnet
depends_on:
db:
condition: service_healthy
restart: always
web:
image: nginx:1.27-alpine
container_name: web
depends_on:
- api
ports:
- "8080:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- appnet
restart: always
networks:
appnet:
volumes:
dbdata:
EOF
6.2-Nginx 配置(nginx.conf)
tee > nginx.conf << "EOF"
# 使用 Docker 内置 DNS 解析容器服务名
resolver 127.0.0.11 valid=5s;
upstream api_backend {
zone api_backend 64k;
server api:3000 resolve; # resolve: 让 Nginx 动态解析多个副本 IP
}
server {
listen 80;
location / {
proxy_pass http://api_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 健康检查路径(可选)
location /healthz {
return 200 "ok";
}
}
EOF
6.3-src/server.js
tee > src/server.js << "EOF"
import http from "http";
import { Pool } from "pg";
const port = process.env.PORT || 3000;
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
idleTimeoutMillis: 30000
});
// 测试连接池是否正常工作
pool.query("SELECT 1")
.then(() => console.log("数据库连接池初始化成功"))
.catch(err => console.error("数据库连接池初始化失败:", err));
const server = http.createServer(async (req, res) => {
if (req.url === "/healthz") {
res.writeHead(200);
res.end("ok");
return;
}
if (req.url === "/") {
let client;
try {
client = await pool.connect();
const { rows } = await client.query("SELECT NOW() as now");
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({
now: rows[0].now,
container: process.env.HOSTNAME,
poolSize: pool.totalCount // 显示当前连接池大小
}));
} catch (e) {
console.error("请求处理错误:", e);
res.writeHead(500);
res.end(`错误: ${String(e)}`);
} finally {
if (client) {
client.release(); // 释放回连接池,而非关闭
}
}
return;
}
res.writeHead(404);
res.end("not found");
});
server.listen(port, () => console.log(`API 服务启动在 ${port} 端口,容器: ${process.env.HOSTNAME}`));
EOF
6.4-环境搭建与测试
启动服务 + 扩容
# 清理 Docker 构建缓存 docker compose down --rmi all -v # docker compose up -d --build npm install docker compose up -d --scale api=3 docker compose ps
输出:
NAME STATE 06-scale-nginx-api-1 running 06-scale-nginx-api-2 running 06-scale-nginx-api-3 running 06-scale-nginx-web-1 running 06-scale-nginx-db-1 running
查看 DNS 解析(Nginx 自动负载)
docker compose exec web nslookup api docker compose exec web nslookup api 127.0.0.11输出:
Name: api Address: 172.18.0.5 Name: api Address: 172.18.0.6 Name: api Address: 172.18.0.3说明:
api有多个容器 IP,Nginx 会轮询转发。
验证负载均衡
# 查看 api 服务日志,确认连接池初始化成功 docker compose logs api | grep "数据库连接池"
连续多次请求:
echo -e "\n测试请求结果:" for i in {1..5}; do curl http://localhost:8080/; echo -e "\n"; done
返回结果中
"now"时间变化微小(多副本响应),可以在日志中看到多个容器轮流输出。看到每个副本均有日志打印。
模拟容器宕机
docker stop 06-scale-nginx-api-2 curl http://localhost:8080/ # 仍可访问
# 清理 docker compose down --rmi all -v
结构说明
服务 说明 dbPostgreSQL 数据库,只在内部网络中 api你从 03 复制的 Node.js 应用 webNginx 反向代理,将请求转发给 api(可多副本)网络 所有服务共用同一 appnet,便于反向代理自动发现
6.5-小点总结
--scale api=3会起api-1/api-2/api-3,服务名api的 DNS 会返回多个 IP。Nginx
server api:3000 resolve;让它感知到多 IP 并轮询。
7-进阶构建:多阶段、缓存加速、环境分离(dev/prod)
目标:BuildKit 缓存、区分 dev/prod 配置、override 文件。
cd ~/compose-labs/07-build-advanced
cp -r ~/compose-labs/03-api-db/* ./
7.1-.dockerignore(演示“构建上下文”与体积差异)
tee > .dockerignore << "EOF"
node_modules
dist
.git
*.log
.DS_Store
bigfile
EOF
7.2-package.json(用最小脚本演示 dev/prod 差异)
tee > package.json << "EOF"
{
"name": "build-advanced-lab",
"private": true,
"type": "module",
"version": "1.0.0",
"scripts": {
"dev": "NODE_ENV=development node --watch src/server.js",
"build": "mkdir -p dist && cp -r src/* dist/",
"start": "node dist/server.js",
"print:env": "node -e \"console.log(process.env.NODE_ENV || 'none')\""
},
"devDependencies": {
"eslint": "^9.0.0"
},
"dependencies": {
"pg": "^8.12.0"
}
}
EOF
7.3-src/server.js(暴露可观察“模式差异”的接口)
tee > src/server.js << "EOF"
import http from "http";
const port = process.env.PORT || 3000;
const startedAt = new Date().toISOString();
const server = http.createServer((req, res) => {
if (req.url === "/healthz") {
res.writeHead(200); res.end("ok"); return;
}
if (req.url === "/mode") {
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({
mode: process.env.NODE_ENV || "undefined",
startedAt,
pid1: process.pid
}));
return;
}
if (req.url === "/") {
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({ hello: "compose-07", now: new Date().toISOString() }));
return;
}
res.writeHead(404); res.end("not found");
});
server.listen(port, () => console.log(`server on ${port}, NODE_ENV=${process.env.NODE_ENV}`));
EOF
7.4-Dockerfile(多阶段 + BuildKit 缓存)
tee > dockerfile << "EOF"
# syntax=docker/dockerfile:1.7
# 依赖阶段:仅安装依赖,并使用缓存挂载加速
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
# 构建阶段:拷贝源码并产出 dist(这里用复制代替编译)
FROM deps AS build
WORKDIR /app
COPY . .
RUN npm run build
# 运行阶段:仅带运行所需文件;非 root 运行
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -S app && adduser -S app -G app
USER app
# 仅复制 dist,刻意不复制 package*.json / node_modules,证明生产镜像更干净
COPY --from=build --chown=app:app /app/dist ./dist
EXPOSE 3000
CMD ["node","dist/server.js"]
EOF
7.5-docker-compose.yml(生产基线:使用 runner 阶段)
tee > docker-compose.yml << "EOF"
services:
api:
build:
context: .
target: runner
environment:
NODE_ENV: production
PORT: "3000"
ports:
- "8080:3000"
restart: always
EOF
7.6-docker-compose.override.yml(开发覆盖:使用 deps 阶段 + 热更新)
tee > docker-compose.override.yml << "EOF"
services:
api:
build:
target: deps
command: ["npm", "run", "dev"]
environment:
NODE_ENV: development
PORT: "3000"
volumes:
- ./:/app
EOF
| 目标 | 你能“看到”的差异 |
|---|---|
| 多阶段构建 | 生产镜像不包含 src/ 和 node_modules/(干净轻量) |
| BuildKit 缓存 | 第二次构建出现 CACHED,构建明显变快 |
| 环境分离 | 使用 docker-compose.yml(prod)与默认(dev+override)时,容器启动命令、环境、行为不同 |
| .dockerignore 生效 | 构建时“发送的上下文大小”明显不同 |
| 非 root 运行 | 生产容器内 id 显示 uid=1000(app),不是 root |
| 运行路径差异 | prod 运行 node dist/server.js;dev 运行 node --watch src/server.js(热更新) |
7.7-生产模式(仅使用 docker-compose.yml,不加载 override)
# 构建+启动
npm install
DOCKER_BUILDKIT=1 docker compose -f docker-compose.yml up --build -d

# 验证服务
curl http://localhost:8080/
curl http://localhost:8080/mode
# 预期返回:
# { "mode": "production", "startedAt": "...", "pid1": <数字> }

# 更一步验证生产镜像
CID=$(docker compose -f docker-compose.yml ps -q api)
# 1. 没有 node_modules
docker exec -it $CID sh -lc 'ls -al | sed -n "1,50p"; test -d node_modules || echo "no node_modules"'
# 2. 没有 src(只带 dist)
docker exec -it $CID sh -lc 'test -d src && echo "has src" || echo "no src"; ls -al dist'
# 3. 非 root
docker exec -it $CID sh -lc 'id'
# 预期:uid=1000(app) gid=1000(app)

# 查看生产容器启动命令
docker inspect $CID | grep -A3 '"Path"' | sed -n '1,6p'
# 预期看到 ["node","dist/server.js"]

# 镜像体积对比(当前目录只有一个服务时仅看单镜像)
docker images | head -n 5
docker history $(docker inspect $CID --format '{{.Image}}') --no-trunc | sed -n '1,20p'
# 你会看到 runner 阶段层数少、体积更小(没有依赖层)
7.8-开发模式(默认会合并 override:target=deps + 热更新)
# 停掉生产容器
docker compose -f docker-compose.yml down
# 用“默认加载”跑(= 基线 + override 合并)
DOCKER_BUILDKIT=1 docker compose up --build -d
docker compose ps

# 验证容器启动命令不同
CID=$(docker compose ps -q api)
docker inspect $CID | grep -A4 '"Path"' | sed -n '1,6p'
# 预期:["npm","run","dev"] -> node --watch src/server.js

验证热更新
# 当前 /mode curl http://localhost:8080/mode # 预期:{"mode":"development", ...} # 查看 / curl http://localhost:8080/ # 预期:{"hello":"compose-07","now":"..."}
修改src/server.js文件,添加一行内容
if (req.url === "/") { res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify({ hello: "compose-07", now: new Date().toISOString(), devTick: Date.now() })); // 添加 devTick: Date.now() return; }curl http://localhost:8080/ # 预期:{"hello":"compose-07","now":"...","devTick":...}
# 预期:返回 JSON 改变(无需重建镜像,体现 dev 模式差异)
# 验证开发容器有源码 & 可写
docker exec -it $CID sh -lc 'test -d src && echo "has src" || echo "no src"'
docker exec -it $CID sh -lc 'ls -al src | sed -n "1,50p"'
# 输出"has src"为开发模式

7.9-BuildKit 缓存:命中与失效的“可视化对比”
首次构建(观察完整构建日志)
# 清理缓存,对其它服务无影响 docker builder prune -af DOCKER_BUILDKIT=1 docker compose build --progress=plain
再次构建(不改任何文件)
DOCKER_BUILDKIT=1 docker compose build --progress=plain # 预期日志中大量出现 "CACHED" # 特别是 deps 阶段的 "npm ci" 会命中缓存挂载
只改源代码(不改 package.json)
echo "// 随机修改标记: $(head -c 8 /dev/urandom | xxd -p)" >> src/server.js DOCKER_BUILDKIT=1 docker compose build --progress=plain # 预期:deps (npm ci) 使用缓存;build & runner 层重建 # 三个阶段(deps、build、runner)确实都存在,但是平时被 BuildKit 合并输出,所以没有观察到build & runner层构建时的日志
改 package.json(模拟升级依赖)
# 往 dependencies 增加一行(比如一个不存在的版本也行,目的仅是触发) # "left-pad": "1.3.0" (举例;你可以仅改版本号触发变化) sed -i 's/"pg": "[^"]*"/"pg":"8.12.0","left-pad":"1.3.0"/' package.json npm install # 重新下载,保证package.json 和 package-lock.json 内容完全同步 DOCKER_BUILDKIT=1 docker compose build --progress=plain # 预期:deps (npm ci) 层缓存失效 -> 重新下载依赖,构建时间明显变长
7.10-.dockerignore 影响:构建上下文大小可视化
制造一个 100MB 的大文件(模拟“无意义大文件”)
dd if=/dev/zero of=bigfile bs=1M count=100确保
.dockerignore已包含bigfilegrep bigfile .dockerignore构建(观察“Sending build context”或 BuildKit 的 transfer 量)
# 方式 A:不使用 BuildKit(输出有明显 "Sending build context") DOCKER_BUILDKIT=0 docker build -t tmp-context-test . # 方式 B:使用 BuildKit,看 "transfer" 更小且更快 DOCKER_BUILDKIT=1 docker build --progress=plain -t tmp-context-test .
被忽略的大文件不会被打进构建上下文,传输更快、构建更快。
这一步直观解释了“为什么 .dockerignore 对 CI/CD 性能影响巨大”。
7.11-环境分离对比(prod vs dev)
prod(仅基线文件)
docker compose -f docker-compose.yml up -d --build CID=$(docker compose -f docker-compose.yml ps -q api) # 这里需要等待容器重启成功再进行下面的操作 docker exec -it $CID sh -lc 'node -p process.env.NODE_ENV' # 预期:production docker inspect $CID | grep -A4 '"Path"' # 预期:node dist/server.js(非 watch,非 volumes) docker exec -it $CID sh -lc 'test -d src || echo "prod: no src"' # 预期:prod: no src
dev(基线 + override 合并)
docker compose down docker compose up -d --build CID=$(docker compose ps -q api) docker exec -it $CID sh -lc 'node -p process.env.NODE_ENV' # 预期:development docker inspect $CID | grep -A4 '"Path"' # 预期:npm run dev -> node --watch src/server.js docker exec -it $CID sh -lc 'test -d src && echo "dev: has src"' # 预期:dev:has src
7.12-小点总结
docker-compose.override.yml会自动合并;生产可用-f指定只用基础文件。BuildKit:
DOCKER_BUILDKIT=1 docker compose build可加速并启用缓存挂载。
更多推荐






























所有评论(0)