ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Docker部署PostgreSQL生产环境实践:从docker-compose到数据卷与备份恢复

Docker部署PostgreSQL生产环境实践:从docker-compose到数据卷与备份恢复 Docker部署PostgreSQL听起来是个很常规的操作但真到生产环境落地的过程中你会发现坑全部藏在细节里。这篇文章就是我在腾讯云Ubuntu 24.04上完整走了一遍之后的记录从选型原因、环境准备、Docker安装、docker-compose编排、数据卷权限、备份恢复到安全组、时区、内存参数这些容易被忽略的角落全部展开讲一遍。不管你是第一次在云服务器上搭建数据库还是已经在用PostgreSQL但想迁到Docker环境这篇应该能帮你省掉不少试错时间。1. 为什么放弃apt安装改用Docker跑PostgreSQL1.1 我在apt方式下遇到的三个实际问题早年在Ubuntu上装PostgreSQL我习惯直接apt install postgresql。这在普通环境确实最快但对云服务器来说会带来几个隐藏成本。第一是版本锁定的问题。Ubuntu 24.04的软件源里默认提供PostgreSQL 16的某个具体小版本这个版本号会随安全更新推进也就是说你无法精确控制当前数据库跑的是16.x中的哪一个更没法在生产环境锁住某个特定补丁级别做回归验证。项目开发到一半apt upgrade之后数据库小版本跳了一两个虽然通常不会出问题但“不可预期”本身就是很大的风险。第二是配置文件分散。PostgreSQL在apt安装后主配置在/etc/postgresql/16/main/postgresql.confHBA配置在/etc/postgresql/16/main/pg_hba.conf数据目录在/var/lib/postgresql/16/main日志又在另一个位置。如果哪天真想迁移或者复制环境零零散散要拷好几个地方很容易漏。第三是卸载和排错不太干净。apt remove之后经常留下一堆数据目录和用户残留重新安装时如果不小心可能还在用旧的数据文件。对于我这种喜欢频繁做销毁重建的人来说这套体系不够“干净”。1.2 Docker方案的优势以及要接受的新问题切到Docker之后上面的问题基本都消失了镜像本身约200MBcompose文件几十行整个数据库环境就是“一卷一配置一镜像”。换机器、迁移、版本备份都非常直接官方镜像postgres的发布节奏和上游同步想锁哪个版本就锁哪个版本。但Docker也带来了新的代价我建议你提前知道数据卷权限需要自己管理。官方镜像内的postgres用户UID是999宿主机的普通用户目录如果不做处理容器会写不进去。不能再用systemctl管理数据库开机自启依赖Docker守护进程与容器restart策略。日志输出不再写到文件而是走容器日志要看历史日志得先习惯docker logs。如果对Docker网络不够熟端口映射和防火墙的坑会给你上一课。这些代价并不可怕但如果你没有心理准备第一次跑起来遇到权限报错的时候会很懵。下面我按从零到可用的顺序把每步操作和理由说清楚。2. Ubuntu 24.04上安装Docker与Compose插件2.1 服务器初始化与软件源选择我用的腾讯云服务器是2核4G的经典配置系统选了Ubuntu 24.04 LTS。刚开出来第一件事不是装Docker而是先做系统更新和基础环境确认sudo apt update sudo apt upgrade -y sudo timedatectl set-timezone Asia/Shanghai sudo apt install -y curl ca-certificates gnupg lsb-release git内核升级后如果需要重启先sudo reboot一次再继续免得后面Docker跑着跑着被系统更新打断。这里有个小细节如果直接用默认的apt源在内地的云主机上拉包速度可能比较慢而且个别包的GPG验证偶尔会卡。在腾讯云上建议把/etc/apt/sources.list.d/ubuntu.sources里的archive.ubuntu.com替换成mirrors.cloud.tencent.com腾讯云公网镜像源对自家机器速度很稳。如果你在云上部署这一步能省不少时间。2.2 安装Docker及Compose插件Ubuntu 24.04的官方软件源里有docker.io这个包但不建议装。原因有两个版本会落后官方好几个大版本而且没有docker compose子命令插件需要额外处理。更推荐添加Docker官方源用docker-ce和docker-compose-pluginsudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://mirrors.cloud.tencent.com/docker-ce/linux/ubuntu/gpg | sudo tee /etc/apt/keyrings/docker.asc /dev/null sudo chmod ar /etc/apt/keyrings/docker.asc echo deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.asc] https://mirrors.cloud.tencent.com/docker-ce/linux/ubuntu $(. /etc/os-release echo $VERSION_CODENAME) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null 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 docker --version docker compose version顺便说一句拉取docker hub镜像时如果经常超时可以在/etc/docker/daemon.json里加一个腾讯云的镜像加速地址然后sudo systemctl restart docker。这个配置对国内云主机很有用但注意不要依赖加速源解决所有问题生产环境还是建议把常用镜像提前拉到本地。2.3 UFW防火墙与Docker的端口冲突Ubuntu默认没开防火墙很多教程会让你ufw enable。但如果你同时也开了Docker会发现一个奇怪现象即使ufw deny 5432宿主机上映射出来的5432端口依然可以被外部访问。原因是Docker会在iptables里插入自己的DOCKER链而这个链的处理优先级比ufw的规则链更高。简单说Docker发布端口时直接绕过了UFW的过滤。这个行为在Ubuntu 22.04和24.04上都会出现。我的建议是放弃在操作系统层面折腾ufw改用云平台的安全组来做入口控制把安全组当作第一道也是唯一一道防火墙。这样端口控制写在控制台里规则清晰还能按IP白名单管理比在服务器上改iptables可靠得多。而且安全组规则和Docker端口映射是叠加的容器把5432映射到宿主机安全组允许某个IP访问两个条件同时满足才会通。安全组没放行的IP就算宿主机ufw全开也连不进来。3. docker-compose编排PostgreSQL 16的完整配置3.1 目录规划与数据卷权限陷阱我先讲数据卷权限因为这是新手翻车率最高的点。PostgreSQL官方镜像默认使用名为postgres的系统用户运行进程这个用户的UID是999。如果你用bind mount把宿主机的/opt/postgres/data挂进容器而这个目录的属主是root容器启动时会直接报类似data directory /var/lib/postgresql/data/pgdata has wrong ownership的错误。解决办法有三种我推荐第三种启动前手动sudo chown -R 999:999 /opt/postgres/data简单但每次创建目录都要记得执行。不挂宿主机目录直接用Docker named volume由Docker自动初始化权限。缺点是数据文件藏在/var/lib/docker/volumes/下直接浏览和备份时不方便。官方推荐的PGDATA子目录方案。把卷挂载到/var/lib/postgresql再把PGDATA指向/var/lib/postgresql/data/pgdata。这样外层目录由宿主机创建容器里的postgres用户可以在父目录下自动生成pgdata子目录。我在/opt/postgres下规划了三个目录data目录挂给数据库文件backup目录挂给pg_dump备份文件。后面备份的时候宿主机的cron直接往/opt/postgres/backup写文件即可不用再docker cp进进出出。3.2 compose文件逐段解说在/opt/postgres/docker-compose.yml里写入如下内容services: postgres: image: postgres:16.4 container_name: postgres16 restart: always environment: POSTGRES_USER: appuser POSTGRES_PASSWORD: ChangeMe_Strong! POSTGRES_DB: appdb TZ: Asia/Shanghai PGDATA: /var/lib/postgresql/data/pgdata ports: - 5432:5432 volumes: - ./data:/var/lib/postgresql/data - ./backup:/backup healthcheck: test: [CMD-SHELL, pg_isready -U appuser -d appdb] interval: 10s timeout: 5s retries: 5 start_period: 10s逐段说下几个容易被忽略的点。image: postgres:16.4我特意锁到小版本而不是用postgres:16或者postgres:latest。生产环境数据库镜像的版本漂移很危险用latest可能在某个不知道的早晨变成17.x届时docker compose pull一不小心就升级了主版本。POSTGRES_USER这个变量只在数据卷首次初始化时生效用来创建超级用户。之后修改这个字段不会改已有用户所以不要以为改了compose里的密码就能换数据库密码。POSTGRES_PASSWORD注意用引号包起来。如果密码包含$、#等特殊字符YAML解析会出问题。PGDATA对应前面提到的权限方案。这里卷挂到/var/lib/postgresql/dataPGDATA子目录设为pgdataPostgreSQL把数据文件放在/var/lib/postgresql/data/pgdata下宿主机对应/opt/postgres/data/pgdata。healthcheck容器启动后docker compose ps状态里会显示healthy/unhealthy让编排系统知道数据库真正可用而不仅仅是进程在跑。3.3 启动验证与数据库连接测试配置写完后先校验语法再启动cd /opt/postgres docker compose config docker compose up -d docker compose ps看到状态为Up并且健康检查显示healthy后进入容器测试连接docker exec -it postgres16 psql -U appuser -d appdb执行几条SQL确认基本功能SELECT version(); SELECT now();如果now()返回的时间不是北京时间说明时区设置还有问题这个坑我在5.2里单独说。4. 数据持久化、备份与恢复的干货4.1 为什么很多人的数据丢了其实大部分情况不是数据丢了而是被删了。两个最常见的误删场景执行docker compose down -v其中-v会把所有未命名的volume一起清掉如果数据存在named volume里就直接消失了。喜欢用docker run --rm测试数据库觉得方便退出后容器删了数据文件也一起没了。我的建议是除非你确定这个数据卷就是临时测试用的否则永远不要随便在down命令后面带-v。另外正式部署一定要用bind mount方式挂载宿主机目录这样就算把容器、compose项目全删了数据文件还在/opt/postgres/data下随便拷走就行。4.2 pg_dump定时备份与清理策略备份数据库最通用的是逻辑备份pg_dump。利用compose里挂载的backup目录备份文件会直接落在宿主机docker exec postgres16 pg_dump -U appuser -d appdb -Fc -f /backup/appdb_$(date %F_%H%M).dump加上宿主机cron定时任务后实现每天凌晨2点自动备份sudo crontab -e 0 2 * * * docker exec postgres16 pg_dump -U appuser -d appdb -Fc -f /backup/appdb_$(date \%F).dump find /opt/postgres/backup -name *.dump -mtime 14 -delete这里有个小坑cron里%会被解释成换行符所以$(date \%F)里的百分号一定要转义。我第一次写cron时没注意备份文件名直接错乱后来查日志才发现。-Fc表示输出自定义格式的压缩包恢复时可以用pg_restore选择性地恢复表比纯SQL备份灵活体积也小一半以上。保留14天既照顾了安全保留期也不至于把磁盘写满。4.3 灾难恢复演练恢复是很多人从来没做过的事但没演练过的备份等于没有备份。下面是一个完整的恢复流程先把备份文件拷到backup目录然后执行docker exec postgres16 pg_restore -U appuser -d appdb --clean --if-exists /backup/appdb_20250101_0200.dump如果目标库不存在可以在执行前先建库docker exec postgres16 createdb -U appuser appdb--clean会在恢复前删除已存在的同名对象--if-exists避免删除时报错。注意如果备份里包含了权限、owner信息使用当前用户恢复时可能遇到权限不足必要时加--no-owner --no-privileges只恢复数据和表结构这样可以有效减少恢复报错。我建议你定期做一次恢复演练把备份dump恢复到另一个临时库上确认文件没有损坏、数据行数对得上。这个演练过程本身很便宜但真到出事故那天你会庆幸自己做过。4.4 镜像升级时如何保住数据小版本升级比较简单cd /opt/postgres docker compose pull docker compose up -d因为数据卷没动容器重建后会继续使用旧数据启动脚本检测到PGDATA已有数据就不会重新initdb。升级前后建议先打一个备份来回滚个保险。跨大版本升级比如16升级到17不能直接替换image标签PostgreSQL的数据目录格式在不同主版本间是有可能变的。稳妥路线是在新容器里跑一个更高版本的实例然后用pg_dump/pg_restore或者pg_upgrade迁移数据。别偷懒直接换image标签跑起来报错再回滚时间成本远高于提前规划。5. 从测试环境到生产环境遇到的几个坑5.1 安全组和防火墙的双重门禁腾讯云服务器的网络入口控制有两个层面第一个是云控制台的安全组第二个是服务器内部的防火墙。很多人只改了一个地方导致数据库起好、端口也映射了外部却死活连不上。完整的排查链路确认容器端口映射docker compose ps看到0.0.0.0:5432-5432/tcp说明映射成功。在服务器本机测试nc -vz 127.0.0.1 5432本机能通说明Docker和PostgreSQL没问题。在另一台机器上测试nc -vz 服务器公网IP 5432如果本机通、外网不通基本可以断定是安全组规则没有放行。到腾讯云控制台给服务器绑定的安全组添加入站规则协议TCP端口5432源地址建议填你自己的固定公网IP或者项目允许的IP段。这里的关键经验是不要一股脑把5432对所有IP开放。数据库密码再复杂暴露到公网就多一分被爆破的风险。至少应该限制运维IP、办公网IP同一VPC内的服务器之间用内网IP互访。5.2 时区导致的时间错乱容器默认时区是UTC。即使你在宿主机设置了Asia/Shanghai容器内部如果不设置TZ环境变量进程拿到的时间仍然是世界标准时间。结果就是应用里写入的created_at等字段全部比北京时间慢8小时轻则日志时间对不上重则业务统计错乱。我的处理方式是双保险在compose的环境变量里加TZ: Asia/Shanghai让容器内系统时间对齐北京时间。进容器执行ALTER SYSTEM SET timezone TO Asia/Shanghai;然后SELECT pg_reload_conf();让PostgreSQL的timezone参数也生效。第二点很多人忽略。PostgreSQL的timezone参数默认是GMT不会自动跟随系统时区。如果你只设置了TZ环境变量SELECT now()返回的仍然是UTC。两个都配上之后now()和current_timestamp才会输出北京时间。5.3 内存配置不合理导致的性能毛刺PostgreSQL官方镜像的默认配置非常保守shared_buffers默认只有128MB。在2核4G的机器上跑点小业务看不出来一旦并发查询起来就频繁读磁盘整体表现得像老牛拉车。我参照PGTune给的推荐在compose的command里直接追加参数command: - -c - shared_buffers1GB - -c - effective_cache_size3GB - -c - work_mem32MB - -c - maintenance_work_mem256MB不同内存规格的大致参考内存shared_bufferseffective_cache_sizework_mem2G512MB1.5GB32MB4G1GB3GB32MB8G2GB6GB64MB注意work_mem不是越大越好它是“每次排序/哈希操作”可用的内存假设并发20个排序32MB就是20*32MB640MB这个账要算清楚。2核4G机器上如果盲目调到128MB甚至256MB内存OOM的风险反而更高。5.4 latest标签引起的版本漂移这一点前面提过我再展开说。postgres:latest或者不带版本号的postgres:16都会在docker compose pull时拉取新的镜像。小版本升级还算安全但大版本更新时官方会把latest指向新版本你下次重建容器数据目录版本不兼容数据库直接起不来。所以生产环境我强烈建议用包含小版本号的标签比如postgres:16.4。什么时候主动升级、用什么流程升级由你决定而不是让拉镜像这个动作替你决定。6. 安全加固与日常运维建议6.1 数据库账号权限拆分compose里用POSTGRES_USER创建的用户实际上拥有超级用户权限这个用户适合做管理不适合让业务代码直连。正确做法是另建一个只具备最小权限的应用账号。官方镜像支持把SQL脚本放在/docker-entrypoint-initdb.d/目录首次初始化数据卷时自动执行。我建了一个init.sqlCREATE USER app_rw WITH PASSWORD app_password; CREATE DATABASE appdb OWNER app_rw; GRANT CONNECT ON DATABASE appdb TO app_rw; GRANT USAGE ON SCHEMA public TO app_rw; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_rw; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_rw;compose里挂载volumes: - ./init.sql:/docker-entrypoint-initdb.d/init.sql需要强调的是这个脚本只会在PGDATA目录为空时执行一次。如果数据卷已经初始化过了想补一个脚本只能手动执行SQL或者重新初始化数据库。业务代码连接时使用app_rw.app_password管理数据库时才用compose里创建的管理员账号。这样就算应用被拖库攻击者拿到的也只是普通读写权限不能乱改数据库配置和用户。6.2 限制远程访问来源如果只有同一VPC内的应用需要连数据库最好的方案是不把5432端口暴露到公网。compose里可以只映射到内网IP或者干脆不写portsports: - 127.0.0.1:5432:5432这样只有宿主机自身可以访问其他设备都连不上。如果希望同一VPC内的其他云服务器能连就把127.0.0.1换成VPC网段对应的内网IP。如果一定要公网访问至少做到安全组源IP只放行运维/办公网段的固定IP。数据库账号使用长度不少于16位、包含大小写数字和符号的强密码。不要用默认端口可以映射成15432:5432这样不规则的外部端口减少被批量扫描命中的概率。必要时配置pg_hba.conf限制来源IP。因为容器内的pg_hba默认只能用scram-sha-256密码验证单独限制来源IP需要把自定义版本挂载进容器这个操作相对复杂网络层限制够用就不动了。6.3 日常巡检清单数据库上线后建议把下面这几件事固定成习惯每天看一眼备份目录里有没有最新dump文件别只依赖croncron也有可能因为容器重启失败而漏跑。每周用docker compose ps确认容器状态和健康检查结果如果显示unhealthy尽早查日志docker compose logs --tail100 postgres。监控磁盘空间。PostgreSQL膨胀、备份文件积累都会悄悄占满磁盘一旦满掉数据库会拒绝写入。定期查慢查询。连进数据库后执行SELECT pid, query, duration, state FROM pg_stat_activity WHERE state idle ORDER BY duration DESC;有异常长期挂起的查询及时排查。这个操作对新上线的项目尤其重要很多性能问题都是上线一周后才暴露出来的。我最后再分享一个实操习惯每次改完compose配置先跑docker compose config检查语法再执行docker compose up -d。虽然多打一条命令但能省掉很多因为YAML缩进、转义错误导致的排错时间。部署数据库没有想象中难难的是把备份、安全、升级这些“看不见的工程”提前想清楚。希望这篇文章能帮你少走几步弯路。
返回列表