上一篇记录了如何编写 Dockerfile、使用 Compose 启动 Node.js 后端和 PostgreSQL,以及容器的基本启停操作。这一篇继续整理我在实际部署中更常用的内容:镜像管理、数据持久化、容器网络、配置管理、日志、备份、更新和故障排查。

本文以 Linux/WSL、Node.js 后端、PostgreSQL 和单机服务器为例。现在 Docker Compose v2 已经集成在 Docker CLI 中,因此命令统一写成:

docker compose ...

旧资料中常见的 docker-compose ... 属于 Compose v1 写法。如果本机已安装 Compose v2,应优先使用前一种形式。可先查看版本:

docker --version
docker compose version
本文命令中的项目目录默认包含 compose.yml。在执行删除、清理或数据库恢复命令前,一定先确认当前目录、容器名和备份文件。

三. Docker 镜像管理

镜像可以理解为容器的只读模板,而容器是镜像的一次运行实例。同一个镜像可以启动多个相互隔离的容器。

1. 查看镜像

docker image ls

# 查看包括中间镜像在内的所有镜像
docker image ls -a

# 只显示镜像 ID
docker image ls -q

镜像列表中的 REPOSITORY 是镜像名,TAG 是标签,IMAGE ID 是镜像标识。标签不是镜像的复制品,而是指向镜像的一种便于阅读的名称。

2. 拉取镜像

docker pull postgres:16-alpine
docker pull node:22-alpine

如果不写标签,Docker 默认使用 latest。但 latest 只是一个普通标签,并不保证它是最新、最稳定或与旧版本兼容。服务器部署时最好至少固定主版本,要求更严格时还可以固定完整版本或镜像摘要。

3. 构建和标记镜像

# 在当前目录构建镜像
docker build -t anime-backend:1.0.0 .

# 不使用构建缓存,适合排查缓存造成的问题
docker build --no-cache -t anime-backend:1.0.0 .

# 为已有镜像增加一个标签
docker tag anime-backend:1.0.0 registry.example.com/anime-backend:1.0.0

每次正式发布使用独立版本号比一直覆盖 latest 更方便回滚。例如保留 1.0.01.1.0,出问题时只需把 Compose 中的镜像标签改回旧版本。

4. 查看镜像详细信息和构建历史

docker image inspect anime-backend:1.0.0
docker history anime-backend:1.0.0

inspect 可以查看镜像入口命令、环境变量、架构等信息;history 可以帮助判断哪一层占用了较多空间。

5. 导出和导入镜像

服务器无法直接访问镜像仓库时,可以先在本机导出,再上传到服务器:

# 导出镜像,保留镜像名和标签
docker save -o anime-backend-1.0.0.tar anime-backend:1.0.0

# 在另一台机器导入
docker load -i anime-backend-1.0.0.tar

docker save/load 操作的是镜像。另有一组 docker export/import 操作容器文件系统,但不会完整保留镜像历史和元数据,一般不适合作为常规镜像迁移方案。

6. 删除和清理镜像

# 删除指定镜像
docker image rm anime-backend:1.0.0

# 删除悬空镜像
docker image prune

# 删除所有没有被容器使用的镜像
docker image prune -a
警告: docker image prune -a 的范围比普通 prune 更大。执行前建议先用 docker system df 查看占用,并确认旧镜像不再用于回滚。

四. 容器管理进阶

1. 查看全部容器和资源使用情况

# 包含已停止的容器
docker ps -a

# 实时查看 CPU、内存和网络使用量
docker stats

# 只查看一次,不持续刷新
docker stats --no-stream

如果某个服务突然变慢或服务器内存不足,docker stats 往往是第一站。

2. 进入容器或执行命令

# 进入带有 bash 的容器
docker exec -it <container_name> bash

# Alpine 等精简镜像通常只有 sh
docker exec -it <container_name> sh

# 不进入交互终端,直接执行一条命令
docker exec <container_name> node --version

# Compose 项目中按服务名执行
docker compose exec backend node --version
docker compose exec db psql -U postgres -d anime

容器内临时修改的文件会随着容器重建而丢失。正式修改应该写进源代码、镜像构建过程或挂载的配置文件中,而不是把运行中的容器当作普通服务器长期手改。

3. 在宿主机和容器之间复制文件

# 从容器复制到当前目录
docker cp <container_name>:/app/logs/error.log ./error.log

# 从宿主机复制到容器
docker cp ./config.json <container_name>:/app/config.json

这适合临时排查。需要长期存在或频繁修改的文件,应该使用数据卷或绑定挂载。

4. 查看容器详细状态

docker inspect <container_name>

# 只查看退出码
docker inspect -f '{{.State.ExitCode}}' <container_name>

# 查看是否因为内存不足而被系统终止
docker inspect -f '{{.State.OOMKilled}}' <container_name>

# 查看容器中运行的进程
docker top <container_name>

常见退出码中,0 表示正常退出,137 常见于进程收到 SIGKILL,可能是内存不足或被强制停止;143 通常表示收到 SIGTERM 后退出。退出码只能提供方向,还要结合日志和 inspect 判断。

5. 配置自动重启

services:
  backend:
    restart: unless-stopped

常见策略:

策略作用
no不自动重启,默认值
on-failure进程异常退出时重启
always无论退出原因都尝试重启
unless-stopped自动重启,但被管理员主动停止后保持停止

个人服务器通常可以使用 unless-stopped。不过自动重启不能代替排障;如果应用不断崩溃,容器可能陷入重启循环。

6. 限制容器资源

在单机 Compose 中,可以直接设置 CPU 和内存限制:

services:
  backend:
    cpus: 1.0
    mem_limit: 512m

修改后重建容器:

docker compose up -d --force-recreate backend

限制资源可以防止一个服务拖垮整台服务器,但设置太小会造成频繁 OOM 或明显变慢。应先用 docker stats 观察正常峰值,再留出余量。

五. 数据卷与数据持久化

容器本身是可替换的。数据库、用户上传文件等重要数据不能只放在容器可写层中,否则删除并重建容器后数据可能消失。

1. 命名卷和绑定挂载

Docker 常用的持久化方式主要有两种:

  • 命名卷(named volume):由 Docker 管理存储位置,适合数据库数据。
  • 绑定挂载(bind mount):把宿主机的明确路径映射进容器,适合配置文件、开发源码和需要直接查看的文件。
services:
  db:
    image: postgres:16-alpine
    volumes:
      - db_data:/var/lib/postgresql/data

  backend:
    volumes:
      - ./uploads:/app/uploads
      - ./config/production.json:/app/config/production.json:ro

volumes:
  db_data:

末尾的 :ro 表示容器只能读取该文件,可以降低程序意外修改配置的风险。

2. 查看和检查数据卷

docker volume ls
docker volume inspect <volume_name>

# 查看当前 Compose 项目实际使用的卷名
docker compose config --volumes

Compose 通常会给卷名加上项目名前缀。例如项目名为 animedb_data 的实际名称可能是 anime_db_data。不要只凭猜测删除卷,应先执行 docker volume lsdocker volume inspect

3. PostgreSQL 备份

下面将数据库导出为 PostgreSQL 自定义格式,备份文件保存在宿主机当前目录:

docker compose exec -T db pg_dump \
  -U postgres \
  -d anime \
  -Fc \
  > anime-$(date +%F-%H%M%S).dump

如果使用纯 SQL 格式:

docker compose exec -T db pg_dump \
  -U postgres \
  -d anime \
  > anime-backup.sql

这里的 -T 会关闭伪终端,避免重定向出来的备份混入终端控制字符。备份完成后应检查文件大小,并定期在测试环境尝试恢复;“有备份文件”不等于“备份一定可用”。

4. PostgreSQL 恢复

恢复自定义格式备份:

docker compose exec -T db pg_restore \
  -U postgres \
  -d anime \
  --clean \
  --if-exists \
  < anime-backup.dump

恢复纯 SQL:

docker compose exec -T db psql \
  -U postgres \
  -d anime \
  < anime-backup.sql
警告: 恢复操作可能覆盖、删除或重复写入现有数据库对象,尤其是带有 --clean 时。正式恢复前应停止后端写入、再次备份当前数据库,并先在测试数据库验证备份。

5. 删除数据卷

# 停止并删除容器和网络,但保留命名卷
docker compose down

# 同时删除 Compose 声明的命名卷
docker compose down -v

# 清理所有没有被容器使用的卷
docker volume prune
严重警告: docker compose down -vdocker volume prune 可能永久删除数据库数据。不要把它们当作普通的停止命令,也不要在未确认备份可恢复时执行。

六. Docker 网络与容器间通信

1. Compose 默认网络

执行 docker compose up 时,Compose 默认会为项目创建一个网络,并让同一项目中的服务加入该网络。容器之间可以通过服务名通信:

backend  --->  db:5432

因此后端连接数据库时,主机名应该是 db,而不是 localhost

DATABASE_URL=postgresql://postgres:password@db:5432/anime

容器中的 localhost 指向容器自己。后端容器连接 localhost:5432,实际上是在寻找后端容器内部的 PostgreSQL,自然会失败。

2. 端口映射不是容器互联的必要条件

services:
  db:
    image: postgres:16-alpine
    # 同一 Compose 网络中的 backend 可以直接访问 db:5432
    # 因此通常不需要写 ports

ports 是把容器端口发布到宿主机,用于从宿主机或外部访问。容器之间通过 Compose 网络通信时,不需要发布数据库端口。

如果只有宿主机本地工具需要连接数据库,可以仅绑定回环地址:

ports:
  - "127.0.0.1:5432:5432"

这样不会监听服务器的所有网络接口。除非已经配置防火墙、访问控制和加密,否则不要把 PostgreSQL 的 5432 端口直接暴露到公网。

3. 查看和排查网络

docker network ls
docker network inspect <network_name>

# 在后端容器中测试服务名解析
docker compose exec backend getent hosts db

# 查看容器加入了哪些网络
docker inspect -f '{{json .NetworkSettings.Networks}}' <container_name>

精简镜像可能没有 pingcurlgetent。不要为了排障直接永久修改生产容器,可以临时启动一个网络工具容器,或使用应用运行时自带的网络能力进行测试。

七. 一份更适合服务器的 Compose 示例

下面是一份完整示例。它不是唯一正确答案,但包含了健康检查、自动重启、资源限制、日志轮转和最小端口暴露等常见配置:

name: anime

services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 20s
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  backend:
    image: registry.example.com/anime-backend:1.2.0
    restart: unless-stopped
    ports:
      - "127.0.0.1:4000:4000"
    environment:
      NODE_ENV: production
      PORT: 4000
      DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test:
        - CMD
        - node
        - -e
        - "fetch('http://127.0.0.1:4000/health').then(r => { if (!r.ok) process.exit(1) }).catch(() => process.exit(1))"
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s
    cpus: 1.0
    mem_limit: 512m
    security_opt:
      - no-new-privileges:true
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

volumes:
  db_data:

配套 .env

POSTGRES_USER=anime_app
POSTGRES_PASSWORD=replace_with_a_long_random_password
POSTGRES_DB=anime

使用前需要注意:

  1. 把示例镜像地址替换为自己的镜像。
  2. 后端需要实现 /health,并且镜像中的 Node.js 版本需要支持示例中的 fetch;否则应换成镜像内实际可用的健康检查命令。
  3. 127.0.0.1:4000:4000 只允许宿主机访问后端,适合由同机 Nginx 或 Caddy 反向代理。如果需要直接从外部访问,再根据防火墙和安全策略调整。
  4. .env 中的密码只是演示占位内容,部署时必须换成长随机密码。

先检查 Compose 最终配置,再启动:

docker compose config
docker compose up -d
docker compose ps

docker compose config 会解析变量并输出合并后的配置。输出中可能包含敏感值,不要把结果随意贴到公开日志或工单中。

八. Compose 项目的常用进阶操作

1. 查看服务状态和日志

docker compose ps
docker compose top

# 查看所有服务最近 100 行日志并持续跟踪
docker compose logs --tail=100 -f

# 只查看后端
docker compose logs --tail=100 -f backend

# 查看最近 30 分钟的日志
docker compose logs --since=30m backend

2. 启停、重启与重建

# 后台启动
docker compose up -d

# 停止服务,但保留容器
docker compose stop

# 再次启动已停止的容器
docker compose start

# 重启服务;不会自动应用 compose.yml 的配置变化
docker compose restart backend

# 应用配置或镜像变化,必要时重建容器
docker compose up -d backend

# 强制重建
docker compose up -d --force-recreate backend

# 删除项目容器和默认网络,默认保留命名卷
docker compose down

一个容易忽略的点是:restart 只重启现有容器,不会把修改后的环境变量、端口或挂载应用进去。配置变更后应使用 docker compose up -d 重建对应服务。

3. 重新构建本地镜像

backend 使用 build: . 时:

docker compose build backend
docker compose up -d backend

# 一条命令完成构建和启动
docker compose up -d --build backend

# 完全忽略构建缓存
docker compose build --no-cache backend

4. 更新仓库镜像

当服务使用 image: 时:

docker compose pull backend
docker compose up -d backend
docker compose ps
docker compose logs --tail=100 backend

仅执行 pull 不会让正在运行的容器自动切换到新镜像,仍需执行 up -d

5. Profiles:按需启动辅助服务

不需要一直运行的管理工具可以放进 profile:

services:
  adminer:
    image: adminer:4
    profiles: ["tools"]
    ports:
      - "127.0.0.1:8080:8080"

默认启动时不会运行 adminer,需要时执行:

docker compose --profile tools up -d adminer

6. 使用多个 Compose 文件覆盖配置

可以把公共配置放在 compose.yml,把生产环境差异放在 compose.prod.yml

docker compose -f compose.yml -f compose.prod.yml config
docker compose -f compose.yml -f compose.prod.yml up -d

后面的文件会覆盖或扩展前面的配置。正式启动前一定先用 config 查看合并结果,避免端口、卷或环境变量的合并方式与预期不同。

九. 环境变量与敏感信息

1. .envenv_fileenvironment 的区别

  • 项目目录中的 .env 主要用于 Compose 文件里的 ${VARIABLE} 插值。
  • env_file 用于把指定文件中的变量传入容器。
  • environment 在 Compose 文件中直接设置容器环境变量,通常拥有更明确的覆盖效果。
services:
  backend:
    env_file:
      - ./backend.env
    environment:
      NODE_ENV: production

不要因为本地 .env 会自动参与 Compose 插值,就误以为其中所有变量都会自动进入容器。容器最终得到哪些变量,应通过 Compose 配置和应用行为明确决定。

2. 不要提交真实密钥

.gitignore 至少应包含:

.env
*.env
!*.env.example

同时提交一份不含真实值的 .env.example

POSTGRES_USER=anime_app
POSTGRES_PASSWORD=change_me
POSTGRES_DB=anime

如果密码已经被提交到 Git,仅从最新版本删除还不够,因为历史记录中仍可能存在。应立即轮换泄露的密码或令牌,再根据需要清理历史。

3. Compose secrets

单机 Compose 也可以把文件作为 secret 挂载到容器的 /run/secrets/,避免把敏感值直接写在 Compose 文件中:

services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

应用必须支持从文件读取 secret,不能假设挂载后会自动变成环境变量。单机 Compose 的文件型 secret 也不等同于完整的集中式密钥管理系统;仍要保护宿主机文件权限和备份。

十. 健康检查与日志管理

1. depends_on 不等于“依赖已经可用”

短写法:

depends_on:
  - db

主要表达启动顺序,并不保证数据库已完成初始化、能够接受连接。更可靠的做法是为数据库配置 healthcheck,再让后端等待 service_healthy,同时让应用本身实现数据库重试。健康检查和重试应该一起使用,因为运行期间数据库也可能短暂断开。

查看健康状态:

docker compose ps
docker inspect -f '{{json .State.Health}}' <container_name>

健康检查失败不会神奇地修复程序,也不一定自动重启容器;它首先提供的是可观察状态。是否重启还取决于编排方式、进程退出和重启策略。

2. 防止日志占满磁盘

Docker 默认日志如果不限制大小,运行久了可能占满服务器磁盘。可以在 Compose 中为每个服务配置轮转:

logging:
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

其中 max-size 表示单个日志文件最大大小,max-file 表示最多保留几个轮转文件。它只限制 Docker 捕获的标准输出和标准错误;如果应用还在容器内写独立日志文件,需要另外处理这些文件的轮转和持久化。

3. 查看 Docker 磁盘占用

docker system df
docker system df -v

先查看,再清理。不要看到磁盘紧张就直接复制一条“大扫除”命令执行。

十一. 服务更新与回滚

1. 使用本地构建的项目

一个稳妥的基本流程是:

# 1. 更新代码
git pull

# 2. 在数据库迁移前备份
docker compose exec -T db pg_dump -U postgres -d anime -Fc > before-update.dump

# 3. 构建镜像
docker compose build backend

# 4. 重建后端
docker compose up -d backend

# 5. 检查状态和日志
docker compose ps
docker compose logs --tail=100 backend

实际项目还应在发布前运行测试。若更新包含数据库迁移,应确认迁移是否向后兼容,以及旧版本应用能否继续使用迁移后的数据库。

2. 使用镜像仓库的项目

建议每次发布使用不可变版本标签:

services:
  backend:
    image: registry.example.com/anime-backend:1.2.0

发布 1.3.0 时修改标签,然后:

docker compose config
docker compose pull backend
docker compose up -d backend
docker compose ps
docker compose logs --tail=100 backend

3. 回滚

如果新版本异常,并且数据库结构仍兼容旧版,把镜像标签改回 1.2.0

docker compose pull backend
docker compose up -d backend
docker compose ps

回滚应用镜像通常比较容易,数据库回滚则危险得多。不要在没有验证的情况下直接恢复旧备份,因为这会丢失备份时间点之后的新数据。数据库变更应优先设计为可向后兼容,并提前准备专门的回滚方案。

4. 关于“零停机”

单个 Compose 服务只有一个容器时,重建期间通常会有短暂停机。真正的零停机往往需要至少两个应用实例、健康检查、反向代理流量切换,以及兼容滚动发布的数据库迁移。对个人项目而言,先把备份、健康检查、快速回滚和维护窗口做好,通常比贸然追求复杂的“零停机”更实用。

十二. 常见故障排查

1. 容器启动后立即退出

docker compose ps -a
docker compose logs --tail=200 backend
docker inspect -f '{{.State.ExitCode}} {{.State.Error}}' <container_name>

优先查看应用启动命令、缺失的环境变量、文件权限、端口占用和数据库连接错误。不要只反复执行 restart,否则只是让错误重复发生。

2. 端口被占用

典型错误是 address already in use。Linux 上可检查:

sudo ss -lntp | grep ':4000'
docker ps --format 'table {{.Names}}\t{{.Ports}}'

找到占用端口的进程或容器后,再决定停止旧服务还是修改端口映射。

3. 后端无法连接数据库

依次检查:

docker compose ps
docker compose logs --tail=100 db
docker compose exec backend getent hosts db
docker compose exec db pg_isready -U postgres -d anime

重点确认数据库主机名是否为服务名 db、端口是否为容器端口 5432、用户名和数据库名是否一致,以及密码中的特殊字符在 URL 中是否需要编码。

4. 挂载目录没有权限

# 查看容器进程使用的用户
docker compose exec backend id

# 查看宿主机目录权限
ls -ld ./uploads

绑定挂载会受到宿主机 UID、GID 和权限影响。应把目录所有者或权限调整到应用实际需要的范围,不要为了省事直接使用 chmod -R 777,更不要把容器长期改成 root 运行来掩盖权限问题。

5. 镜像拉取失败

docker login registry.example.com
docker pull registry.example.com/anime-backend:1.2.0

检查镜像名、标签、仓库登录状态、DNS、代理和服务器剩余空间。私有仓库凭据应通过受控方式保存,不要写进 Dockerfile。

6. 磁盘空间不足

df -h
docker system df -v
docker ps -a
docker image ls
docker volume ls

确认空间去向后,才选择性清理:

docker container prune
docker image prune
docker builder prune

更彻底的命令:

docker system prune -a
警告: docker system prune -a 会删除所有未被现有容器使用的镜像以及其他未使用对象,可能清掉备用回滚镜像。它默认不删除卷,但仍应逐项阅读确认提示;不要随意追加 --volumes

7. 容器因内存不足退出

docker stats --no-stream
docker inspect -f '{{.State.OOMKilled}}' <container_name>
dmesg -T | grep -i -E 'out of memory|oom'

如果 OOMKilledtrue,需要检查应用内存泄漏、并发量、缓存大小和资源限制,而不是只把重启策略改成 always

8. Compose 文件实际配置与预期不同

docker compose config
docker compose config --services

这可以发现变量没有加载、多文件覆盖结果异常、服务名写错等问题。注意配置输出可能包含展开后的密码。

十三. 安全与日常维护建议

  1. 尽量让应用使用非 root 用户。 可以在 Dockerfile 中使用官方镜像提供的用户,或创建专用用户。运行时再通过 docker compose exec backend id 验证。
  2. 只暴露真正需要的端口。 数据库通常不需要发布端口;后端若由同机反向代理访问,可以只绑定 127.0.0.1
  3. 固定镜像版本。 至少固定主版本,重要生产环境使用完整版本或 digest,并保留可回滚版本。
  4. 不要把密钥写进镜像。 Dockerfile 中的密钥可能留在构建历史或镜像层中。
  5. 限制资源和日志。 防止单个容器或无限增长的日志拖垮整台服务器。
  6. 定期更新基础镜像和依赖。 更新前先测试,并关注不兼容变更。
  7. 备份要异地保存。 备份与数据库放在同一块磁盘上,磁盘损坏时可能一起丢失。
  8. 定期做恢复演练。 未验证过的备份只能算“可能可用”。
  9. 谨慎挂载 Docker Socket。 /var/run/docker.sock 几乎等同于宿主机高权限,不要随意挂给业务容器。
  10. 不要随意使用 --privileged 它会显著削弱容器隔离,普通 Web 应用通常不需要。

十四. 常用命令速查表

查看状态

docker compose ps
docker compose top
docker compose logs --tail=100 -f backend
docker stats
docker system df -v

启动、停止和重建

docker compose up -d
docker compose stop
docker compose start
docker compose restart backend
docker compose up -d --force-recreate backend
docker compose down

构建和更新

docker compose build backend
docker compose up -d --build backend
docker compose pull backend
docker compose up -d backend

调试

docker compose exec backend sh
docker compose exec db psql -U postgres -d anime
docker inspect <container_name>
docker inspect -f '{{.State.ExitCode}}' <container_name>
docker inspect -f '{{.State.OOMKilled}}' <container_name>

备份

docker compose exec -T db pg_dump -U postgres -d anime -Fc > anime-backup.dump

清理

docker container prune
docker image prune
docker builder prune
清理前先执行 docker system df -v。涉及 -a--volumesvolume prunedown -v 时尤其要小心,以免删除回滚镜像或持久化数据。

结语

使用 Docker 一段时间后,我逐渐意识到,“把程序装进容器”只是开始。真正影响服务是否可靠的,往往是数据有没有备份、端口是否合理暴露、日志会不会塞满磁盘、更新失败后能不能快速回滚,以及出了问题能否沿着日志、状态、网络和资源占用逐步定位。

对个人服务器而言,不必一开始就引入非常复杂的编排系统。把 Compose 配置、数据卷、健康检查、日志轮转、版本标签、备份恢复和基础监控做好,已经足以让大多数小型项目运行得更踏实。Docker 的价值也正在于此:不仅让应用“在我的机器上能跑”,还让部署和维护过程变得可重复、可检查、可恢复。

最后修改:2026 年 06 月 23 日
支持一下吧~