☰
RHEL 8上使用Docker Compose实现多容器环境一致编排实践
2026/9/26 20:19:56 网站建设 项目流程

从第一次在 RHEL 8 上把 Docker Compose 跑通说起

如果你维护过几套环境,大概率经历过这样的场景:开发环境里跑得好好的应用,一上测试环境就起不来,数据库连不上、依赖版本对不上、配置文件里写死了某个 IP。问题查到最后,往往就是那几句经典台词——“我这明明是好的”“你是不是环境没配对”。这套锅来回甩的次数多了,你就会意识到,容器化不是解决部署速度的问题,而是解决环境可信度的问题。而 Docker Compose,恰恰是把多容器应用的组织方式从“靠记忆”变成“靠代码”的第一步。

这篇文章聊的就是在 RHEL 8 上,怎么用 Docker Compose 把一套复杂的多容器应用编排起来,并且在开发、测试、生产三套环境之间尽量做到“同一份编排,差异最小化”。我不讲那种只有一个容器的 Hello World,我们直接上一套有真实业务感的组合:应用服务 + 关系型数据库 + 缓存 + 反向代理,每一层都有状态、有依赖、有配置差异。搞懂这套,你后面的项目基本可以套模板了。

先说清楚适合谁读:你是后端开发者、运维工程师或 DevOps 新手,RHEL 8 是你的服务器系统,你大概知道 Docker 是什么但还没系统性地用 Compose 管过多容器应用,或者你已经在用 Compose 但被开发和生产环境不一致折腾过——这篇文章就是给你写的。

1. 环境一致性到底难在哪,怎么用 Compose 拆解

1.1 “在我机器上是好的”的本质原因

很多人把环境不一致归结为运维不认真,这个结论太粗暴了。实际上,环境不一致的根源有三个:

第一,依赖版本漂移。开发机上是 Python 3.9 的某个小版本,测试机上是 3.8 的另一个小版本,看起来兼容,但某个库的二进制编译产物就是不一样,跑起来报一个莫名其妙的错。这种问题如果不锁版本,排查起来极其消耗时间。

第二,配置散落且无版本。你可能有三个不同的配置文件:开发用 config.dev.yml,测试用 config.test.yml,生产用 config.prod.yml。它们之间存在差异是正常的,但这些差异是否被追踪?是否有评审?是否有人不小心把生产数据库地址改错了提交上去?如果配置是散落的文件而不是受控的模板,早晚出事。

第三,进程管理方式不同。开发机上你可能是前台跑服务的,测试机上可能是 systemd 托管的,生产又是另外一套方式。启动顺序、日志处理、重启策略,这些一旦不同,应用行为必然出现难以解释的差异。

Docker Compose 怎么解决?它把应用定义成“一组服务”,每个服务是一个镜像加一组配置。镜像本身是依赖的“定格快照”,Compose 文件是启动方式的“唯一说明书”。整个系统从“跑起来”变成了“被定义出来”。你只要把同一条docker compose up -d的命令在任意一台机器上执行,启动的就是同一套逻辑的东西。

1.2 Compose 管的是“一组服务”,不是单个容器

单容器场景下,你docker run就够了,甚至写个脚本循环启动都行。但多容器应用复杂度完全不一样:服务之间有依赖关系,有网络隔离需求,有配置传递,有数据持久化。这些问题如果不用编排工具,你就得自己写启动脚本、维护网桥、手动等待依赖就绪——这套活干一次能忍,干十次你一定会骂人。

Compose 的价值在于把“服务拓扑”声明化。你告诉它有哪些服务、每个服务用什么镜像、什么端口、什么依赖、什么环境变量,它负责实际执行时创建独立网络、按依赖顺序启动、处理健康检查。如果服务 A 依赖服务 B 的数据库就绪,Compose 通过depends_on加condition让 A 等待 B 的健康检查通过后再启动。这个机制在复杂应用里非常关键,比脚本里sleep 10可靠得多。

还有一点容易被忽略:Compose 项目默认创建一个独立的网络。这意味着你用 Compose 里的服务名就能互相访问,比如应用服务里配置数据库地址写db:5432而不是localhost:5432或某个固定的 IP。这个设计的副作用就是,你在本机和在服务器上跑同一套编排,内部连接字符串完全不用改。

1.3 RHEL 8 上为什么值得用 Compose,而不是绕过它

RHEL 8 的一个微妙之处在于,系统自带的容器工具是 Podman,而且 Red Hat 官方推荐的也是 Podman 和 systemd 集成。这就让很多人在 RHEL 8 上纠结:到底要不要装 Docker?

我的建议是分情况。如果你所在团队已经全面拥抱 Podman 的 Quadlet 或 systemd 方式,那可以继续;但如果你的目标是“快速让一套多容器应用跑起来,且团队里已经有用 Compose 的习惯”,那 Docker Compose 依然是效率最高的选择。RHEL 8 本身对 Docker 并不友好,但可以通过 Docker 官方仓库安装docker-ce,而且跑起来很稳定。这里的关键在于你清楚自己在做什么:RHEL 8 是生产级系统,Docker 和它能兼容,但你要把 SELinux、firewalld、仓库源这些前置问题一次性处理干净,否则后面每次重启都会出幺蛾子。

另外,Podman 其实也支持docker-compose风格的编排文件,通过podman-compose或者podman play kube也能做到类似效果。不过实操体验上,docker compose的生态更成熟,尤其是在健康检查、服务依赖、日志驱动这些细节上,文档多、坑有前车之鉴,遇到问题容易查。

2. RHEL 8 环境准备与 Docker Compose 安装

2.1 基础环境检查

在装任何东西之前,先把系统状态确认一遍。RHEL 8 默认安装完成后,有几项配置会直接影响 Docker 的使用,提前处理好能省掉后面一大半的坑。

第一是SELinux。RHEL 8 默认 enforcing,这在多数情况下是好事,但 Docker 挂载卷时如果没处理好上下文,容器内经常会报 permission denied。我不是建议你直接关掉 SELinux——生产环境这么做不明智。更合理的做法是给挂载目录打上正确的标签,或者用:z后缀让 Docker 自动调整卷的 SELinux 上下文。实操中我用的是后者,简单高效。

第二是firewalld。RHEL 8 默认开着 firewalld,你需要把容器映射到宿主机的端口放行。注意,Docker 默认会操作 iptables 规则,但 firewalld 是上层管理工具,两者同时存在时规则可能互相干扰。稳妥的方案是:固定映射端口后,用firewall-cmd --add-port显式放行,而不是图省事关掉防火墙。

第三是yum 仓库配置。RHEL 8 没有订阅的情况下,默认仓库只有 baseos 和 appstream,Docker 官方仓库需要单独配置。这里有个实操经验:Docker 官方给的仓库地址用的是centos/rhel的路径,在 RHEL 8 上直接配docker-ce.repo时,要把$releasever修改成8,否则 yum 会解析失败。

我的检查清单如下,这几条命令跑完再继续,后面会顺畅很多:

# 确认系统版本 cat /etc/redhat-release # 确认 SELinux 状态 getenforce # 确认防火墙状态 systemctl status firewalld # 确认内核版本,容器相关模块是否加载 uname -r

2.2 安装 Docker Engine

RHEL 8 上装 Docker 的步骤不多,但细节要到位。我先说结论:用 Docker 官方仓库安装docker-ce,不要用 RHEL 自带源里的老版本。官方仓库的版本持续更新,而且你后续用 Compose v2 时,对 Docker 引擎版本有要求,老版本容易踩功能缺失的坑。

# 安装 yum-utils,用来管理仓库 sudo dnf install -y yum-utils # 添加 Docker 官方仓库 sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo # 重点:因为 RHEL 8 在官方仓库里没有匹配的 $releasever,需要手动改成 8 sudo sed -i 's/\$releasever/8/g' /etc/yum.repos.d/docker-ce.repo # 安装 Docker sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

注意最后一行。docker-compose-plugin这个包提供的docker compose子命令就是 Compose v2,也就是现在官方推荐的版本。别再去单独装 Python 版的docker-compose了,那个项目已经停止维护,解析depends_on的条件语义也不如 v2 完整。

安装完成后,启动服务并设置开机自启:

sudo systemctl enable --now docker sudo systemctl status docker

这里还建议顺手验证一下 Docker 是否正常工作:

sudo docker run --rm hello-world

如果这一步报“Cannot connect to the Docker daemon”,检查一下docker.socket是否启动,或者当前用户是否在docker组里。开发机上为了让日常操作不用敲 sudo,可以执行:

sudo usermod -aG docker $USER

注意:加组之后要重新登录终端才生效。生产环境建议别这么干,省心和安全之间要有取舍。

2.3 确认 Compose 版本

装完 Docker 之后,第一件事确认 Compose 插件能用:

docker compose version

正常会输出类似Docker Compose version v2.24.x的版本号。如果你看到的是docker-compose命令,说明装的是旧的独立版本,建议升级到 v2。后面文章里所有命令都基于docker compose(带空格)这个语法。

关于版本选择多说一句:Compose 文件格式(version字段)现在已经不重要了,官方已经不建议在 Compose 文件里写version: '3.8'这种字段,新版引擎会直接读取结构内容。所以文件顶部不用纠结写哪个版本号,你只要保证本地的 Compose 插件版本不要太老即可。

3. 设计一套真实的复杂多容器应用

3.1 业务场景选型:不只是“能跑”,还得“能看”

为了让整篇文章不流于空谈,我定义一套具体的业务场景。假设你需要在 RHEL 8 服务器上部署一个知识管理系统,它包含四个服务:

  • app:主应用,一个 Java/Spring Boot 或 Python/Django 之类的 Web 服务,负责业务逻辑。
  • db:PostgreSQL,保存业务数据。
  • redis:缓存,保存会话和热点数据。
  • web:Nginx,反向代理,接收外部请求,转发给 app,并处理静态资源。

这套架构覆盖面足够广,有状态服务(数据库)、有状态较轻的缓存、有无状态应用、有流量入口。你可以把这套组合迁移到任何一种多容器业务上,比如热词里提到的 OpenKM 文档管理系统,它也是类似的服务组合(应用 + 数据库 + 可选的对象存储)。

选这个组合还有一个原因:这四个角色在 Compose 里的写法各有各的讲究。数据库关心数据卷和初始化脚本,Redis 关心持久化模式和参数,应用服务关心构建方式和环境变量,Nginx 关心端口映射和配置文件挂载。全跑通一套,你基本什么组合都能写了。

3.2 目录结构:从第一行就为多环境铺路

很多人写 Compose 项目,目录结构是随手建的,这给后面埋了很多雷。我的做法是提前定好“项目根目录 - 服务目录 - 配置目录”三级结构,所有文件放进去后一眼能看清谁是谁。

一套推荐的目录结构长这样:

myapp/ ├── docker-compose.yml ├── .env # 公共环境变量 ├── env/ │ ├── app.dev.env │ ├── app.prod.env │ └── db.prod.env ├── app/ │ ├── Dockerfile │ └── src/ ├── nginx/ │ ├── nginx.conf │ └── conf.d/ │ └── default.conf └── data/ ├── db/ # 数据库数据目录,gitignore └── logs/

这个结构解决了几件事:环境差异通过env/目录下的文件按需加载,根目录保持干净;每个服务有自己的配置目录,替换起来不互相干扰;数据目录独立出来,备份和回滚都方便。记住一句话:Compose 文件是骨架,env 文件是血肉,目录结构是安放它们的腔体。

3.3 docker-compose.yml 核心配置拆解

直接上主文件。我标注了关键配置项,后面逐个解释为什么这么写。

name: myapp services: db: image: postgres:16-alpine restart: unless-stopped env_file: - ./env/db.common.env - ./env/db.${ENV}.env volumes: - ./data/db:/var/lib/postgresql/data - ./db/init:/docker-entrypoint-initdb.d:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-postgres}"] interval: 10s timeout: 5s retries: 5 networks: - backend redis: image: redis:7-alpine restart: unless-stopped command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}"] volumes: - ./data/redis:/data healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] interval: 10s timeout: 5s retries: 5 networks: - backend app: build: context: ./app args: - APP_VERSION=${APP_VERSION:-latest} image: myapp/app:${APP_VERSION:-latest} restart: unless-stopped env_file: - ./env/app.${ENV}.env environment: - DB_HOST=db - REDIS_HOST=redis depends_on: db: condition: service_healthy redis: condition: service_healthy expose: - "8000" networks: - backend web: image: nginx:1.25-alpine restart: unless-stopped depends_on: app: condition: service_started ports: - "${HTTP_PORT:-8080}:80" volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx/conf.d:/etc/nginx/conf.d:ro networks: - backend - frontend networks: backend: frontend: volumes:

接下来讲几个关键设计点。

第一,网络分前后端。backend网络承载 db、redis、app 三个服务,frontend只接 web。这样你的应用服务不会直接暴露到外部,web 是唯一入口。多一层网络隔离,就少一分被攻击的风险。这在生产环境里是非常常规的做法,有些人图省事把所有服务丢一个网络,还把每个服务都ports映射出来,这是给自己找麻烦。

第二,健康检查加依赖等待。注意两个细节:db 的健康检查用pg_isready,redis 用redis-cli ping,都是服务自带的轻量探测手段,不额外装工具。app 里depends_on的写法很关键——它要求 db 和 redis 的健康状态必须是 healthy 才开始启动 app。这套机制彻底取代了脚本里那种sleep 30的笨办法,依赖就绪了就启动,等多久都没关系。

第三,环境变量分“公共”和“环境专属”。db服务下我用了两个 env_file:第一个是公共的,第二个是带${ENV}变量的。这样 PostgreSQL 的用户名、数据库名在公共文件里只有一份,密码这类按环境区分的放专属文件。这个设计的好处是:如果想新建一套 staging 环境,你不需要改 compose 主文件,只需要新增两个 env 文件,然后ENV=staging docker compose up -d就搞定了。

第四,镜像管理策略。注意 app 服务我同时写了build和image两个字段。这有两层意思:在开发环境你直接docker compose up --build构建并运行;但一旦构建完成,生成的名称为myapp/app:latest的镜像可以docker push到私有仓库,生产环境直接docker compose pull拉对应的镜像。这是提高开发与生产一致性的核心思路之一——开发和生产用同一个镜像,而不是各自构建各自的。

第五,数据卷。我把数据库和 Redis 的数据都挂到宿主机目录,而不是用命名卷。这个选择有两面性:优点是好备份好管理,你直接打包./data目录就能迁移;缺点是对宿主机文件系统性能要求更高。不过在单机部署场景下,这个取舍是值得的。生产环境更稳妥的做法是给./data/db单独挂一块数据盘,避免系统和数据互相干扰。

4. 处理开发与生产的核心差异:环境变量与覆盖文件

4.1 一套 Compose 文件怎么兼顾不同环境

有人看到这里会问:你前面写的docker-compose.yml里用了很多${ENV}之类的变量,那开发环境和生产环境最终跑起来,配置到底差在哪?我列几个真实的差异点你就懂了:

配置项开发环境生产环境
数据库密码弱密码或默认密码强密码,定期轮换
日志级别DEBUGINFO 或 WARNING
容器重启策略unless-stoppedunless-stopped
端口映射8080:8080:80
镜像来源本地构建私有仓库拉取
数据目录本地测试数据独立数据盘

能看到,真正差异大的其实是“环境参数”,而不是“架构定义”。所以我的核心原则是:docker-compose.yml 保持一份且只写公共逻辑,差异全部通过 env 文件和 override 文件注入。这条路走下去,你就能避免“开发一个 compose 文件、生产另一个”的割裂局面——那种做法会导致生产环境变成开发环境不可验证的黑盒。

4.2 env_file 与 .env 的分工

很多人分不清env_file和.env的区别,这里我用一句话说透:env_file是给容器内部注入环境变量用的,.env是给 Compose 自己解析变量用的。

举例:你在db服务里写了env_file: ./env/db.prod.env,那么这个文件里的POSTGRES_PASSWORD=xxx会被注入到容器里;而你在 Compose 文件里写的${HTTP_PORT:-8080},解析时读取的是项目根目录下的.env文件或 shell 里已有的同名变量。

这个分工意味着两层配置互不干扰,但也要求你写 Compose 文件时想清楚:哪些变量是容器运行时要的,哪些是 Compose 自身布局时要的。我见过有人把数据库密码写在.env里,结果 Compose 文件里根本用不到,而容器里也没注入进去,最后只能大眼瞪小眼。

4.3 override 文件的正确打开方式

Compose 有一个特性我放在这里单独讲,就是多文件合并。

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

这个用法的核心场景是:当开发和生产在“架构层面”都有显著差异时,比如开发环境要挂载源码目录做热更新,而生产环境直接跑镜像;或者开发环境要多起一个 mock 服务,而生产有额外的监控容器,这时候单独写一个 override 文件比在同一份文件里写一堆条件判断要清晰得多。

一个典型的开发环境 override 文件长这样:

services: app: volumes: - ./app/src:/app/src # 源码热挂载 environment: - DEBUG=true - AUTO_RELOAD=true command: python manage.py runserver 0.0.0.0:8000

生产 override 文件长这样:

services: app: image: registry.example.com/myapp/app:${APP_VERSION} restart: always logging: driver: json-file options: max-size: "100m" max-file: "3"

这种做法的好处是,你从开发切到生产,执行一条加-f的 Compose 命令就够了,而不用维护两份完全不同的配置。还有个小技巧:Compose 默认会自动加载docker-compose.override.yml,如果这个文件存在,docker compose up时它会被自动合并。我在项目里会把开发环境的默认覆盖写在docker-compose.override.yml,生产部署脚本里显式-f指定生产覆盖文件,这样既方便开发默认体验,又能保证生产环境不会读到开发配置。

5. 实操:从零到一跑通整套部署

5.1 初始化文件与环境变量

假设你打开了 RHEL 8 服务器,已经装好了 Docker 和 Compose。现在开始实操。首先创建项目目录和所需的子目录,然后逐个写入配置文件。

mkdir -p /opt/myapp/{app,nginx/conf.d,env,db/init,data/{db,redis,logs}} cd /opt/myapp

如果这台机器上的主应用代码本身就在仓库里,直接把代码 clone 到app/src下。如果你只是先验证编排,可以先放一个简单的应用基座在后面构建。

接下来写项目根目录的.env文件,这个文件控制 Compose 自己解析时的公共变量:

cat > .env <<'EOF' ENV=dev HTTP_PORT=8080 APP_VERSION=1.0.0 POSTGRES_USER=myapp POSTGRES_DB=myapp POSTGRES_PASSWORD=devpass REDIS_PASSWORD=devredispass EOF

注意:我这里把ENV=dev写进.env,后面所有引用${ENV}的地方都会变成dev。部署到生产时,只要改.env里的ENV=prod,或者通过 shell 导出ENV=prod再执行 Compose,就能切换到生产参数。

5.2 逐个服务验证启动

不要一上来就docker compose up -d一把梭。我的习惯是先拉起依赖服务,再拉起应用,再进入验证环节。虽然 Compose 本身有依赖控制,但手动分步跑能更快定位问题。

先把数据库拉起来:

docker compose up -d db docker compose ps

这时你会看到 db 容器启动,进入 healthy 可能需要几秒到几十秒。等它变 healthy 后,验证一下数据库连接:

docker compose exec db psql -U myapp -d myapp -c "SELECT 1;"

能返回结果就说明初始化脚本和认证配置没问题。这里有个我踩过的坑:PostgreSQL 的初始化脚本只在数据目录为空时执行。如果你改了初始化 SQL 但数据卷里已有旧数据,脚本不会重新跑,你会以为配置生效了其实没有。最直接的解决办法是删除数据目录再起,或者手动进容器执行 SQL。

Redis 同理:

docker compose up -d redis docker compose exec redis redis-cli -a devredispass ping

返回PONG就正常。

依赖服务就绪后再启动主应用:

docker compose up -d app

如果 app 是本地构建的,这里会自动执行build,可能要等几分钟。构建过程遇到依赖下载慢或编译失败,都会在此刻暴露。别慌,看日志:

docker compose logs -f app

最后把 Nginx 拉起来,验证整个链路:

docker compose up -d web curl http://localhost:${HTTP_PORT:-8080}/healthz

如果返回 200,整套系统就跑通了。这个/healthz接口我在应用代码里实现,前端 Nginx 将其反向代理到 app 的/healthz。这一条可以从外到内验证:Nginx 是否正常转发、app 是否正常响应、心跳链路是否通畅。

5.3 开发环境的热更新配置

开发环境的痛点在于:容器里跑应用,改代码要重建镜像,效率太低。解决办法是挂载源码目录进容器,配合开发服务器自动重载的功能。

在docker-compose.override.yml里写:

services: app: volumes: - ./app/src:/app/src environment: - FLASK_ENV=development - FLASK_DEBUG=1 command: flask run --host 0.0.0.0 --port 8000

这样之后,你改本机代码,容器内立刻生效,不用反复构建镜像。这是开发环境体验提升最大的一个动作,花两分钟配置,能省掉后面无数的重建等待。

有两点副作用想提醒你:第一,挂载源码之后,容器里的代码和宿主机文件系统产生强耦合,这时候 Compose 的depends_on和健康检查依然有效,但容器的“可移植性”下降了——但没关系,这是开发环境的预期行为;第二,开发容器里跑的依赖包如果和生产镜像不一致,容易出现开发环境和生产行为不一致,所以建议requirements.txt或package.json这些依赖锁定文件要严格同源。

5.4 生产环境的启动步骤

生产流程与开发最大的差别是:镜像不本地构建,而是从私有仓库拉取。在 CI 阶段构建好镜像并推送,部署时执行:

export ENV=prod export HTTP_PORT=80 docker compose -f docker-compose.yml -f docker-compose.prod.yml pull docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

整个部署过程,你不接触源码构建,只负责“拉镜像 + 起服务”,好处是交付链路变短、可重复性强。如果后面的变更需要回滚,只要把APP_VERSION指向上一个版本号,重新执行 pull 和 up 即可。

5.5 查看运行状态与日志

日常运维最常用的三个命令:

# 查看所有服务状态 docker compose ps # 查看某个服务的实时日志 docker compose logs -f web # 进入容器排查问题 docker compose exec app /bin/sh

在 RHEL 8 上有一点要注意:因为 SELinux 可能限制容器内对挂载目录的读写,遇到权限问题时,优先用:z或:Z后缀调整挂载卷的 SELinux 标签。我一般对只读挂载用:ro,对需要读写的挂载加:z,这样能保持 SELinux 开启,又不用频繁告警。

6. 常见问题与排查技巧实录

6.1 端口冲突

在 RHEL 8 上执行docker compose up时,如果宿主机某端口已被占用,Docker 会直接报错退出,提示Bind for 0.0.0.0:8080 failed: port is already allocated。

排查思路分三步:先看是不是自己之前起的容器占着:

docker ps -a | grep 8080

如果没有,再看系统上其他进程:

ss -lntp | grep 8080

如果确认是残留容器,把旧的停掉删除,或者调整.env里的HTTP_PORT。这里有个经验:尽量不要让应用直接监听 80 端口,除非你已经规划好防火墙和 SELinux 的放行规则。默认用 8080 起步,等一切稳定了再用 Nginx 或防火墙做端口转发。

6.2 容器内无法访问宿主机服务

有些应用在部署时要访问宿主机上的某个服务,比如连一个宿主机上直装的 MySQL。这里有个常见的网络误区:在容器内localhost指向的是容器自己,不是宿主机。

你必须显式指定宿主机 IP。在 RHEL 8 上,可以通过host.docker.internal这个 Docker 20.10 之后提供的特殊域名,或者在 Compose 文件的extra_hosts里手动映射宿主机 IP:

services: app: extra_hosts: - "host.docker.internal:host-gateway"

这样容器内就能用host.docker.internal访问宿主机服务了。但不建议长期依赖这个特性,因为宿主机的 IP 和容器网络存在耦合,生产环境里还是应该把基础设施服务也容器化,统一纳入 Compose 管理。

6.3 数据库初始化脚本不生效

前面提过一次,这里展开讲。Postgres 镜像会在数据目录为空时执行/docker-entrypoint-initdb.d/下的脚本,如果数据目录已有旧数据,新增的初始化脚本就不会执行。

解决思路有两种:一种是在本地开发时,如果改了初始化脚本,直接清掉数据目录再重启:

docker compose down sudo rm -rf data/db docker compose up -d db

另一种是把初始化更改为手动执行:

docker compose exec -T db psql -U myapp -d myapp < db/init/001_create_table.sql

生产环境里迁移数据库结构,不要依赖镜像的初始化目录,应该用 Flyway、Liquibase 这类专门的迁移工具。Compose 里的初始化脚本只适合第一次部署时建库建表。

6.4 容器内 Nginx 配置没生效

Nginx 的配置是挂载进去的,我加的是:ro后缀。如果你改了宿主机上的nginx/conf.d/default.conf,需要执行 reload 让 Nginx 生效:

docker compose exec web nginx -s reload

这里有个很隐蔽的问题:挂载单个文件而不是目录时,如果宿主机文件不存在,Docker 会自动创建一个目录,导致容器内挂载点变成一个空目录,Nginx 报配置丢失。解决办法是确保宿主机上的文件路径存在且是普通文件,先写入再挂载起容器。

6.5 RHEL 8 的 SELinux 权限问题

这是 RHEL 系列上独有的坑,必须单独列出来。你在挂载数据卷后,容器内进程写不了数据目录,报错类似:

PermissionError: [Errno 13] Permission denied: '/var/lib/postgresql/data'

第一反应别是chmod 777,那是给自己埋雷。正确做法是给挂载目录打上 container 标签:

sudo chcon -Rt svirt_sandbox_file_t /opt/myapp/data

或者更简单,直接在挂载卷后缀加:z,让 Docker 自动调整上下文。我在前面已经强调过这个技巧,这里再补一句:生产环境如果遇到这种问题,优先理解标签的含义再操作,不要一关了之。

6.6 排查技巧速查表

症状优先排查项常用命令
容器频繁重启应用启动失败,查看退出码和日志docker compose ps,docker compose logs app
服务间无法连通是否在同一个 Compose 网络docker compose exec app getent hosts db
数据丢失卷挂载是否正常指定docker inspect <容器> -f '{{json .Mounts}}'
容器内时间不对是否挂载了宿主时区date对比,-v /etc/localtime:/etc/localtime:ro

7. 从“能跑”到“用得稳”,再说几句实在话

用过一段 Compose 后你会明显感觉到,它最大的价值不是省去了docker run的参数之苦,而是逼迫你把环境定义这件事变透明。透明的东西才有讨论余地、才有版本历史、才能被审查。

我个人在 RHEL 8 上实际跑这套方案时,很多次被 SELinux 和防火墙的细节卡住,但反过来看,这些坑也逼着我去理解系统层面的安全机制,而不是一味地“关掉就好”。容器化不是目的,可重复、可预期、可解释才是目的。

最后分享一个小习惯:每次改完 Compose 文件或 env 文件,执行docker compose config看最终合并后的配置。这个命令会把你所有变量解析、override 合并后的结果完整打印出来。你对它扫一眼,比上了生产才发现配置错了要安心得多。这套“先 config 再 up”的流程,算是我踩了无数坑之后养成的肌肉记忆了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询