3. PostgreSQL容器化基础
在 Kubernetes 上运行 PostgreSQL,第一步不是写 YAML,而是先搞清楚“容器里到底装了什么”。官方 postgres 镜像已经替我们做了大量约定:数据目录固定为 /var/lib/postgresql/data,初始化脚本放在 /docker-entrypoint-initdb.d,通过一组 POSTGRES_* 环境变量控制首次启动行为。理解这些约定,才能判断哪些配置应该用环境变量、哪些应该写进自定义镜像、哪些必须交给持久卷。
官方镜像的启动逻辑
官方镜像的入口脚本 docker-entrypoint.sh 会先判断数据目录是否为空。如果为空,说明是首次启动,它会执行 initdb 创建数据库集簇,然后按文件名字典序执行 /docker-entrypoint-initdb.d 下的 .sh、.sql、.sql.gz 脚本;如果目录非空,则跳过初始化,直接以现有数据启动。这个“空目录才初始化”的判断非常关键:它意味着初始化脚本只会在第一次启动时运行一次,后续重启不会重复执行。因此在 Kubernetes 中,只要 PVC 里已有数据,改初始化脚本不会生效,必须通过迁移工具或手动执行来变更。
镜像支持的主要环境变量包括:POSTGRES_PASSWORD(必填,除非用 POSTGRES_HOST_AUTH_METHOD=trust)、POSTGRES_USER(默认 postgres)、POSTGRES_DB(默认与用户名同名)、POSTGRES_INITDB_ARGS(传给 initdb 的参数,如 --data-checksums)、PGDATA(自定义数据目录)。这些变量只在首次初始化时被读取,之后修改不会改变已有集簇。
POSTGRES_PASSWORD 一旦用于初始化,就写入了数据库的认证信息。后续即使删除该环境变量,数据库仍使用旧密码。生产环境应通过 Secret 注入,而不是明文写在 Deployment 里。数据目录与持久化边界
容器本身是无状态的,PostgreSQL 的状态全部落在 PGDATA 指向的目录。在 Kubernetes 中,这个目录必须挂载到 PersistentVolumeClaim 上,否则 Pod 重建后数据丢失。一个常见误区是把整个 /var/lib/postgresql/data 挂载为 emptyDir 或宿主机目录,前者随 Pod 消失,后者在多节点调度时无法保证数据一致性。正确做法是声明 PVC,并在容器中通过 volumeMounts 挂到 PGDATA。
另一个细节是权限。官方镜像以 postgres 用户(UID 999)运行,如果 PVC 的挂载点属主不是 999,initdb 会因无法写入而失败。部分存储类支持 fsGroup,可在 Pod 的 securityContext 中设置 fsGroup: 999,让 kubelet 自动调整卷的组权限。若存储类不支持,则需要在初始化容器中手动 chown。
初始化脚本的编写与执行顺序
初始化脚本适合做建库、建用户、装扩展、导入基础数据等一次性操作。脚本按文件名字典序执行,因此建议用数字前缀控制顺序,例如 01-create-extensions.sql、02-create-schema.sql、03-seed-data.sql。SQL 脚本默认以 POSTGRES_USER 身份连接到 POSTGRES_DB 执行;如果需要切换数据库,可在脚本里用 \c 或 psql -d。
-- 01-create-extensions.sql
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- 02-create-app-user.sql
CREATE ROLE app_user LOGIN PASSWORD 'change-me';
GRANT CONNECT ON DATABASE appdb TO app_user;
需要强调的是,初始化脚本运行在 initdb 之后、数据库正式对外服务之前,此时 PostgreSQL 只监听本地 socket。脚本执行失败会导致容器启动失败,因此脚本应尽量幂等、可重入,避免依赖外部网络资源。
构建自定义镜像
官方镜像能满足大部分场景,但当我们希望固化扩展、预置配置模板、内置备份工具或统一时区与 locale 时,就需要构建自定义镜像。推荐用多阶段或直接基于官方镜像扩展:
FROM postgres:16.3-bookworm
# 安装扩展与工具
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
postgresql-16-pg-stat-statements \
postgresql-16-cron \
wal-g \
&& rm -rf /var/lib/apt/lists/*
# 预置初始化脚本
COPY initdb/ /docker-entrypoint-initdb.d/
# 覆盖默认配置模板
COPY conf/postgresql.conf /etc/postgresql/postgresql.conf
COPY conf/pg_hba.conf /etc/postgresql/pg_hba.conf
这里有一个容易踩的坑:官方镜像的 CMD 是 postgres,入口脚本会读取 PGDATA 下的 postgresql.conf。如果我们把配置放在 /etc/postgresql,需要在启动参数中显式指定 -c config_file=/etc/postgresql/postgresql.conf,否则不会生效。更稳妥的做法是把配置作为 ConfigMap 挂载,并在 command 中传入 postgres -c config_file=...。
推送与版本管理
构建完成后,按 registry.example.com/platform/postgres:16.3-custom-v1 这样的规则打标签并推送。标签应包含基础版本和自定义版本,便于回溯。推送前用 docker run 本地验证初始化脚本能正常执行、扩展能加载、配置生效。推送后,在 Kubernetes 的 StatefulSet 中引用该镜像,并通过 imagePullPolicy: IfNotPresent 避免每次重启都拉取。
关键配置示例
下面是一份可直接参考的 StatefulSet 片段,展示镜像、环境变量、初始化脚本挂载与持久化的配合方式。示例参数属于教学假设,实际使用时需替换为真实仓库地址与密码来源。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: pg
spec:
serviceName: pg
replicas: 1
selector:
matchLabels: { app: pg }
template:
metadata:
labels: { app: pg }
spec:
securityContext:
fsGroup: 999
containers:
- name: postgres
image: registry.example.com/platform/postgres:16.3-custom-v1
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5432
env:
- name: POSTGRES_USER
value: app_user
- name: POSTGRES_DB
value: appdb
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: pg-secret
key: password
- name: POSTGRES_INITDB_ARGS
value: "--data-checksums --encoding=UTF8"
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 20Gi
注意 PGDATA 被设置为子目录 pgdata,这是官方镜像推荐的做法:因为挂载点本身可能包含 lost+found 等目录,直接作为 PGDATA 会导致 initdb 认为目录非空而拒绝初始化。把数据放在子目录里,可以规避这个问题。
docker run --rm -e POSTGRES_PASSWORD=test registry.example.com/platform/postgres:16.3-custom-v1,日志中出现 database system is ready to accept connections 即表示初始化链路正常。总结来看,官方镜像的“空目录才初始化”机制决定了初始化脚本的一次性特征,数据目录必须持久化且权限正确,自定义镜像适合固化扩展与配置模板,而环境变量只在首次启动生效。把这四点理清,后续在 Kubernetes 中编排 StatefulSet、配置探针和备份策略时,就不会因为镜像行为理解偏差而反复排障。