跳转至正文内容

独立技术知识论坛 主机玩家与运维交流社区 • 与德国 netcup GmbH 无官方隶属关系。

NETCUP GUIDE
知识论坛 · 实时优惠码池
运维教程 难度:中级进阶 耗时:35 分钟 // 阅读约 18 分钟 • 核验于

如何在 Netcup 上安装 Docker Compose v2 并构建高安全性的多容器生产架构

基于 Python 的早期 docker-compose v1 独立二进制文件早已终止生命周期(End-of-Life),不仅停止接收安全补丁,而且无法兼容现代 BuildKit 构建特性与最新 Compose Specification 规范。在 Netcup KVM 架构的 VPS 与独立物理核心 Root Server 实例上,生产环境的容器编排完全依赖于采用 Go 语言重构的 Docker Compose v2 CLI 插件(docker compose)。本系统工程指南深入讲解经过签名验证的官方插件安装、多层网络隔离(internal: true 气隙后端网桥)、基于 service_healthy 探针的确定性启动顺序、针对 8 GB 内存 Netcup 节点量身定制的 cgroupv2 内存与 CPU 预算控制、基于文件的 Docker Secrets 密钥管理以及 systemd 生命周期单元封装。

楼主 (OP) // 实测发起人 Marcus Vance 首席云基础设施架构师(欧洲 KVM 与网络工程)
版主复核 // 共同维护 Elena Rostova 高级 Linux 系统与安全审计工程师
帖子状态 // 最后修订 社区核查规范 →
版主按语 // 实测环境与社区透明说明 已在搭载 Linux 6.8 内核与 cgroupv2 资源控制器的 Netcup KVM 实例(Ubuntu 24.04 LTS 与 Debian 12 Bookworm)上,基于 docker-compose-plugin v2.29+ 完成全部验证。

环境要求 // 前置条件清单

检查清单
  • ✓ 一台运行 Ubuntu 24.04 LTS 或 Debian 12 的 Netcup VPS(如配备 8 GB 内存的 VPS 1000 G12.5)或 Root Server(如配备 8 GB ECC 内存的 RS 1000 G12.5)
  • ✓ 已从 Docker 官方 APT 软件源安装 Docker Engine (docker-ce)
  • ✓ 具备 sudo 权限与 SSH 访问能力的非 root 管理员账户

核心技术结论 (Key Takeaways)

核心摘要
  • 01 仅从 Docker 官方 APT 软件源安装 docker-compose-plugin,确保在系统安全更新时 Compose 插件能够与 docker-ce 引擎保持原子级同步升级。
  • 02 将每个应用栈严格划分为至少两个独立网桥:一个用于反向代理流量接入的 frontend 前端网桥,以及一个设置 internal: true 的气隙隔离后端网桥(专供数据库与缓存使用)。
  • 03 在 compose.yaml 中强制声明 deploy.resources.limits(内存与 CPU 上限),防止失控的 Node.js 或 Java 容器耗尽宿主机物理内存并触发 Linux 内核 OOM Killer 误杀 sshd 或 PostgreSQL 进程。
  • 04 摒弃在环境变量中明文传递数据库密码的做法,改用权限为 chmod 0400 且属主为 root 的凭据文件配合 Docker secrets: 挂载至容器内存文件系统。
  • 05 使用 depends_on: condition: service_healthy 并配合原生 CLI 就绪探针(如 pg_isready、redis-cli ping)构建启动依赖链,而非仅依赖容器进程启动顺序。
📑 本文目录导航(共 5 节) ▼
§ 01

第 1 步:清理遗留 v1 二进制文件并安装 Docker Compose v2 CLI 插件

在 Netcup VPS 或 Root Server 上部署 Docker Compose v2 之前,首要任务是排查宿主机中是否残留已废弃的 v1 Python 脚本(常见路径为 /usr/local/bin/docker-compose)或 Debian/Ubuntu 官方 universe 仓库中严重过时的 docker-compose 软件包。如果 /usr/bin 与 /usr/local/bin 下同时存在新旧两个版本的二进制文件,CI/CD 自动化部署流水线和定时维护脚本极易因 $PATH 环境变量的优先级差异而调用错误的解析器,从而触发隐蔽的 YAML Schema 解析故障。

Docker Compose v2 采用 Go 语言静态编译,作为原生插件直接集成在 Docker CLI 插件架构目录 /usr/libexec/docker/cli-plugins/docker-compose 下。由于它直接通过原生 Go 客户端库与 containerd 及 dockerd 的 Unix 套接字通信,不再经过繁重的 Python 解释器包装层,因此在执行多容器状态收敛、并行镜像拉取(Parallel Image Pulls)以及实时日志流聚合时,其 CPU 占用率与内存开销均大幅降低,尤其适合高密度部署的 Netcup 服务器。

此外,现代 Docker Compose v2 与 docker-buildx-plugin(BuildKit 引擎)深度耦合。当您在生产环境执行 docker compose build 时,Compose v2 会自动调用 BuildKit 实现多阶段构建的并发图层缓存、构建期密钥安全挂载(--secret)以及跨平台镜像编译,解决了早期版本构建速度慢且容易残留悬空中间镜像层的痛点。

架构对比:遗留 docker-compose v1 与现代 Docker Compose v2 插件
功能与架构属性遗留 Compose v1 (docker-compose)现代 Compose v2 (docker compose)
运行时与开发语言Python 3 解释器 + pip 依赖库静态编译的 Go 二进制 CLI 插件
Ubuntu/Debian 二进制路径/usr/bin/docker-compose 或 /usr/local/bin//usr/libexec/docker/cli-plugins/docker-compose
配置文件规范 (Schema)锁定在割裂的 v2.x / v3.x 版本规范统一的 Compose Specification(无需声明 version: 字段)
cgroupv2 资源配额限制必须开启 Swarm 模式才能解析 deploy.resources单机模式下原生强制执行 deploy.resources.limits
镜像构建引擎遗留的单线程同步 Docker 构建器原生集成 BuildKit (docker-buildx-plugin) 并支持并发缓存
安全维护状态已终止维护 (2023 年起正式废弃)通过 Docker 官方 APT 仓库持续提供安全更新
立即移除已废弃的顶层 version: 声明字段

在 Docker Compose v2 中,compose.yaml 顶层的 version: "3.8" 或 version: "3.9" 声明已被官方正式废弃,保留该字段会触发每次命令执行时的黄色弃用警告。Compose v2 采用滚动演进的 Compose Specification 规范——请直接以 name: 或 services: 作为 compose.yaml 的起始行。

验证官方 APT 软件源并安装 docker-compose-plugin

首先卸载系统内可能冲突的发行版自带软件包,确认 Docker 官方 GPG 密钥环已正确注册在 /etc/apt/keyrings/docker.asc,随后同时安装 docker-compose-plugin 与 docker-buildx-plugin。安装完成后,立即核验二进制插件权限与运行时版本号,确保 CLI 子命令注册正常。

清理遗留二进制文件并安装官方 Docker Compose v2 插件
bash
# 1. 卸载遗留的 v1 软件包并删除手动下载的旧版二进制文件
sudo apt remove -y docker-compose || true
sudo rm -f /usr/local/bin/docker-compose

# 2. 更新 APT 软件源索引并安装官方 Compose v2 与 Buildx 插件
sudo apt update
sudo apt install -y docker-compose-plugin docker-buildx-plugin

# 3. 验证插件二进制文件路径与运行时版本
ls -la /usr/libexec/docker/cli-plugins/
docker compose version
docker buildx version

命令与参数详解: 清理遗留的 v1 安装包与手动下载的二进制文件,通过 APT 安装官方 Go 语言版 Compose v2 与 Buildx CLI 插件,并验证插件安装路径及版本输出。

预期终端输出
-rwxr-xr-x 1 root root 64231808 Oct 24 14:12 docker-compose
Docker Compose version v2.29.7
github.com/docker/buildx v0.17.1

安全与生产运维提示: 切勿通过未经签名校验的 curl 命令直接从 GitHub Releases 下载二进制文件并写入 /usr/local/bin。使用经过 GPG 签名验证的官方 APT 软件源,能够让系统自带的 unattended-upgrades 自动静默修复高危 CVE 漏洞。

§ 02

第 2 步:构建加固的 /opt/stacks 目录规范与 Secrets 密钥架构

随手将容器编排文件零散放置在 /home/ubuntu 或 /root 目录下,不仅会导致多管理员协作运维时路径混乱,还会导致后续部署自动化 Restic 异地备份脚本时遗漏关键配置。在生产环境的 Netcup 服务器上,推荐将所有 Docker Compose 工作负载统一存放在 /opt/stacks/<stack-name> 目录树下,并施以严格的 POSIX 权限控制(0750 属组为 root:docker)。

更重要的是,通过内联 environment: 环境变量直接传递高价值敏感凭据(例如 PostgreSQL 超级用户密码、JWT 签名私钥或 S3 对象存储 API 令牌)存在明显的安全隐患:任何拥有 docker 用户组权限的进程或开发人员,只需执行 docker inspect <container> 即可从容器元数据 JSON 中直接读取所有明文环境变量;甚至当 Node.js 或 Python 应用在未捕获异常时将 process.env 或 os.environ 序列化写入堆栈日志,这些数据库凭据也会直接泄露到外部日志平台中。

为了有效隔离敏感信息,我们应当将非敏感的运行时参数(如镜像版本号、时区、监听端口)存放在 .env 文件中供 Compose 进行变量插值,而将高敏感凭据作为只读文件保存在宿主机 0400 权限目录下,并通过 Compose 顶层的 secrets: 机制将其安全挂载到容器内部的 /run/secrets/<secret_name> 内存文件系统(tmpfs)中。生成密钥文件时,务必使用 tr -d "\n" 剥离尾部的换行符,防止数据库入口脚本将换行符误判为密码字符的一部分。

初始化 /opt/stacks/core-app 目录并生成高强度权限锁定的 Secret 密钥文件
bash
# 1. 创建标准化的应用栈目录结构并锁定权限
sudo mkdir -p /opt/stacks/core-app/{secrets,config,data}
sudo chown -R root:docker /opt/stacks/core-app
sudo chmod 0750 /opt/stacks/core-app
sudo chmod 0700 /opt/stacks/core-app/secrets

# 2. 使用 OpenSSL CSPRNG 生成去除尾部换行符的 32 字节高熵密码文件
openssl rand -base64 32 | tr -d '\n' | sudo tee /opt/stacks/core-app/secrets/postgres_password.txt > /dev/null
openssl rand -base64 32 | tr -d '\n' | sudo tee /opt/stacks/core-app/secrets/app_jwt_secret.txt > /dev/null
sudo chmod 0400 /opt/stacks/core-app/secrets/*.txt

# 3. 在 .env 中写入非敏感的运行时配置参数(供 Compose 变量插值使用)
sudo tee /opt/stacks/core-app/.env > /dev/null <<'EOF'
COMPOSE_PROJECT_NAME=core_app
TZ=Europe/Berlin
POSTGRES_VERSION=16.4-alpine3.20
REDIS_VERSION=7.4-alpine
APP_IMAGE=ghcr.io/example-org/core-api:1.14.2
APP_PORT=127.0.0.1:8080
EOF
sudo chmod 0600 /opt/stacks/core-app/.env
sudo ls -la /opt/stacks/core-app/secrets/ /opt/stacks/core-app/.env

命令与参数详解: 创建标准化的 /opt/stacks/core-app 编排目录,使用 OpenSSL CSPRNG 生成去除尾部换行符(tr -d "\n")的 32 字节高熵密码文件,将密钥文件权限锁定为 0400 (-r--------),并将镜像版本与回环绑定地址存入 .env。

预期终端输出
-rw------- 1 root docker 164 Oct 24 14:22 /opt/stacks/core-app/.env

/opt/stacks/core-app/secrets/:
total 16
drwx------ 2 root docker 4096 Oct 24 14:22 .
drwxr-x--- 5 root docker 4096 Oct 24 14:22 ..
-r-------- 1 root root     44 Oct 24 14:22 app_jwt_secret.txt
-r-------- 1 root root     44 Oct 24 14:22 postgres_password.txt

安全与生产运维提示: PostgreSQL、MariaDB 和 MySQL 的官方容器镜像均原生支持 _FILE 后缀约定(例如 POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password),容器入口脚本会直接从内存挂载点读取密码,从而不会在 docker inspect 或进程环境列表中暴露明文凭据。

§ 03

第 3 步:多层网络分段隔离(frontend 前端网桥与 internal: true 气隙后端网桥)

当您在单个 compose.yaml 中定义了多个服务却未显式配置自定义 networks: 时,Docker Compose 会将所有容器放置在同一个默认共享网桥(<project>_default)上。这意味着该网桥上的任何一个容器都可以扫描并连接其他所有容器的任意端口,且每个容器都默认具备通过宿主机 MASQUERADE NAT 规则访问公网的能力。

同时,理解 Docker 内置 DNS 解析器(固定监听在每个容器网络命名空间内的 127.0.0.11:53)的工作机制至关重要:只有同时挂载在同一个自定义网桥上的容器,才能通过服务名称(如 db 或 redis)互相解析 IP 地址。如果将边缘反向代理与后端数据库分别放置在不同的自定义网桥上,内核不仅会在二层/三层通过 DOCKER-ISOLATION-STAGE-1 与 DOCKER-ISOLATION-STAGE-2 iptables 规则链直接丢弃跨网桥数据包,而且边缘容器甚至无法解析出数据库容器的内部 IP。

为了在 Netcup 服务器上构建纵深防御体系,抵御内网横向移动(Lateral Movement)与服务端请求伪造(SSRF)攻击,我们将容器网络拓扑划分为两个相互隔离的 Linux 网桥(Bridge Networks):

  • ▪ frontend_net(外部/边缘接入网桥): 仅在边缘反向代理(Caddy / Traefik / Nginx)与上游 Web 应用容器(app)之间共享。如果您的 Web 应用程序需要主动调用外部第三方 Webhook 或支付 API,该网桥允许正常的出站公网连接。
  • ▪ backend_internal(internal: true 气隙隔离后端网桥): 仅用于连接 app 与后端的 postgres 数据库及 redis 缓存。设置 internal: true 会指示 Docker 的 iptables/nftables 网络驱动直接阻断来自该子网的所有默认网关 NAT 出站路由。即使攻击者利用应用层漏洞在 PostgreSQL 容器内触发了命令执行,或在 Redis 中执行了恶意 Lua 脚本,由于该容器处于气隙网络中,攻击者也无法向公网发起反向 Shell 连接或直接外传数据。
  • ▪ 显式子网固定(IPAM Subnet Pinning): 在 ipam.config 中显式声明静态 CIDR 地址段(例如 172.28.10.0/24 与 172.28.20.0/24),能够防止 Docker 随机分配网段时与您后续部署的 WireGuard VPN 隧道(10.x.x.x)或企业内网 RFC1918 路由表发生路由冲突。
核验 Linux 网桥隔离状态与 internal: true 防火墙阻断规则
bash
# 启动应用栈后(见第 5 步),检查生成的 Linux 网桥与 iptables 隔离规则链:
docker network ls
sudo iptables -S DOCKER-ISOLATION-STAGE-1
sudo iptables -S DOCKER-USER

命令与参数详解: 列出当前活动的 Docker 自定义网络,并检查 Linux 内核 netfilter 中用于强制执行不同自定义网桥之间流量隔离的 DOCKER-ISOLATION-STAGE-1 规则链。

预期终端输出
NETWORK ID     NAME                          DRIVER    SCOPE
8f3a1c92b104   core_app_backend_internal     bridge    local
c41e9021a77b   core_app_frontend_net         bridge    local
-P DOCKER-ISOLATION-STAGE-1 ACCEPT
-A DOCKER-ISOLATION-STAGE-1 -i br-8f3a1c92b104 ! -o br-8f3a1c92b104 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -i br-c41e9021a77b ! -o br-c41e9021a77b -j DOCKER-ISOLATION-STAGE-2
§ 04

第 4 步:cgroupv2 资源配额预算与确定性健康检查依赖链

在配备 8 GB 内存的 Netcup VPS 1000 G12.5(4 vCores)或 Root Server RS 1000 G12.5(4 颗独享 AMD EPYC 物理核心)上,不加资源限制地裸跑容器是引发服务器非计划性宕机的常见原因。如果突发流量洪峰或应用代码中的内存泄漏导致某个容器在几秒内吞噬 7.8 GB 内存,Linux 内核的全局内存溢出杀手(OOM Killer)会根据 /proc/<pid>/oom_score 评分终止占用内存最高的进程——往往在数据库事务刷盘一半时强行向 postgres 或 dockerd 发送 SIGKILL,造成表空间损坏。

Ubuntu 24.04 与 Debian 12 默认启用挂载于 /sys/fs/cgroup 的统一 cgroupv2 控制组层级结构。当您在 compose.yaml 中显式声明 deploy.resources.limits 与 reservations 时,Docker Compose v2 会将这些配额直接写入对应容器 cgroup 切片下的 memory.max、memory.low 以及 cpu.max 内核接口。一旦某个容器达到 memory.max 上限,内核仅在该容器独立的 cgroup 命名空间内触发 OOM 回收并重启该容器,从而保障宿主机操作系统、sshd 远程会话以及同机的 PostgreSQL 数据库稳定运行。

8 GB 内存 Netcup VPS 1000 G12.5 / RS 1000 G12.5 (4 核心) 推荐容器内存与 CPU 配额预算表
系统组件 / 容器服务CPU 限制 (cpus)内存软保留 (reservations)内存硬上限 (limits.memory)工程设计依据
Linux 宿主机系统、sshd、systemd-journald0.50 核 (不绑定)768M1024M保障 SSH 随时响应、netfilter 连接跟踪表及内核文件页缓存 (Page Cache)
边缘反向代理 (caddy / traefik)0.75 核128M384M处理高并发 TLS 1.3 握手、HTTP/3 QUIC UDP 缓冲区与 Zstd 实时压缩
核心 Web 应用程序 (app)1.50 核512M2048M限制应用堆内存上限(如 NODE_OPTIONS=--max-old-space-size=1536),防止波及宿主机
PostgreSQL 16 数据库 (db)1.50 核1024M3072M容纳 shared_buffers=768MB、work_mem 排序缓冲及 /dev/shm 并行查询工作进程
Redis 7 内存缓存 (redis)0.50 核256M768M在 768M 容器硬上限内强制执行 maxmemory 512mb 与 allkeys-lru 淘汰策略

包含健康检查、Secrets 密钥与资源硬限制的完整生产级 compose.yaml

将以下完整的编排配置保存至 /opt/stacks/core-app/compose.yaml。请重点观察 app 服务如何利用带有 condition: service_healthy 的 depends_on 声明:普通的 depends_on 仅等待容器 PID 1 进程被创建就立即启动下游应用,而此时 PostgreSQL 往往还在重放 WAL 预写日志,导致 Web 应用因连接被拒绝而崩溃重启。配合 pg_isready 与 redis-cli ping 健康探针后,Compose 引擎会严格等待数据库进入可接受 SQL 读写的状态后再启动 app 容器。

/opt/stacks/core-app/compose.yaml
yaml
name: ${COMPOSE_PROJECT_NAME:-core_app}

x-logging-defaults: &default-logging
  driver: "json-file"
  options:
    max-size: "25m"
    max-file: "4"
    compress: "true"

services:
  db:
    image: postgres:${POSTGRES_VERSION:-16.4-alpine3.20}
    container_name: core_app_db
    restart: unless-stopped
    user: "70:70"
    shm_size: 256mb
    environment:
      TZ: ${TZ:-UTC}
      POSTGRES_DB: core_prod
      POSTGRES_USER: core_user
      POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
    secrets:
      - postgres_password
    command:
      - "postgres"
      - "-c"
      - "shared_buffers=768MB"
      - "-c"
      - "effective_cache_size=2048MB"
      - "-c"
      - "work_mem=16MB"
      - "-c"
      - "maintenance_work_mem=192MB"
      - "-c"
      - "random_page_cost=1.1"
      - "-c"
      - "effective_io_concurrency=200"
      - "-c"
      - "log_min_duration_statement=250"
    volumes:
      - pgdata:/var/lib/postgresql/data
    networks:
      - backend_internal
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U core_user -d core_prod -h 127.0.0.1"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 20s
    deploy:
      resources:
        limits:
          cpus: "1.50"
          memory: 3072M
        reservations:
          cpus: "0.50"
          memory: 1024M
    security_opt:
      - no-new-privileges:true
    logging: *default-logging

  redis:
    image: redis:${REDIS_VERSION:-7.4-alpine}
    container_name: core_app_redis
    restart: unless-stopped
    command:
      - "redis-server"
      - "--appendonly"
      - "yes"
      - "--appendfsync"
      - "everysec"
      - "--maxmemory"
      - "512mb"
      - "--maxmemory-policy"
      - "allkeys-lru"
      - "--save"
      - "900 1"
      - "--save"
      - "300 10"
    volumes:
      - redisdata:/data
    networks:
      - backend_internal
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 10s
    deploy:
      resources:
        limits:
          cpus: "0.50"
          memory: 768M
        reservations:
          cpus: "0.15"
          memory: 256M
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    logging: *default-logging

  app:
    image: ${APP_IMAGE:-nginx:1.27-alpine}
    container_name: core_app_web
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    ports:
      - "${APP_PORT:-127.0.0.1:8080}:80"
    environment:
      TZ: ${TZ:-UTC}
      DB_HOST: db
      DB_PORT: 5432
      DB_NAME: core_prod
      DB_USER: core_user
      DB_PASSWORD_FILE: /run/secrets/postgres_password
      JWT_SECRET_FILE: /run/secrets/app_jwt_secret
      REDIS_URL: redis://redis:6379/0
    secrets:
      - postgres_password
      - app_jwt_secret
    networks:
      - frontend_net
      - backend_internal
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:80/ > /dev/null || exit 1"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 15s
    deploy:
      resources:
        limits:
          cpus: "1.50"
          memory: 2048M
        reservations:
          cpus: "0.25"
          memory: 512M
    security_opt:
      - no-new-privileges:true
    logging: *default-logging

secrets:
  postgres_password:
    file: ./secrets/postgres_password.txt
  app_jwt_secret:
    file: ./secrets/app_jwt_secret.txt

volumes:
  pgdata:
    driver: local
  redisdata:
    driver: local

networks:
  frontend_net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.10.0/24
  backend_internal:
    driver: bridge
    internal: true
    ipam:
      config:
        - subnet: 172.28.20.0/24

命令与参数详解: 定义了一个高安全性的三层 Compose v2 生产架构:包含 YAML 日志轮转锚点(x-logging-defaults)、针对 Netcup NVMe 存储优化的 Postgres 启动参数(random_page_cost=1.1)、基于文件的 Secrets 挂载、service_healthy 确定性启动依赖、cgroupv2 内存/CPU 硬限制以及 internal: true 气隙数据库子网。

安全与生产运维提示: 将 ports 显式绑定到回环地址 127.0.0.1:8080:80 可以确保 Docker 的 DOCKER iptables 规则链不会绕过 UFW 防火墙将 8080 端口直接暴露到公网 IPv4 地址上。

§ 05

第 5 步:校验配置插值并注册 systemd 生命周期管理服务单元

在启动或变更任何 compose.yaml 之前,务必养成先执行 docker compose config 的工程习惯。该命令会完整渲染变量插值后的最终 YAML 结构——自动展开 .env 中的所有变量、合并 YAML 锚点(*default-logging)并校验相对路径下的密钥文件是否存在。如果存在任何缩进错误或字段类型不匹配,docker compose config 会在不中断线上现有容器的前提下提前报错拦截。

虽然容器配置中的 restart: unless-stopped 能够指示 dockerd 在宿主机重启后自动拉起单个容器,但将整个多容器应用栈封装为一个标准的 systemd 参数化模板服务单元(/etc/systemd/system/compose-stack@.service),能够为系统管理员提供与 Linux 操作系统原生的生命周期集成:不仅可以通过 systemctl status 与 journalctl 统一审计启动日志,还能严格定义开机启动依赖顺序(After=docker.service network-online.target),并在服务器关机维护时触发优雅的 docker compose stop --timeout 30 停机钩子,确保 PostgreSQL 在限时内完成检查点刷盘。

校验配置文件语法、启动容器栈并实时检查健康状态与资源限制
bash
cd /opt/stacks/core-app

# 1. 校验 YAML 语法与 .env 变量插值结果
sudo docker compose config --quiet && echo "Compose syntax OK"

# 2. 并行拉取镜像并在后台启动应用栈,阻塞等待所有容器通过健康检查
sudo docker compose up -d --wait

# 3. 检查运行中的容器状态、健康探针输出及实时 cgroupv2 内存限制
sudo docker compose ps
sudo docker stats --no-stream

命令与参数详解: 静默校验 compose.yaml 语法,使用 --wait 标志在后台启动整个应用栈(命令行会阻塞等待,直到所有容器的 healthcheck 探针均返回 healthy 状态),随后通过 docker stats 验证内核已生效的内存上限。

预期终端输出
Compose syntax OK
NAME             IMAGE                       STATUS                   PORTS
core_app_db      postgres:16.4-alpine3.20    Up 28 seconds (healthy)  5432/tcp
core_app_redis   redis:7.4-alpine            Up 28 seconds (healthy)  6379/tcp
core_app_web     nginx:1.27-alpine           Up 16 seconds (healthy)  127.0.0.1:8080->80/tcp

CONTAINER ID   NAME             CPU %     MEM USAGE / LIMIT   MEM %     NET I/O
a1b2c3d4e5f6   core_app_db      0.18%     64.2MiB / 3GiB      2.09%     4.1kB / 3.2kB
b2c3d4e5f6a1   core_app_redis   0.22%     9.8MiB / 768MiB     1.28%     2.8kB / 1.9kB
c3d4e5f6a1b2   core_app_web     0.05%     14.1MiB / 2GiB      0.69%     1.5kB / 1.1kB
/etc/systemd/system/compose-stack@.service (可复用的 Systemd 参数化模板单元)
ini
[Unit]
Description=Docker Compose v2 Stack (%i)
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/stacks/%i
ExecStartPre=/usr/bin/docker compose config --quiet
ExecStart=/usr/bin/docker compose up -d --remove-orphans --wait
ExecReload=/usr/bin/docker compose up -d --remove-orphans --wait
ExecStop=/usr/bin/docker compose stop --timeout 30
TimeoutStartSec=300
TimeoutStopSec=60

[Install]
WantedBy=multi-user.target

命令与参数详解: 创建一个可复用的参数化 systemd 模板服务单元(compose-stack@<目录名>.service)。只需执行 sudo systemctl enable --now compose-stack@core-app.service,即可通过标准的 systemctl 命令统一托管 /opt/stacks/<目录名> 下的任意容器栈。

常见故障排查方案

排障手册

[故障现象] 使用 8080:80 发布的容器端口绕过了 UFW 防火墙,直接在 Netcup 公网 IPv4 上暴露

根本原因: 默认情况下,Docker 的 netfilter 集成会将端口转发规则直接插入到 nat 表与 filter 表的 DOCKER 链中,而 Linux 内核对这些规则链的评估优先级高于 UFW 的 ufw-before-input 规则链。

解决方案: 当容器服务位于本地反向代理后端时,务必在 compose.yaml 中为宿主机端口映射加上回环地址前缀(127.0.0.1:8080:80)。仅允许边缘反向代理容器在 0.0.0.0 上绑定 80 与 443 端口。

[故障现象] 执行 docker compose up --wait 时报错 dependency failed to start: container core_app_db is unhealthy

根本原因: PostgreSQL 容器初始化失败——最常见的原因是 /run/secrets/postgres_password 文件末尾包含了多余的换行符、文件权限导致容器内 UID 70 无法读取,或者现有的 pgdata 命名卷是由旧版本 Postgres 初始化的。

解决方案: 运行 sudo docker compose logs db --tail=50 以及 sudo docker inspect --format "{{json .State.Health}}" core_app_db,查看健康检查探针的具体退出码与标准错误输出。

[故障现象] 启动新的 Compose 应用栈时报错 Pool overlaps with other one on this address space

根本原因: compose.yaml 中定义的 ipam.config.subnet CIDR 网段与宿主机上已有的 Docker 网桥或默认地址池(172.17.0.0/16–172.31.0.0/16)发生了地址重叠。

解决方案: 执行 docker network inspect $(docker network ls -q) | grep -E "Name|Subnet" 检查当前已分配的所有子网,并为新应用栈指定一个不重叠的 /24 私有网段。

[故障现象] 高负载下容器突然退出,且状态码显示为 Exit Code 137

根本原因: 退出码 137(即 128 + 9 SIGKILL)表明该容器的常驻内存(RSS)突破了 deploy.resources.limits.memory 设定的硬上限,被 Linux 内核 cgroupv2 内存控制器强制终止。

解决方案: 执行 sudo dmesg -T | grep -i "oom-kill" 与 docker inspect core_app_web --format "{{.State.OOMKilled}}" 确认 OOM 事件。请调优应用自身的堆内存上限(例如设置 Node.js 的 NODE_OPTIONS 或 JVM 的 -Xmx 使其保持在容器上限的 75% 以内),或在 compose.yaml 中适当上调 deploy.resources.limits.memory。

[故障现象] 执行 docker compose build 时容器内无法解析外部域名或无法连接 APT 镜像源

根本原因: 使用 systemd-resolved 的 Netcup KVM 实例默认将本地 DNS 存根监听器绑定在 127.0.0.53,导致处于自定义网桥或特殊 MTU 环境下的构建容器在进行 UDP DNS 查询时失败。

解决方案: 在 /etc/docker/daemon.json 中显式配置递归 DNS 服务器("dns": ["46.38.225.230", "1.1.1.1", "9.9.9.9"]),然后运行 sudo systemctl restart docker 重启引擎。

常见问题解答 (FAQ)

权威解答

在 Docker Compose v2 中,不开启 Docker Swarm 模式也能使 deploy.resources.limits 生效吗?

完全可以。早期的 docker-compose v1 在非 Swarm 模式下会直接忽略 deploy: 配置块(除非额外传入 --compatibility 参数);而现代 Docker Compose v2 在单机 Linux 宿主机上会原生读取 deploy.resources.limits 与 reservations(包括 cpus 与 memory),并直接通过内核 cgroupv2 强制执行。

Docker 命名卷(Named Volumes)在 Netcup VPS 或 Root Server 的物理磁盘上存放在哪里?

在顶层 volumes: 关键字下声明的命名卷物理存储在 Netcup 根 NVMe/SSD 文件系统的 /var/lib/docker/volumes/<project>_<volume_name>/_data 目录下。与绑定挂载(Bind Mounts)不同,当新容器首次启动并挂载空命名卷时,Docker 会自动将目标目录的文件属主与权限初始化为容器镜像内部的对应用户。

如何在不停止数据库容器的前提下,单独平滑更新多容器栈中的某一个应用镜像?

只需修改 /opt/stacks/core-app/.env 中的镜像版本号标签,然后执行 sudo docker compose pull app && sudo docker compose up -d --no-deps app。--no-deps 参数会指示 Compose 仅重建 app 容器,而让底层的 db 与 redis 容器保持零中断持续运行。

为什么要在 PostgreSQL 容器配置中显式设置 shm_size: 256mb?

默认情况下,Docker 为每个容器挂载的 /dev/shm 共享内存分区硬编码上限仅为 64 MB。当 PostgreSQL 16 在多核 Netcup RS 1000 G12.5 或 RS 2000 G12.5 上执行复杂分析查询或并行哈希连接(Parallel Hash Joins)时,一旦共享内存消耗超过 64 MB,就会立刻抛出 ERROR: could not resize shared memory segment: No space left on device 错误。显式设置 shm_size: 256mb(或 512mb)即可解决这一限制。

参考资料 // 官方技术文档与核验来源

延伸阅读 // 核心架构评测与买前必读

领取优惠码 →