Add a hand-written PostgreSQL chart for toolshed

Not Bitnami's: that registry has been actively unstable here (it broke
Contour twice, infra issue #4) and PostgreSQL publishes no official chart.
A single StatefulSet, PVC and Service is small enough that owning it costs
less than depending on an unstable repackage.

Credentials come from an existing Secret rather than being generated by
the chart — a chart that generates its own password regenerates it on
every render and silently locks you out of the existing volume.

Details that matter and are easy to get wrong:
  - PGDATA is a subdirectory of the mount, not the mount itself. initdb
    refuses to run in a directory that already has contents.
  - Probes run through a shell. Kubernetes does not expand $(VAR) inside
    exec probe commands, only in command/args.
  - fsGroup 70 so the volume stays writable after the entrypoint drops
    from root to the postgres user on the Alpine variant.
  - shared_buffers cut to 32MB from PostgreSQL's 128MB default. The node
    has 8GB and was at its ceiling before this.

Verified with helm template.
This commit is contained in:
Mukul Sharma
2026-09-04 16:21:26 +05:30
parent 5d4d3d44e5
commit d4bb8985fb
5 changed files with 223 additions and 0 deletions
@@ -0,0 +1,89 @@
{{- $name := .Values.fullnameOverride | default "postgresql" -}}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ $name }}
namespace: {{ .Release.Namespace }}
labels:
app: {{ $name }}
spec:
serviceName: {{ $name }}
replicas: 1
selector:
matchLabels:
app: {{ $name }}
template:
metadata:
labels:
app: {{ $name }}
spec:
securityContext:
# The official image starts as root, initialises the data directory,
# then drops to the postgres user (uid 70 on the Alpine variant).
# fsGroup makes the provisioned volume group-writable so that drop
# still leaves the data directory usable.
fsGroup: 70
terminationGracePeriodSeconds: 60
containers:
- name: postgres
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
- -c
- shared_buffers={{ .Values.config.sharedBuffers }}
- -c
- max_connections={{ .Values.config.maxConnections }}
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: {{ .Values.existingSecret }}
key: {{ .Values.secretKeys.username }}
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.existingSecret }}
key: {{ .Values.secretKeys.password }}
- name: POSTGRES_DB
value: {{ .Values.database | quote }}
# PGDATA must be a SUBDIRECTORY of the mount, not the mount
# itself: initdb refuses to run in a directory that already has
# contents, and a freshly provisioned volume is not always empty.
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
ports:
- name: postgres
containerPort: 5432
protocol: TCP
# Run through a shell so the environment expands — Kubernetes does
# not substitute $(VAR) inside exec probe commands.
readinessProbe:
exec:
command: ["sh", "-c", 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" -h 127.0.0.1']
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
livenessProbe:
exec:
command: ["sh", "-c", 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" -h 127.0.0.1']
initialDelaySeconds: 30
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 6
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
{{- if .Values.persistence.enabled }}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: {{ .Values.persistence.storageClass | quote }}
resources:
requests:
storage: {{ .Values.persistence.size | quote }}
{{- end }}