Docker Compose 部署 pgAdmin 与 PostgreSQL 最佳实践

📅 2026/7/7 21:15:22 👁️ 阅读次数 📝 编程学习
Docker Compose 部署 pgAdmin 与 PostgreSQL 最佳实践

1. 为什么我坚持用 Docker 跑 pgAdmin,而不是装个桌面版?

你有没有过这种体验:在终端里敲psql -U admin -d mydb,输完密码手心冒汗,生怕一个DROP DATABASE mydb;按错回车?或者写完一条UPDATE users SET status = 'active' WHERE id = 123;,得反复核对三遍 WHERE 条件才敢执行?更别提想看看这条 SQL 到底走了索引还是全表扫描——EXPLAIN输出那堆缩进嵌套的文本,读起来像解密二战电报。命令行当然强大,但它不是为人类直觉设计的。它不告诉你字段类型在哪儿改、主键怎么加、备份文件存在哪个路径、甚至连接失败时到底卡在哪一步。

pgAdmin 4 就是来解决这个“人机摩擦”的。它不是另一个花里胡哨的数据库客户端,而是 PostgreSQL 官方背书、深度集成的浏览器管理平台。你打开 Chrome 或 Edge,输入http://localhost:5050,登录进去,看到的不是黑底绿字的命令行,而是一个结构清晰的树形导航:服务器 → 数据库 → 模式 → 表 → 列 → 索引。点一下就能新建表,拖一拖就能重排字段顺序,右键一下就能生成备份脚本。它的 Query Tool 面板分三块:左边写 SQL,中间出结果,右边打日志,连F5执行和Ctrl+Enter运行都给你配好了快捷键。最绝的是那个可视化查询计划图——你点一下“Explain”按钮,原本晦涩的Seq Scan on orders文本,瞬间变成带箭头、带耗时百分比、带数据量提示的流程图,哪个节点慢、哪个索引没用上,一眼就看明白。

但问题来了:装个桌面版 pgAdmin,得下载、安装、适配系统版本、处理依赖冲突,换台电脑又得重来一遍。而用 Docker Compose 跑,整个环境就浓缩在一个docker-compose.yml文件里。我上周给新来的前端同事配开发环境,把这份文件发过去,他docker compose up -d之后,五分钟后就在浏览器里建好了第一张表。没有“你的 Mac 版本太老”、没有“Windows 上 Python 环境冲突”,只有干净、可复现、可共享的一份声明式配置。这不是偷懒,是把“环境一致性”这个隐形成本,从每次协作的摩擦力,变成了一个git clone就能解决的确定性动作。它解决的从来不是“能不能用”,而是“能不能让所有人,在任何时间、任何机器上,用完全一致的方式去用”。

2. 整体架构设计与核心思路拆解

2.1 为什么必须是双容器协同,而不是单容器打包?

很多人第一次接触这个需求,第一反应是:“能不能把 PostgreSQL 和 pgAdmin 打包进同一个镜像里?”答案是技术上可行,但工程上极不推荐。这背后是容器设计哲学的根本分歧:单一职责原则(Single Responsibility Principle)

PostgreSQL 是一个状态化、高 I/O、需要持久化存储的数据库服务。它关心磁盘性能、内存分配、WAL 日志刷盘策略。而 pgAdmin 是一个无状态、面向用户的 Web 应用层代理。它只负责接收 HTTP 请求、渲染 HTML、调用后端psqlpg_dump命令。两者生命周期完全不同:数据库要 7x24 小时常驻,pgAdmin 可能每天只开几个小时;数据库升级要谨慎灰度,pgAdmin 升级可以随时重建容器。如果硬塞进一个容器,你就会陷入“牵一发而动全身”的泥潭——想升级 pgAdmin 的 UI,得连带着重启整个数据库;想给 PostgreSQL 加个shared_buffers参数,得重新构建整个镜像;更别说日志分离、资源限制、健康检查这些基础能力,单容器里全得自己手动缝合。

Docker Compose 的services机制,正是为这种松耦合协作而生。我们定义两个独立的服务:postgrespgadmin。它们通过networks共享一个私有网络pgnetwork,在这个网络里,pgadmin容器可以直接用postgres这个名字当主机名去连接数据库,就像访问局域网内另一台机器一样自然。Docker 内置的 DNS 服务会自动把服务名解析成对应容器的 IP 地址。这种设计,让每个组件都保持了最大的自治性。我可以在不碰postgres服务一行配置的情况下,把pgadmin的镜像从9.13升级到9.14,只需改一个标签,docker compose up -d之后,旧容器自动销毁,新容器无缝接管,数据库连接毫秒级恢复。这才是现代开发环境该有的弹性。

2.2 为什么网络模型选bridge而非hostnone

Docker 提供了多种网络驱动,host模式会让容器直接使用宿主机的网络命名空间,省去一层 NAT,性能略好。但代价是彻底丧失网络隔离。pgadmin默认监听5050端口,如果宿主机上已有进程占用了5050host模式下容器启动必然失败,且你无法通过ports字段做端口映射来规避——因为host模式下端口就是宿主机的端口。更严重的是安全风险:host模式下,容器内运行的任何服务,其端口都会直接暴露在宿主机上,相当于把防火墙撤掉了。

none模式则过于封闭,容器完全无网络,连ping都不通,pgadmin根本无法访问postgres,更别说对外提供 Web 服务了。

所以bridge是唯一合理的选择。它是 Docker 的默认网络驱动,为每个 Compose 项目创建一个独立的虚拟网桥。pgnetwork就是这样一个自定义 bridge 网络。它的优势在于三点:隔离性、可控性、可发现性。隔离性保证了pgnetwork内的通信不会干扰宿主机或其他项目的网络;可控性体现在你可以精确指定ports映射,比如把容器内的5050映射到宿主机的8080,避免端口冲突;可发现性则是 Docker DNS 的功劳,服务名即主机名,无需硬编码 IP,也无需维护/etc/hosts。我在实际项目中见过太多因host模式导致的端口冲突问题,最后都回归bridge,并用127.0.0.1:5050:5050这种显式绑定,既安全又稳定。

2.3 为什么数据持久化必须用 named volume,而不是 bind mount?

volumes在 Compose 文件里写了两处:postgres_datapgadmin_data。这里有个关键细节:它们都是named volume(命名卷),而不是./data:/var/lib/postgresql这样的bind mount(绑定挂载)。

区别在哪里?Bind mount 是把宿主机上的一个具体路径,比如/Users/you/project/data,直接挂载进容器。它的好处是文件可见、可编辑,适合开发时放源码。坏处是路径强依赖宿主机环境。你把这个 Compose 文件发给 Windows 同事,/Users/you/project/data这个路径在 Windows 上根本不存在,docker compose up直接报错。更麻烦的是权限问题:PostgreSQL 容器内以postgres用户(UID 999)运行,而 macOS/Linux 上你的用户 UID 通常是 501 或 1000,两者不匹配,挂载后容器可能没有写权限,数据库初始化失败。

Named volume 则完全不同。它是 Docker 自己管理的存储卷,存放在 Docker 的数据目录下(如/var/lib/docker/volumes/),完全与宿主机路径解耦。你只需要起个名字,比如postgres_data,Docker 会自动在后台创建一个目录,并确保容器内进程有正确的读写权限。无论你在 macOS、Ubuntu 还是 WSL2 上运行,只要 Docker Engine 在,这个卷就存在、就可用。而且,named volume 支持docker volume prune这样的清理命令,可以一键删除所有未被使用的卷,而 bind mount 的清理则需要你手动rm -rf,极易误删。我曾经在一次 CI 流水线中误用了 bind mount,导致测试数据库的 WAL 日志文件被写入到 Git 仓库根目录,差点提交了几个 GB 的二进制垃圾。那次教训之后,所有需要持久化的服务,一律强制使用 named volume。

3. 核心细节解析与实操要点

3.1 环境变量的安全实践:.env文件不是可选项,是必选项

原文提到“硬编码凭证是坏主意”,这话说得轻了。在团队协作中,把PGADMIN_DEFAULT_PASSWORD: secret这样的行留在docker-compose.yml里,等同于把公司数据库的钥匙挂在 GitHub 的门把手上。Git 历史记录是永久的,哪怕你后来删了这行,git log -p依然能翻出来。我亲眼见过一个初创公司,因为一位工程师在公开仓库里提交了包含生产数据库密码的 Compose 文件,导致三天后所有用户数据被勒索软件加密。

.env文件是 Docker Compose 内置的安全机制。它的工作原理很简单:当你运行docker compose up时,Compose 引擎会自动读取当前目录下的.env文件,将其中的KEY=VALUE对加载为环境变量,然后在docker-compose.yml中通过${KEY}语法引用。这个过程完全在本地完成,.env文件本身不会被传入容器,也不会出现在任何 Docker API 调用中。它只是一个纯文本的“本地配置上下文”。

但光有.env还不够,必须配合.gitignore。这是第二道保险。.gitignore文件的作用,是告诉 Git “永远不要追踪这个文件”。你必须在项目根目录下创建.gitignore,并加入这一行:

.env

这样,即使你不小心执行了git add ..env也不会被加入暂存区。我有个习惯,在初始化任何新项目时,第一件事就是创建.env.gitignore,并用echo ".env" >> .gitignore确保万无一失。另外,.env文件的权限也值得留意。在 Linux/macOS 上,执行chmod 600 .env,将其权限设为仅所有者可读写,能防止其他用户通过ls -la看到文件内容。Windows 用户则需在属性里取消“继承权限”,只保留当前用户。

3.2depends_on的真实作用与常见误解

depends_on是 Compose 文件里最容易被误解的字段。很多新手以为它能保证“postgres容器里的 PostgreSQL 服务已经完全启动并接受连接”,然后才启动pgadmin。这是个危险的错觉。

depends_on的实际行为非常朴素:它只控制容器的启动顺序,即docker compose up时,先docker run启动postgres容器,等这个run命令返回成功(容器进程已 fork 出来),再启动pgadmin容器。它完全不检查postgres容器内部的 PostgreSQL 进程是否已初始化完毕、是否已监听5432端口、是否已准备好接受连接。PostgreSQL 的初始化可能需要几秒到十几秒,尤其是在首次启动、需要格式化数据目录时。pgadmin容器启动后,如果立刻尝试连接postgres:5432,大概率会收到Connection refused错误,然后崩溃退出。

所以,depends_on解决的只是“容器进程启动顺序”的问题,而非“服务就绪状态”的问题。要解决后者,你需要额外的健康检查机制。Docker Compose v2.3+ 支持healthcheck字段。你可以在postgres服务下添加:

healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 30s timeout: 10s retries: 5 start_period: 40s

这段配置的意思是:容器启动后 40 秒开始,每 30 秒执行一次pg_isready命令,检查数据库是否就绪。如果连续 5 次失败,则标记容器为 unhealthy。然后,你可以在pgadmin服务里用depends_on的扩展语法:

depends_on: postgres: condition: service_healthy

这样,pgadmin就真的会等到postgres服务健康后才启动。不过,对于本地开发环境,depends_on加上一点简单的重试逻辑(比如pgadmin启动脚本里加个sleep 5)通常就足够了。但在 CI/CD 或生产预演环境中,healthcheck是必备项。

3.3servers.json:让团队免去重复注册服务器的手动操作

想象一下,一个十人团队,每人每天都要docker compose up启动开发环境。如果每次启动后,每个人都得在 pgAdmin 里右键“Register Server”,填一遍Host: postgres,Port: 5432,Username: admin,Password: secret……这不仅是时间浪费,更是错误温床。某个人手抖输错了密码,连不上库,开始怀疑人生,最后发现是自己填错了。

servers.json就是这个问题的终极答案。它是一个 JSON 格式的配置文件,pgAdmin 在启动时会自动读取它,并将其中定义的服务器连接预加载到左侧的“Servers”树中。你不需要任何插件或额外工具,只需在 Compose 文件里加一行volumes挂载:

volumes: - ./servers.json:/pgadmin4/servers.json

注意路径/pgadmin4/servers.json,这是 pgAdmin 容器内约定的加载位置,不能写错。

servers.json的结构看似简单,但有几个坑必须避开。首先是服务器 ID 必须是字符串"1",而不是数字1。JSON 规范要求对象的键必须是字符串,"1"是合法的键,1则不是。其次,SSLMode字段强烈建议设置为"prefer"。PostgreSQL 默认启用 SSL,但本地开发环境通常不配证书,"prefer"模式表示“如果服务器支持 SSL 就用,不支持就降级为明文”,能最大程度兼容。最后,MaintenanceDB字段必须填POSTGRES_DB的值,即你创建的初始数据库名,不能填postgres(这是系统库,pgAdmin 不允许用它做维护库)。我见过太多人在这里填错,导致连接后看不到自己的数据库,只看到一个空荡荡的postgres库。

4. 实操过程与核心环节实现

4.1 从零开始:完整的docker-compose.yml.env文件实录

现在,让我们把所有理论付诸实践。以下是我日常开发中使用的、经过千锤百炼的docker-compose.yml文件。它不是照搬原文的简化版,而是融合了所有最佳实践的生产就绪模板:

version: '3.8' services: postgres: image: postgres:18-alpine container_name: postgres environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} volumes: - postgres_data:/var/lib/postgresql/data - ./init.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 30s timeout: 10s retries: 5 start_period: 40s restart: unless-stopped networks: - pgnetwork pgadmin: image: dpage/pgadmin4:9.13 container_name: pgadmin environment: PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL} PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD} PGADMIN_LISTEN_PORT: 5050 # 关键配置:禁用匿名登录,强制使用凭据 PGADMIN_CONFIG_LOGIN_BANNER: "Development Environment - Do Not Use For Production" ports: - "127.0.0.1:5050:5050" volumes: - pgadmin_data:/var/lib/pgadmin - ./servers.json:/pgadmin4/servers.json depends_on: postgres: condition: service_healthy restart: unless-stopped networks: - pgnetwork volumes: postgres_data: driver: local pgadmin_data: driver: local networks: pgnetwork: driver: bridge ipam: config: - subnet: 172.20.0.0/16

这个文件里,我做了几处关键增强:

  • 使用postgres:18-alpine镜像,体积比postgres:18小 70%,启动更快,更适合本地开发。
  • 添加了./init.sql挂载,用于在数据库首次初始化时自动执行建表语句,省去手动创建。
  • healthcheckdepends_on的组合,确保了真正的服务就绪依赖。
  • restart: unless-stopped让容器在意外崩溃后自动恢复,提升稳定性。
  • ipam配置为网络指定了固定子网172.20.0.0/16,避免与其他 Docker 网络冲突。

配套的.env文件内容如下:

# PostgreSQL Config POSTGRES_USER=admin POSTGRES_PASSWORD=dev_password_123 POSTGRES_DB=myapp_dev # pgAdmin Config PGADMIN_DEFAULT_EMAIL=admin@myapp.local PGADMIN_DEFAULT_PASSWORD=pgadmin_dev_456

提示:PGADMIN_DEFAULT_PASSWORD的强度要求很高。pgAdmin 4.7+ 版本强制要求密码至少 8 位,且必须包含大小写字母、数字和特殊字符。dev_password_123这样的密码会被拒绝。我习惯用openssl rand -base64 12 | tr -d "=+/"生成一个 12 位的随机字符串作为密码。

4.2init.sql:自动化数据库初始化的实战技巧

./init.sql文件是提升开发效率的神器。它会在 PostgreSQL 容器首次启动、数据目录为空时,自动执行其中的 SQL 语句。这比每次手动在 pgAdmin 里点点点创建表、插入测试数据,高效太多了。

一个典型的init.sql文件内容如下:

-- 创建一个专用的 schema,避免污染 public CREATE SCHEMA IF NOT EXISTS app; -- 创建一个示例 users 表 CREATE TABLE IF NOT EXISTS app.users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 插入几条测试数据 INSERT INTO app.users (email) VALUES ('dev@example.com'), ('test@example.com'), ('qa@example.com') ON CONFLICT (email) DO NOTHING; -- 创建一个索引,加速按邮箱查询 CREATE INDEX IF NOT EXISTS idx_users_email ON app.users(email);

这里有几个实用技巧:

  • 所有CREATE语句都加上IF NOT EXISTS,确保多次执行不会报错。因为init.sql只在数据目录为空时执行,但如果你docker volume rm postgres_data后又重建,它会再次执行,IF NOT EXISTS能避免重复创建的错误。
  • INSERT ... ON CONFLICT DO NOTHING是 PostgreSQL 的“UPSERT”语法,能防止插入重复邮箱时报错,让脚本更健壮。
  • 我习惯把业务表都放在app这个 schema 下,而不是默认的public。这样在 pgAdmin 的树形导航里,public下干干净净,全是系统对象,app下才是我的业务表,一目了然。CREATE SCHEMA IF NOT EXISTS app;这一行必须放在最前面,否则后面的CREATE TABLE app.users会失败。

4.3 连接验证与故障排查:从docker psdocker logs的完整链路

启动命令docker compose up -d执行后,第一步永远是验证容器状态:

docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"

你会看到类似这样的输出:

NAMES STATUS PORTS postgres Up 2 minutes 5432/tcp pgadmin Up 2 minutes 127.0.0.1:5050->5050/tcp

如果STATUS里显示Restarting (1)Exited (1), 说明容器启动失败,必须查日志。

查日志的黄金法则:先查失败的那个,再查依赖的那个。比如pgadmin显示Restarting,你应该先docker logs pgadmin。如果日志里出现psql: error: connection to server at "postgres" (172.20.0.2), port 5432 failed: Connection refused,这说明postgres服务还没起来,或者pgadmin启动太快。这时,你应该立刻docker logs postgres,看 PostgreSQL 是否初始化成功。一个健康的postgres日志末尾,应该有类似database system is ready to accept connections的字样。

如果postgres日志正常,但pgadmin还是连不上,那就检查网络连通性。进入pgadmin容器内部,用pingtelnet测试:

# 进入 pgadmin 容器 docker exec -it pgadmin sh # 测试能否解析 postgres 服务名 nslookup postgres # 测试能否连通 postgres 的 5432 端口 apk add --no-cache busybox-extras # Alpine Linux 需要单独安装 telnet telnet postgres 5432

如果nslookup失败,说明 Docker DNS 有问题,检查networks配置是否一致;如果telnet失败,说明postgres容器的5432端口没有正确监听,检查postgres服务的exposeports字段(虽然bridge网络下expose不是必须的,但有时能帮上忙)。

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

5.1 “登录页面打不开”:端口、绑定与防火墙的三重排查

http://localhost:5050打不开,是最常见的入门问题。别急着重装,按这个顺序排查:

  1. 确认容器确实在运行docker ps | grep pgadmin。如果没输出,说明容器没起来,回到上一节查日志。
  2. 确认端口映射是否正确docker port pgadmin。输出应该是5050 -> 127.0.0.1:5050。如果输出是5050 -> 0.0.0.0:5050,说明你没写127.0.0.1:前缀,或者 Compose 文件没生效。检查ports字段是否为"127.0.0.1:5050:5050"
  3. 确认宿主机端口未被占用:在 macOS/Linux 上,lsof -i :5050;在 Windows 上,netstat -ano | findstr :5050。如果发现其他进程占用了5050,要么杀掉它,要么在 Compose 文件里把ports改成"127.0.0.1:5051:5050"
  4. 确认浏览器没走代理:尤其是公司网络,很多企业防火墙会拦截localhost。试试用127.0.0.1:5050替代localhost:5050,或者换个浏览器(Chrome 的隐身模式)。

注意:127.0.0.1:5050:5050这个绑定,意味着pgadmin只能被本机访问。如果你想从同一局域网的其他电脑访问,比如用 iPad 查看,就必须改成0.0.0.0:5050:5050,但务必确保你的电脑防火墙已关闭,或者只在可信的内网环境下这么做。安全和便利,永远是一道选择题。

5.2 “连接服务器失败”:localhost陷阱与 Docker DNS 的真相

在 pgAdmin 的“Server Registration”对话框里,Host name/address字段填localhost,是新手最大的误区。我见过太多人在这里栽跟头,然后发帖问“为什么连不上”。

原因在于:localhost在容器里,指的是容器自己,不是你的宿主机,也不是postgres容器。pgadmin容器的localhost127.0.0.1,而postgres容器的localhost是它自己的127.0.0.1,两者完全隔离。它们之间唯一的通信桥梁,就是 Docker 的自定义网络pgnetwork,以及 Docker DNS 为每个服务名分配的 IP。

所以,Host name/address必须填postgres,也就是docker-compose.ymlservices下定义的那个服务名。Docker DNS 会把它解析成postgres容器在pgnetwork网络中的真实 IP,比如172.20.0.2。你可以用docker network inspect pgnetwork命令查看这个 IP。

如果填了postgres还是连不上,检查Port字段。PostgreSQL 容器默认监听5432,但如果你在postgres服务里加了ports: ["5433:5432"],那么外部(包括pgadmin容器)要连的端口就是5433,而不是5432。记住:ports字段是“宿主机端口:容器端口”,而pgadminpostgres在同一个 Docker 网络里,它们之间通信用的是容器端口,即5432

5.3 “备份文件找不到”:pg_dump的工作路径与挂载卷的博弈

在 pgAdmin 里点击Tools -> Backup,选择Custom格式,点“Backup”,然后弹出一个保存对话框,让你选路径。你满怀希望地选了~/Downloads/myapp_backup.backup,点确定,结果等了半天,~/Downloads目录下什么也没有。

这是因为你搞错了pg_dump的执行主体。pg_dump命令是在pgadmin容器内部执行的,它看到的~/Downloads,是容器内部的路径,而不是你宿主机的~/Downloads。容器里根本没有这个目录,所以备份文件被丢进了容器的/tmp或某个临时目录,随着容器停止而消失。

正确的做法是:利用 Docker 的 volume 挂载,把宿主机的一个目录,挂载进pgadmin容器,作为备份的“落点”。修改pgadmin服务的volumes

volumes: - pgadmin_data:/var/lib/pgadmin - ./servers.json:/pgadmin4/servers.json - ./backups:/backups # 新增这一行

然后,在 pgAdmin 的备份对话框里,“File”字段,就填/backups/myapp_backup.backup。因为./backups目录被挂载到了容器的/backups,所以这个文件会实实在在地出现在你宿主机的./backups目录下。

提示:./backups目录需要提前创建,mkdir backups。并且,由于pgadmin容器内运行的用户是pgadmin(UID 5050),它需要对这个目录有写权限。在 macOS/Linux 上,执行chmod 777 backups最简单;在生产环境,应该用chown把目录所有权交给 UID 5050 的用户。

5.4 “Query Tool 执行慢”:客户端渲染瓶颈与大数据集的应对策略

当你在 Query Tool 里执行SELECT * FROM huge_table;,结果集有 10 万行,pgAdmin 页面会卡住、假死,甚至浏览器崩溃。这不是 PostgreSQL 慢,而是 pgAdmin 的前端渲染慢。它要把 10 万行数据全部加载到浏览器内存里,再用 JavaScript 渲染成 HTML 表格,这是一个 O(n) 的计算密集型任务。

解决方案有两个:

  • 前端限制:在 pgAdmin 的File -> Preferences -> Browser -> Query Tool里,把Maximum number of rows to fetch设为1000。这样,即使你执行SELECT *,它也只取前 1000 行,保证界面流畅。
  • 后端优化:养成写 SQL 的好习惯。永远不要在开发环境执行无LIMITSELECT *。用EXPLAIN ANALYZE分析慢查询,给WHERE条件的字段加索引。如果真要导出全量数据,用Tools -> Backup导出Plain格式(SQL 脚本),或者用psql命令行工具:docker exec -it postgres psql -U admin -d mydb -c "COPY (SELECT * FROM huge_table) TO '/tmp/export.csv' WITH CSV HEADER;",然后docker cp postgres:/tmp/export.csv ./export.csv拷贝出来。命令行工具没有渲染负担,处理百万行数据也如丝般顺滑。

6. 进阶技巧与团队协作规范

6.1 多环境配置:docker-compose.override.yml的威力

一个项目不可能只有一个环境。你有dev(开发)、staging(预发布)、prod(生产)三种场景。dev环境用postgres:18-alpineprod环境必须用postgres:18(因为 Alpine 的 musl libc 和 glibc 有细微差异,生产环境要追求最大兼容性);dev环境pgadmin开放5050端口,prod环境则完全不暴露pgadmin,只用psql命令行管理。

Docker Compose 的override机制就是为此而生。你保留一个基础的docker-compose.yml,定义所有服务的共性(镜像名、网络、卷)。然后,为每个环境创建一个docker-compose.<env>.yml文件,比如docker-compose.prod.yml,里面只写差异部分:

# docker-compose.prod.yml version: '3.8' services: postgres: image: postgres:18 # 生产环境需要更大的 shared_buffers command: postgres -c shared_buffers=256MB pgadmin: # 生产环境不暴露 pgadmin 端口 ports: []

启动时,用-f参数指定多个文件:

# 启动生产环境 docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d # 启动开发环境(默认会加载 docker-compose.override.yml) docker compose up -d

docker-compose.override.yml是 Compose 的默认覆盖文件,你把它当作dev环境的专属配置。这种“基线 + 覆盖”的模式,让配置管理变得清晰、可追溯、无重复。我所有的项目,都严格遵循这个三层结构:docker-compose.yml(基线)、docker-compose.override.yml(开发)、docker-compose.prod.yml(生产)。

6.2 安全加固:从127.0.0.1绑定到反向代理的平滑过渡

127.0.0.1:5050:5050是开发环境的安全底线,但它无法满足远程协作的需求。比如,你在家办公,想让在北京的同事也能访问你的 pgAdmin 查看数据库结构。此时,0.0.0.0:5050:5050是最直接的方案,但风险极高。

更优雅的方案是引入Nginx 反向代理。你启动一个 Nginx 容器,让它监听80端口,然后把所有对http://your-server-ip/pgadmin/的请求,转发给http://pgadmin:5050/。Nginx 作为入口网关,可以轻松加上 HTTPS、Basic Auth、IP 白名单等安全层。

一个最小化的nginx.conf示例:

upstream pgadmin_backend { server pgadmin:5050; } server { listen 80; server_name _; location /pgadmin/ { proxy_pass http://pgadmin_backend/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 重写 URL,去掉 /pgadmin 前缀 proxy_redirect http:// $scheme://$host/pgadmin/; } }

然后在docker-compose.yml里加入nginx服务,并把它和pgadmin放在同一个pgnetwork里。这样,你的同事访问http://your-server-ip/pgadmin/,就能看到 pgAdmin 登录页,而pgadmin容器本身仍然只绑定127.0.0.1:5050,安全边界从未被突破。反向代理不是银弹,但它是把“便利”和“安全”这对矛盾,调和到最佳平衡点的成熟实践。

6.3 CI/CD