Docker 实战:从会用到用好

会 docker run 不够,Dockerfile 最佳实践、多阶段构建、Compose 编排、镜像瘦身才是日常真正需要的。

$1.3k 字/约 7 min👁— views

Docker 实战:从会用到用好

很多人会 docker run,但 Dockerfile 写得一塌糊涂,镜像动辄 2GB,CI 跑几分钟。这篇聚焦日常真正有用的实践。

容器 vs 虚拟机

容器不是"轻量级虚拟机",本质上是带隔离的进程

技术 隔离机制 启动时间 开销
容器 Linux namespace + cgroup < 1秒 极低
虚拟机 Hypervisor + 独立内核 数十秒 较高
  • namespace:隔离进程、网络、文件系统、用户
  • cgroup:限制 CPU、内存、IO 资源使用

容器共享宿主机内核,这是它轻量的原因,也是安全边界比 VM 弱的原因。

Dockerfile 最佳实践

层缓存利用

# 糟糕:每次代码变化都重新安装依赖
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build

# 好:先复制依赖文件,利用缓存
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

缓存失效规则: 某层失效,其后所有层都失效。把变化频率低的放前面(依赖 > 配置 > 源代码)。

.dockerignore

node_modules/
.git/
.env
*.log
dist/
coverage/
.DS_Store

减少构建上下文大小,防止 node_modules(数百MB)被发送到 Docker daemon。

非 root 用户运行

FROM node:20-alpine
WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --chown=appuser:appgroup . .

USER appuser

EXPOSE 3000
CMD ["node", "server.js"]

多阶段构建

多阶段构建是缩小镜像体积最有效的方法

# Go 应用:编译 + 运行分离
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server ./cmd/server

FROM scratch
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/server"]
# 最终镜像:~10MB(而不是 ~800MB 的 golang 镜像)
# Node.js 前端应用
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
# 最终镜像:~25MB

镜像瘦身技巧

# 1. 使用 slim 基础镜像
FROM python:3.12-slim  # ~130MB vs python:3.12 的 ~900MB

# 2. 合并 RUN 层(减少层数)
# 不好的写法(3层)
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# 好的写法(1层)
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

# 3. 不要缓存 pip 包
RUN pip install -r requirements.txt --no-cache-dir

# 4. 只安装需要的包
RUN apt-get install -y --no-install-recommends libpq-dev && \
    rm -rf /var/lib/apt/lists/*

Docker Compose

# docker-compose.yml
version: '3.9'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./uploads:/app/uploads
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:
docker compose up -d           # 后台启动
docker compose down            # 停止并删除容器
docker compose logs -f app     # 实时查看日志
docker compose exec app bash   # 进入容器
docker compose build --no-cache

网络模式

# bridge(默认):容器间用服务名互相通信
# 在 compose 中,服务名就是 DNS 名
# app 访问 db:postgresql://db:5432/xxx

# host:容器直接使用宿主机网络
docker run --network host nginx

# 自定义网络
docker network create mynet
docker run --network mynet --name service1 image1
docker run --network mynet --name service2 image2
# service2 可以 ping service1

数据持久化

# Volume(推荐用于持久数据)
docker volume create mydata
docker run -v mydata:/app/data myimage
# volume 由 Docker 管理,路径在 /var/lib/docker/volumes/

# Bind Mount(推荐用于开发时挂载代码)
docker run -v $(pwd)/src:/app/src myimage

# 备份 volume
docker run --rm -v mydata:/data -v $(pwd):/backup alpine \
    tar czf /backup/mydata-backup.tar.gz -C /data .

# 恢复
docker run --rm -v mydata:/data -v $(pwd):/backup alpine \
    tar xzf /backup/mydata-backup.tar.gz -C /data

常见坑

PID 1 信号处理

# shell 形式(sh 是 PID 1,不转发信号给 node)
CMD node server.js

# exec 形式(node 直接是 PID 1,接收 SIGTERM)
CMD ["node", "server.js"]

# 使用 tini(需要僵尸进程回收时)
FROM node:20-alpine
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]

时区问题

# Alpine
RUN apk add --no-cache tzdata && \
    cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo "Asia/Shanghai" > /etc/timezone && \
    apk del tzdata

# 或通过环境变量
ENV TZ=Asia/Shanghai

编码问题

ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8

生产实践

# 资源限制
docker run \
    --memory 512m \
    --memory-swap 512m \
    --cpus 0.5 \
    --pids-limit 100 \
    myimage

# 重启策略
docker run --restart unless-stopped myimage

# 日志驱动
docker run \
    --log-driver json-file \
    --log-opt max-size=10m \
    --log-opt max-file=3 \
    myimage

调试技巧

# 进入运行中的容器
docker exec -it <container_id> bash

# 查看容器详细信息
docker inspect <container_id>
docker inspect <container_id> | jq '.[0].NetworkSettings'

# 查看日志
docker logs -f --tail 100 <container_id>

# 资源使用
docker stats --no-stream

# 事件监听
docker events --filter type=container

与 GitHub Actions 集成

# .github/workflows/docker.yml
name: Build and Push

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: myuser/myapp:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max