# Docker 使用笔记:重新理解镜像、容器与数据持久化

主题:Docker 实用技巧与经验总结

字数:2800字

阅读时间:5分钟

最近重新梳理了一遍 Docker 的使用,发现之前很多理解都停留在表面。这篇笔记记录一些实用的认知和踩过的坑。

核心概念的重新理解

镜像不只是"安装包"

以前我把镜像理解成简单的安装包,但实际上镜像是分层的只读文件系统

每个 Dockerfile 指令都会创建一层:

bash 复制代码
FROM node:20          # 第1层:基础镜像
WORKDIR /app          # 第2层:创建目录
COPY package*.json .  # 第3层:复制文件
RUN npm install       # 第4层:安装依赖
COPY . .              # 第5层:复制源码
CMD ["npm", "start"]  # 最后一层:启动命令

这个分层设计带来几个好处:

  1. 缓存优化:如果前面的层没变,Docker 会直接用缓存
  2. 存储效率:多个镜像可以共享相同的基础层
  3. 快速构建:只重新构建变化的层

我之前写 Dockerfile 经常把 COPY . . 放在 RUN npm install 前面,导致每次代码改动都要重新安装依赖。调整顺序后构建速度快了很多。

容器的生命周期管理

容器的状态转换比我想象的复杂:

scss 复制代码
创建(created) → 运行(running) → 暂停(paused) → 停止(stopped) → 删除(removed)

实用命令:

ruby 复制代码
# 查看容器资源占用(CPU、内存、网络IO)
docker stats

# 查看容器内进程
docker top <container>

# 实时查看日志
docker logs -f --tail 100 <container>

# 进入容器调试
docker exec -it <container> bash

# 容器与宿主机文件传输
docker cp <container>:/path/to/file ./local/path

一个常见误区:docker stopdocker kill 的区别。

  • stop:发送 SIGTERM,给容器 10 秒优雅退出时间
  • kill:直接发送 SIGKILL,强制终止

生产环境应该用 stop,让应用有时间清理资源、关闭连接。

run vs start:理解容器的创建与启动

这是我最初混淆的地方:

  • docker run:创建容器并启动
  • docker start:启动已存在的容器

实际使用:

yaml 复制代码
# 第一次运行(创建新容器)
docker run -d --name myapp -p 3000:3000 myapp:latest

# 停止容器
docker stop myapp

# 再次启动(复用同一个容器)
docker start myapp

为什么要区分?因为容器内的非持久化数据 在容器存在时会保留,但 docker run 每次都会创建全新的容器。

Volume:数据持久化的三种方式

Volume 是我踩坑最多的地方。Docker 有三种数据挂载方式:

1. Named Volume(推荐)

kotlin 复制代码
docker run -d \
  -v postgres-data:/var/lib/postgresql/data \
  postgres:16

优点:

  • Docker 自动管理存储位置
  • 可以在容器间共享
  • 备份和迁移方便

查看 volume:

bash 复制代码
docker volume ls
docker volume inspect postgres-data

Volume 实际存储在 /var/lib/docker/volumes/ 下(Linux)或 Docker Desktop 的虚拟机里(Mac/Windows)。

2. Bind Mount(开发环境)

bash 复制代码
docker run -d \
  -v $(pwd)/data:/var/lib/postgresql/data \
  postgres:16

优点:

  • 直接映射到宿主机目录
  • 可以实时编辑文件
  • 适合开发环境热重载

坑点:权限问题

容器内进程通常以特定 UID 运行(如 Postgres 的 UID 999),如果宿主机目录权限不对,会启动失败。

解决方案:

bash 复制代码
# 方式1:修改宿主机目录权限
chown -R 999:999 ./data

# 方式2:在 Dockerfile 中指定 USER
USER postgres

3. Tmpfs Mount(临时数据)

arduino 复制代码
docker run -d \
  --tmpfs /tmp:rw,size=100m \
  myapp

数据存在内存中,容器停止即删除。适合临时缓存、session 数据。

Volume 的实际使用场景

我的项目里这样组织:

yaml 复制代码
services:
  postgres:
    image: postgres:16
    volumes:
      # 数据持久化用 named volume
      - postgres-data:/var/lib/postgresql/data
      # 初始化脚本用 bind mount
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro

  app:
    build: .
    volumes:
      # 开发环境代码热重载
      - ./src:/app/src
      # node_modules 不要覆盖(重要!)
      - /app/node_modules

volumes:
  postgres-data:

注意/app/node_modules 这行很关键。

如果不写,宿主机的 ./src 会覆盖容器内的 /app,导致 node_modules 被覆盖(宿主机可能没有 node_modules 或版本不一致)。

网络:容器间通信的正确姿势

默认网络模式

Docker Compose 会自动创建一个网络,同一个 compose 文件里的服务可以通过服务名互相访问。

yaml 复制代码
services:
  redis:
    image: redis:alpine
  
  app:
    build: .
    environment:
      # 直接用服务名作为 hostname
      REDIS_HOST: redis

在 app 容器内,redis 会被自动解析到 redis 容器的 IP。

自定义网络

有时需要多个 compose 文件共享网络:

yaml 复制代码
# compose1.yml
networks:
  shared-network:
    external: true

services:
  postgres:
    networks:
      - shared-network
yaml 复制代码
# compose2.yml
networks:
  shared-network:
    external: true

services:
  app:
    networks:
      - shared-network

先创建网络:

lua 复制代码
docker network create shared-network

这样两个 compose 项目的容器就可以互相通信了。

端口映射的细节

yaml 复制代码
# 只监听 localhost(更安全)
-p 127.0.0.1:6379:6379

# 监听所有网络接口(谨慎使用)
-p 6379:6379

# 随机宿主机端口
-p 6379

生产环境建议:

  • 数据库等敏感服务只暴露给内网或特定 IP
  • 用 Nginx 等反向代理统一管理对外端口

Docker Compose 的进阶用法

多环境配置

我用三个文件管理不同环境:

yaml 复制代码
# docker-compose.yml(基础配置)
services:
  app:
    build: .
    env_file: .env

# docker-compose.dev.yml(开发环境覆盖)
services:
  app:
    volumes:
      - ./src:/app/src
    command: npm run dev

# docker-compose.prod.yml(生产环境覆盖)
services:
  app:
    restart: always
    logging:
      driver: "json-file"
      options:
        max-size: "10m"

使用:

bash 复制代码
# 开发环境
docker compose -f docker-compose.yml -f docker-compose.dev.yml up

# 生产环境
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

depends_on 的陷阱

depends_on 只保证启动顺序 ,不保证服务就绪

yaml 复制代码
services:
  app:
    depends_on:
      - postgres  # 只保证 postgres 先启动,不保证数据库已准备好

实际上 Postgres 可能还在初始化,app 连接会失败。

解决方案1:在 app 代码里加重试逻辑

ini 复制代码
async function connectDB(retries = 5) {
  for (let i = 0; i < retries; i++) {
    try {
      await pool.connect();
      return;
    } catch (err) {
      if (i === retries - 1) throw err;
      await sleep(2000);
    }
  }
}

解决方案2:用 wait-for-it.sh 等工具

yaml 复制代码
services:
  app:
    depends_on:
      - postgres
    command: >
      sh -c "
        ./wait-for-it.sh postgres:5432 --
        npm start
      "

健康检查

定义服务真正"就绪"的条件:

yaml 复制代码
services:
  postgres:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
  
  app:
    depends_on:
      postgres:
        condition: service_healthy

现在 app 会等到 Postgres 真正就绪后再启动。

Dockerfile 最佳实践

多阶段构建减小镜像体积

sql 复制代码
# 构建阶段
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

# 运行阶段
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]

这样最终镜像只包含运行时需要的文件,不包含源码和开发依赖。

我的一个 Next.js 项目从 1.2GB 降到 180MB。

.dockerignore 很重要

bash 复制代码
node_modules
.git
.env
*.log
dist
coverage

避免把不必要的文件复制到镜像里,加快构建速度,减小镜像体积。

使用特定版本的基础镜像

ruby 复制代码
# ❌ 不推荐:latest 版本不稳定
FROM node:latest

# ✅ 推荐:固定版本
FROM node:20.10.0-alpine

# ✅ 更好:使用 SHA256
FROM node:20.10.0-alpine@sha256:abc123...

这避免了"我本地能跑,生产环境不行"的问题。

实际踩过的坑

坑1:容器时区问题

容器内默认 UTC 时区,导致日志时间不对。

解决:

bash 复制代码
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

坑2:PID 1 僵尸进程

容器内的主进程是 PID 1,需要负责回收子进程。如果用 shell 脚本启动应用,会产生僵尸进程。

csharp 复制代码
# ❌ 不推荐
CMD npm start

# ✅ 推荐:使用 exec 或 tini
CMD ["node", "dist/index.js"]

# ✅ 或使用 tini
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/index.js"]

坑3:Docker in Docker

在 CI/CD 中需要在容器里构建镜像,不要用 Docker in Docker(DinD),用 Docker socket 映射:

javascript 复制代码
services:
  ci:
    image: docker:24
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

这样容器可以调用宿主机的 Docker daemon,避免嵌套 Docker 的各种问题。

性能优化建议

1. 使用 BuildKit

ini 复制代码
export DOCKER_BUILDKIT=1
docker build .

BuildKit 提供并行构建、更好的缓存、构建秘钥管理等特性。

2. 清理无用资源

perl 复制代码
# 清理悬空镜像
docker image prune

# 清理停止的容器
docker container prune

# 清理无用的 volume
docker volume prune

# 一键清理所有(谨慎使用)
docker system prune -a

定期清理,否则磁盘会爆满。

3. 限制容器资源

yaml 复制代码
services:
  app:
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 2G
        reservations:
          cpus: '0.5'
          memory: 512M

避免单个容器吃掉所有资源。

总结

Docker 的学习曲线不在于命令本身,而在于理解:

  • 镜像的分层机制和缓存策略
  • 容器的生命周期和状态管理
  • 数据持久化的不同方案和适用场景
  • 网络通信的细节和安全考虑
  • Compose 的编排能力和多环境管理

这些理解都来自实际项目中的踩坑和总结。建议多动手实践,特别是搭建一个完整的多容器项目(前端 + 后端 + 数据库 + 缓存),会遇到很多书上没有的细节问题。

后续计划深入研究 Docker 的网络驱动(bridge、overlay、macvlan)和容器安全加固(AppArmor、Seccomp)。

相关推荐
Rover.x3 小时前
Spring国际化语言切换不生效
java·后端·spring
IT_陈寒4 小时前
Redis 7个性能优化技巧,让我们的QPS从5k提升到20k+
前端·人工智能·后端
百锦再4 小时前
金仓数据库提出“三低一平”的迁移理念
开发语言·数据库·后端·python·rust·eclipse·pygame
ZHE|张恒4 小时前
深入理解 Spring 原理:IOC、AOP 与事务管理
java·后端·spring
expect7g5 小时前
Flink-To-Paimon 读取机制
大数据·后端·flink
kida_yuan5 小时前
【从零开始】18. 持续优化模型微调
后端·llm
倚栏听风雨5 小时前
Agent 认知+ReAct模式
后端
申阳5 小时前
Day 5:03. 基于Nuxt开发博客项目-页面结构组织
前端·后端·程序员