
前阵子要在 Ubuntu 22.04 上给团队搭一套内部系统数据层选了 PostgreSQL 16.10。起初图省事直接用apt install postgresql结果先是版本和系统源对不上后面想升级又怕数据文件散落在各个目录里不好收拾。折腾一圈后我彻底换成容器化部署一条docker run把数据库跑起来数据目录独立挂载备份、升级、换机器都变得非常干净。这篇保姆级指南就基于我实际操作的过程整理把 Ubuntu 下的 PostgreSQL 16.10 容器化部署从环境准备、镜像选择、数据卷挂载到日常备份和升级防坑全部过一遍适合刚接触 Docker 的运维同学也适合想从裸机安装迁到容器里的开发者。1. 为什么我建议把 PostgreSQL 16.10 跑在 Ubuntu 容器里1.1 裸机安装的痛点版本碎片化与卸载残留Ubuntu 的官方 apt 源里虽然有 PostgreSQL但版本往往比官方最新系列落后不少。想装 16.10 这种具体修订版本通常得额外添加 PostgreSQL 官方维护的 apt 源或者用第三方 PPA。这两个路子都能走通但有个非常现实的问题Ubuntu 系统源和 PostgreSQL 源混在一起后依赖关系变得很脆弱。比如你装过postgresql-15再装postgresql-16系统里会同时存在多个版本的数据目录和启动脚本一旦后续做apt autoremove很容易把还在用的旧版本组件清掉导致服务起不来。卸载也不省心。裸机安装的 PostgreSQL 会在/etc/postgresql、/var/lib/postgresql、/var/log/postgresql等多个目录写文件手动卸载很难清理干净。我帮同事处理过一台机器卸载后ps aux | grep postgres看不到进程了但apt list --installed | grep postgres还能列出一堆残留包端口也被占着。这种卸载残留问题在容器环境下基本不存在容器就是一个隔离的运行单元删掉容器和镜像剩下的只有你主动挂载出来的数据卷想要回退或重建都很直接。1.2 容器化带来的四个具体收益第一条是环境一致性。同一个postgres:16.10镜像在开发笔记本、测试服务器、生产服务器上跑出来的数据库核心行为是一致的不会出现在我机器上是好的这种尴尬。镜像里已经打包好了 PostgreSQL 的可执行文件、默认配置和依赖库宿主机上缺什么库、系统版本差多少都不影响数据库运行。第二条是一键回滚。裸机升级 PostgreSQL 小版本时要备份、停服、替换二进制、重启、验证每一步出错都要手动恢复。容器部署时升级只是换一个镜像标签把数据卷挂载到postgres:16.9上它就是 16.9挂到postgres:16.10上它就是 16.10。当然实际升级不能这么粗暴后面第 5 章我会细说但回滚的基础能力是容器天生就有的。第三条是资源隔离。数据库吃内存和 CPU 比较凶通过 Docker 的--memory和--cpus参数可以直接限制 PostgreSQL 容器的资源上限避免它把宿主机拖垮。这在裸机安装时反而要多做一层 systemd 配置或 cgroup 设置门槛高不少。第四条是可移植性。整个数据库环境可以用一个 Compose 文件或docker run命令描述出来换台新机器把命令和数据卷备份带过去几分钟就能恢复一个同样环境。对团队协作来说这种基础设施即代码的思路比在 wiki 里写十步安装文档可靠得多。1.3 什么时候不建议用容器跑数据库我也不是无脑推荐所有场景都容器化。单机小项目、对磁盘 IO 延迟极度敏感、已经有成熟裸机 HA 运维体系的团队不一定要折腾容器。容器本身有额外一层的网络和存储转发开销虽然 PostgreSQL 官方镜像质量很高但这种开销在极端高并发场景下确实可感知。更重要的一点容器化并没有降低数据库运维的复杂度只是把复杂度从装软件转移到了管数据。如果你对 Docker 的数据卷、网络模型完全不熟直接上手容器数据库遇到数据丢失问题时反而比裸机更难排查。我的建议是先理解本章后面讲的数据卷和备份机制再动手部署否则不如老老实实用裸机。2. 部署前置工作Ubuntu 版本确认、Docker 安装与镜像加速2.1 先确认宿主机信息动手前先花一分钟确认系统版本和 CPU 架构。不同架构对应的镜像虽然都是postgres:16.10但 Docker 拉取时会自动匹配宿主机的平台提前确认可以避免后续排查不必要的困惑。# 查看 Ubuntu 版本 lsb_release -a cat /etc/os-release # 查看 CPU 架构 uname -m我这边输出是x86_64Ubuntu 22.04.4 LTS。如果你的机器是 ARM 架构比如树莓派或部分云服务器同样能跑 PostgreSQL 官方镜像官方仓库里有 arm64 版本Docker 会自动选择。只要内核是 64 位基本都能顺畅部署。确认完架构后建议顺手sudo apt update sudo apt upgrade -y把系统基础包更新一下。特别是内核版本不要太旧否则 Docker 的存储驱动和网络功能可能会受限。注意升级内核后可能要重启机器如果机器上有存量的重要服务先评估再操作。2.2 安装 Docker Engine走官方 apt 源而不是 snap 版Ubuntu 上最容易踩的坑是直接sudo snap install docker。Snap 版 Docker 用起来挺省事但它有几个问题一是 snap 包在部分云主机或容器环境里跑不起来二是它和 systemd 的集成方式不太常规后续配 docker-compose、配日志轮转时容易遇到奇怪问题三是版本更新节奏不完全受自己控制生产环境需要的是确定性的版本。我推荐用 Docker 官方 apt 源安装 Docker Engine步骤固定版本可控。在这之前先把可能存在的旧版 Docker 清掉sudo apt remove docker docker-engine docker.io containerd runc sudo snap remove docker 2/dev/null || true然后安装依赖并添加 Docker 官方 GPG 密钥和 apt 源sudo apt update sudo apt install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod ar /etc/apt/keyrings/docker.gpg echo \ deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release echo $VERSION_CODENAME) stable | \ sudo tee /etc/apt/sources.list.d/docker.list /dev/null这里要注意$(. /etc/os-release echo $VERSION_CODENAME)会自动识别 Ubuntu 的代号22.04 对应jammy24.04 对应noble。别手动写死版本代号否则升级系统后源会失效。接着安装 Docker 引擎和 Compose 插件sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin装完后启动并验证sudo systemctl enable --now docker sudo docker version能看到 Client 和 Server 两段版本信息说明 Docker 守护进程已经正常工作了。如果需要当前用户免 sudo 执行 docker 命令把自己加入 docker 组并重新登录即可sudo usermod -aG docker $USER2.3 配置镜像加速器拉镜像更快更稳国内环境直接拉 Docker Hub 镜像经常超时建议在/etc/docker/daemon.json里配置 registry mirror。新建或编辑这个文件{ registry-mirrors: [ https://docker.m.daocloud.io, https://dockerproxy.com ] }实际可用加速地址变化很快这个文件里的示例我建议只用来测试连通性生产环境最好用公司内网的私有镜像仓库或不依赖第三方加速。配置完后重启 Dockersudo systemctl restart docker docker info | grep -A5 Registry Mirrors能看到镜像加速地址列表说明生效了。如果重启后 docker 起不来先journalctl -u docker -n 50查日志大概率是 daemon.json 格式写错或者网络不通。2.4 拉取镜像为什么用postgres:16.10而不是 latest镜像标签的问题值得单独说。很多教程喜欢用latest或16但这两个标签都是浮动的latest永远指向当前最新版本16指向 16 系列当前最新修订版。今天部署时用的是 16.10明天镜像仓库更新后你再 pull 一次可能就变成 16.11 了。数据库这种对版本敏感的服务我强烈建议锁定具体小版本标签docker pull postgres:16.10 docker images | grep postgrespostgres:16.10这种标签格式表示 16 主版本下的第 10 个修订版本如果哪天官方镜像仓库里没有精确的 16.10 标签那就拉取当时仓库里最新的 16.x 标签比如postgres:16.11并把本文所有命令里的版本号同步替换。这个标签锁定习惯能避免明明没升级重启后数据库却换了个版本的诡异问题。拉完镜像后可以顺手docker inspect postgres:16.10看一眼镜像的架构、Entrypoint 和环境变量定义后面手动写 run 命令时心里更有底。3. 容器启动与初始化run 命令逐项拆解、数据卷挂载、首次配置3.1 最简部署命令一条命令跑起来这是我常用的生产起步命令不是最花哨的但足够稳docker run -d \ --name pg16 \ --restart always \ -p 5432:5432 \ -e POSTGRES_USERappuser \ -e POSTGRES_PASSWORDStrongPassword!2024 \ -e POSTGRES_DBappdb \ -e TZAsia/Shanghai \ -v pgdata:/var/lib/postgresql/data \ -v /etc/localtime:/etc/localtime:ro \ postgres:16.10逐项拆解一下-d后台运行容器不会霸占当前终端。--name pg16给容器起个固定名字后续docker logs pg16、docker exec -it pg16都靠它定位。--restart always宿主机重启、Docker 守护进程重启后容器会自动拉起。数据库服务必须设置这个策略否则机器一重启数据库就失联。-p 5432:5432把宿主机的 5432 端口映射到容器内的 5432。前面不写 IP 表示监听宿主机所有网卡生产环境建议改成-p 127.0.0.1:5432:5432或内网 IP避免直接暴露公网。-e POSTGRES_USER指定超级用户名。不设置的话默认是postgres。-e POSTGRES_PASSWORD超级用户密码。这个必须设置官方镜像在未设置密码时会拒绝启动数据库。-e POSTGRES_DB初始化时自动创建的数据库名不设置则默认创建一个与用户同名的库。-e TZAsia/Shanghai设置容器时区影响数据库会话的now()等时间函数结果。-v pgdata:/var/lib/postgresql/data把命名数据卷挂载到容器内的数据目录数据落在这块卷上。-v /etc/localtime:/etc/localtime:ro让容器和宿主机共享时间配置文件进一步对齐时间。执行完命令后用docker ps看容器状态。第一次启动会有一个初始化过程大概几秒到几十秒取决于磁盘速度。状态显示healthy或Up就基本没问题。3.2 为什么必须挂载数据卷容器文件系统的生命周期容器本身是临时工镜像提供了只读的文件系统层容器内的写入一开始落在容器可写层上。如果哪天你执行docker rm pg16这个可写层连同里面的数据会一起被删掉数据直接没了。裸机安装时数据文件散落在系统目录里至少删除还有心理准备容器里一旦误删找回难度更大。所以数据卷挂载是容器数据库的命根子。上面命令里的-v pgdata:/var/lib/postgresql/data创建了一个由 Docker 管理的命名卷数据存放在宿主机/var/lib/docker/volumes/pgdata/_data下。想确认数据目录状态docker volume inspect pgdata如果想把数据放在更好找的目录比如/data/pgsql用 bind mountmkdir -p /data/pgsql sudo chown -R 999:999 /data/pgsql docker run -d --name pg16 -v /data/pgsql:/var/lib/postgresql/data ...这里有个关键细节PostgreSQL 官方镜像默认以 uid 999 的 postgres 用户运行bind mount 的宿主机目录必须给 uid 999 读写权限否则容器启动时没权限写数据目录报错退出的概率极高。chown 999:999是为了卷目录权限做准备命名卷由 Docker 自动处理bind mount 则需要手动处理。提示PostgreSQL 18 之后官方镜像把默认数据目录路径改成了/var/lib/postgresql/18/docker这类带主版本号的目录16.10 仍然使用/var/lib/postgresql/data。你要是以后升级到 18记得同步换挂载路径别照搬 16 的配置。3.3 首次初始化数据库名、用户、时区、编码怎么定官方镜像在数据目录为空的情况下会执行一次数据库初始化你通过环境变量指定的参数只在这次初始化里生效。一旦数据目录里已经有数据再改环境变量是不会重建的这点经常有人踩坑。建议这样组合-e POSTGRES_USERappuser \ -e POSTGRES_PASSWORDStrongPassword!2024 \ -e POSTGRES_DBappdb \ -e POSTGRES_INITDB_ARGS--encodingUTF8 --lc-collateC --lc-ctypeC \关于排序规则lc-collate我推荐保持C而不是用en_US.UTF-8。原因很实际C排序规则在索引和比较上的性能更稳定en_US.UTF-8虽然更符合语言习惯但不同环境下的 locale 数据可能有细微差异迁移数据时容易出现排序不一致。如果业务对中文排序有特殊要求再单独在建表时指定COLLATE不要在集群级去赌 locale 一致性。时区设置建议双保险环境变量TZAsia/Shanghai加上挂载/etc/localtime。容器默认时区是 UTC数据库的now()返回的是容器时间不设置的话你会在连接工具里看到比北京时间慢 8 小时的怪现象。注意这只影响客户端会话显示timestamptz类型存的是绝对时间不受影响但运维时看日志、看last_activity非常容易被 8 小时差价搞晕。3.4 容器启动失败的排查链路第一次启动遇到问题是最正常的我按排查顺序给你列一下第一步docker ps -a看容器退出码。退出码 0 表示正常退出通常是环境变量或命令有逻辑问题非 0 退出码则要看日志定位。第二步docker logs pg16 --tail 50看具体报错。最常见的几类The database cluster was initialized with ...说明数据目录已有旧数据和环境变量冲突。chmod: changing permissions of /var/lib/postgresql/data: Operation not permitted说明 bind mount 目录权限不对。could not bind to address ...: Address already in use说明宿主机 5432 端口被其他进程占用了可以先sudo ss -lntp | grep 5432找出来。FATAL: could not write to file pg_wal/... : No space left on device是磁盘满了这种要先清磁盘再启动。第三步确认环境变量。没有设置POSTGRES_PASSWORD时官方镜像会故意启动失败并打印提示这是安全机制不是 bug。还有一种常见问题容器起来了但数据库监听异常。可以用docker exec -it pg16 pg_isready -U appuser -d appdb在容器内部验证。如果宿主机连接不上但容器内正常重点查端口映射和防火墙而不是数据库本身。4. 连接测试与服务巡检从宿主机、容器、外部客户端三个视角4.1 先看容器状态和健康检查部署完成后第一件事是确认服务真的活着。docker ps只显示进程级存活不代表 PostgreSQL 接受连接。我习惯在 run 命令里直接带健康检查参数docker run -d --name pg16 \ --health-cmdpg_isready -U appuser -d appdb \ --health-interval30s \ --health-timeout5s \ --health-retries3 \ ...pg_isready是 PostgreSQL 自带的探活工具能建立 TCP 连接并发送 ping返回accepting connections就说明数据库正常。配合健康检查后docker ps里能看到STATUS列变成Up X minutes (healthy)脚本里也能用docker inspect --format {{.State.Health.Status}} pg16拿到状态。注意健康检查本身有代价。pg_isready每秒或每 30 秒连一次数据库如果连接数被健康检查占满反而会影响业务。线上实例把--health-interval调成 60 秒--health-timeout调成 10 秒别写太激进。4.2 宿主机与本容器内的连接测试容器跑起来后先做最小范围的连通性验证。在宿主机上pg_isready -h 127.0.0.1 -p 5432如果宿主机没装 PostgreSQL 客户端可以用容器内的 psqldocker exec -it pg16 psql -U appuser -d appdb -c SELECT version();看到 PostgreSQL 16.10 的版本信息就说明数据库对外服务正常。接着顺手验证一下核心配置SHOW listen_addresses; SHOW max_connections; SHOW timezone;listen_addresses在官方镜像里默认是*也就是容器内所有网卡都监听。max_connections默认 100如果业务并发高通过-c max_connections200传递给容器启动命令调整或者挂载自定义postgresql.conf。这里提醒一句别仗着容器方便就反复乱改配置每次配置变更后一定要做SELECT pg_reload_conf();而不是直接重启容器重启会让所有活跃会话断掉。4.3 其他容器通过自定义网络访问如果你业务应用也跑在 Docker 里最推荐的连接方式是创建一个自定义网络让应用容器和数据库容器在同一个 Docker 网络内通信这样连宿主机端口都不用暴露docker network create app-net docker network connect app-net pg16 docker run -d --name app --network app-net -e DB_HOSTpg16 ...在app-net网络里应用容器可以直接用pg16作为数据库主机名去连接Docker 内置 DNS 会解析到容器 IP。这种方式的优点有三个不依赖宿主机的端口占用情况-p映射可以省略或只暴露给运维跳板机。容器间数据包默认不经过宿主机端口监听层少一层转发。只有加入app-net的容器才能连接数据库网络隔离更干净。如果数据库容器和应用容器不在同一个网络连接时就只能用宿主机 IP 加映射端口这也能工作但安全性差一截。我的习惯是同一台机器上的多个服务全部放进一个自定义网络数据库只在内网网络里提供服务不对外映射端口。4.4 外部客户端Navicat/psql连接与远程访问注意事项用 Navicat、pgAdmin 或本地 psql 连接时连接参数是主机填宿主机 IP端口 5432用户和密码用POSTGRES_USER、POSTGRES_PASSWORD设置的值。官方镜像的默认pg_hba.conf会允许所有主机使用密码认证scram-sha-256 或 md5具体看版本所以外部连接不上的排查优先级是容器端口映射有没有开 - 宿主机防火墙有没有放行 - 密码对不对而不是先去改 PostgreSQL 配置。防火墙放行命令示例如果你用了 ufwsudo ufw allow from 192.168.1.0/24 to any port 5432 proto tcp远程访问的安全建议我按重要程度排一下不要在公网环境把 5432 端口暴露给0.0.0.0。数据库是被扫描的重点目标密码爆破脚本分分钟就能盯上。用强密码并且定期换。PostgreSQL 的密码复杂度完全靠自觉。生产环境给超级用户改名或者建一个仅用于业务的最小权限用户日常连接用业务账号。对关键实例开启 SSL 连接镜像里默认没有配置证书这个需要额外挂载证书文件并修改postgresql.conf我会在后续文章里专门写。5. 备份恢复、版本升级与日常运维的防坑清单5.1 逻辑备份pg_dump / pg_restore 的标准姿势容器数据库没有免死金牌备份必须自己扛。最常用的是逻辑备份工具pg_dump它把数据库对象和数据导出成一个文件恢复时再用pg_restore导入。备份单个库docker exec -u postgres pg16 pg_dump -Fc -d appdb -f /tmp/appdb.dump docker cp pg16:/tmp/appdb.dump ./appdb_$(date %F).dump解释一下参数-Fc表示自定义格式它是压缩的能保留对象依赖信息适合pg_restore恢复-d appdb指定要导出的库-u postgres指定以 postgres 用户执行避免权限问题。docker cp把容器内临时文件拷回宿主机因为容器重建后/tmp里的文件就没了。恢复到一个新建的容器环境docker exec -i pg16 pg_restore -U appuser -d appdb --clean --if-exists ./appdb.dump--clean会在导入前删除目标库中已存在的同名对象--if-exists让删除语句不因对象不存在而报错。这样恢复是全量覆盖式的适合从备份恢复场景。如果你嫌手动麻烦可以写一个简单的定时任务。比如每天凌晨 2 点备份0 2 * * * docker exec -u postgres pg16 pg_dump -Fc -d appdb -f /backup/appdb_$(date \%F).dump这个脚本要确保宿主机有/backup目录且 Docker 用户有写权限。更严谨的做法是把 dump 文件也异地同步一份光放在同台机器上机器挂了备份就白做了。5.2 小版本升级从旧修订版切到 16.10 的容器镜像替换版本升级是容器部署最爽的场景之一。小版本升级比如 16.9 到 16.10流程如下第一步备份。先按 5.1 的方式做一次pg_dump同时也可以考虑做文件级备份停库后直接打包数据卷。第二步停掉旧容器docker stop pg16 docker rename pg16 pg16-old第三步用新镜像启动容器数据卷仍然挂载原卷环境变量保持一致docker run -d --name pg16 \ --restart always \ -p 5432:5432 \ -e POSTGRES_USERappuser \ -e POSTGRES_PASSWORDStrongPassword!2024 \ -e POSTGRES_DBappdb \ -e TZAsia/Shanghai \ -v pgdata:/var/lib/postgresql/data \ postgres:16.10由于数据卷已经初始化过镜像初始化脚本会跳过 initdb直接启动现有数据目录所以环境变量里密码、用户这些只是声明不会覆盖已有数据。第四步验证应用确认没问题之后清理旧容器docker rm pg16-old跨大版本升级15 到 16不能用这个流程必须先pg_dump或pg_upgrade。容器化解决了环境不一致的痛点但没有解决 PostgreSQL 本身的升级逻辑这点一定记牢。5.3 资源限制与日志轮转防止数据库拖垮宿主机数据库是资源大户给它一个笼子是对宿主机上其他服务的保护。启动时加资源限制--memory1g --cpus2如果容器已经启动了可以用docker container update动态调整docker container update --memory1g --cpus2 pg16内存限制要结合 PostgreSQL 的配置来定。如果给容器 1G 内存但shared_buffers设为 512MB再加上连接数和缓存的 overhead可能直接 OOM。PostgreSQL 官方建议shared_buffers设为物理内存的 25% 左右我先按 4G 内存的测试机给个经验值shared_buffers1GB、effective_cache_size3GB容器--memory4g。具体值要压测后调整别照抄。日志轮转也是容易被忽略的点。Docker 默认把 stdout 日志文件无限写在宿主机上一个高频数据库一晚上写几十 GB 日志很常见。在daemon.json里限制{ log-driver: json-file, log-opts: { max-size: 100m, max-file: 3 } }改完重启 Docker 只对新容器生效老容器要重建才应用新策略。5.4 踩过的坑时区漂移、共享内存太小、文件权限、连接数最后把我在实际部署中踩过比较有代表性的几个坑统一列一下每个都值得提前预防坑现象原因解法容器内时间慢 8 小时now()返回 UTC 时间容器默认时区 UTC-e TZAsia/Shanghai并挂载/etc/localtime/dev/shm不足大数据量排序、并行查询报 shared memory 错误容器默认/dev/shm只有 64MB--shm-size1g加大共享内存bind mount 目录权限错误容器启动即退出日志提示数据目录无权限宿主机目录属主不是 uid 999chown -R 999:999 /data/pgsql连接数被占满应用报too many connectionsmax_connections默认 100连接池配置不当调整连接池或启动时加-c max_connections300并预留连接给运维端口被占容器启动报Address already in use宿主机已有进程占用 5432ss -lntp查占用进程换端口或关停旧进程/dev/shm这个问题我要多说一句。PostgreSQL 的并行查询和部分排序操作会用到共享内存容器默认的/dev/shm很小一旦数据量大就会报could not resize shared memory segment之类的错误。解决方案是在 run 命令里加--shm-size1g。这个参数不需要在 PostgreSQL 配置里改动属于容器层面的资源分配加完就能缓解。文件权限的坑最容易阴沟翻船。如果你用 bind mount 而不是命名卷一定要确保目录属主正确。很多新手先mkdir /data/pgsql然后直接挂载容器日志里全是Permission denied。我的习惯是chown 999:999提前做掉加一句chmod 700更保险。连接数的坑则要提醒max_connections100默认值对于内部系统可能够用但很多 ORM 会默认创建连接池连接数一下就涨上去。不要盲目调大这个值先检查应用侧连接池配置再决定要不要给数据库扩容。写在最后的运维手记容器化部署 PostgreSQL 没有让我变成甩手掌柜反而把运维重点从装软件转移到了管数据上。我的习惯是无论多忙备份永远是第一优先级生产环境永远不用latest标签每次改配置后在变更记录里写上改了什么、为什么改。这套 16.10 的部署流程我复现过很多次从一台 Ubuntu 22.04 的测试机迁移到 24.04 的生产机整个过程中唯一真正费神的还是数据备份和恢复演练。如果你也准备把 PostgreSQL 塞进容器建议先在自己电脑上完整走一遍备份恢复再上生产——这一步能帮你省掉未来很多个加班夜。