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 避免每次重启都拉取。

PostgreSQL 容器化:镜像构建与启动链路 构建阶段(镜像层) 基础镜像 postgres:16.3-bookworm 安装扩展与工具 pg_stat_statements / wal-g COPY 初始化脚本 /docker-entrypoint-initdb.d/ COPY 配置模板 postgresql.conf / pg_hba.conf 推送镜像仓库 启动阶段(容器运行时) entrypoint 启动 读取 POSTGRES_* 环境变量 PGDATA 是否为空? 首次启动判断 是 initdb 创建集簇 应用 POSTGRES_INITDB_ARGS 执行 initdb.d 脚本 按文件名字典序 启动 postgres 进程 监听 5432 端口 否 跳过初始化 直接启动 持久化 PVC /var/lib/ postgresql/data fsGroup 999 权限 数据保留 Pod 重建不丢 读写 PGDATA 初始化脚本仅在 PGDATA 为空时执行一次;后续变更需通过迁移工具或手动执行

关键配置示例

下面是一份可直接参考的 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、配置探针和备份策略时,就不会因为镜像行为理解偏差而反复排障。