Problem Description
During development of an AI training pipeline on Docker Desktop’s integrated Kubernetes cluster, the PostgreSQL pod that stores experiment metadata and feature‑store data fails to start. The pod repeatedly enters CrashLoopBackOff and the container logs contain errors such as:
2024-07-15 10:12:34.567 UTC [1] LOG: could not open configuration file "/etc/postgresql/conf.d/custom.conf": Permission denied
2024-07-15 10:12:34.568 UTC [1] FATAL: could not open configuration file "/run/secrets/pg_hba.conf": No such file or directory
Inspection of the filesystem inside the container shows that both /etc/postgresql/conf.d and /run/secrets are either empty or contain files with mode 0000. The failure blocks the downstream MLflow/feature‑store services, preventing any experiment metadata from being persisted.
Root Cause Analysis
The PostgreSQL Docker image expects configuration fragments in /etc/postgresql/conf.d and secret‑based files (e.g., pg_hba.conf) in /run/secrets. Kubernetes mounts ConfigMaps and Secrets as files, but several interactions cause the observed failures:
- Default file mode: When a ConfigMap is mounted without an explicit
defaultMode, the API server creates files with mode0644. However, on Docker Desktop (macOS/Windows) the underlying file‑sharing layer can reset permissions to0000, leading to “Permission denied”. - Missing
fsGroup: The PostgreSQL container runs as thepostgresuser (UID 999). Files created by the kubelet belong toroot. Without asecurityContext.fsGroup, the group ownership is not changed, so thepostgresuser cannot read the mounted files. - Docker Desktop hostPath limitation: The
/run/secretsdirectory is often backed by ahostPathvolume for local debugging. Docker Desktop does not support arbitrary hostPath mounts on macOS/Windows, resulting in an empty directory at runtime. - SELinux/AppArmor context: On Linux hosts, the default security context may label the volume with
root:rootand a restrictive SELinux type, preventing the container from accessing the files.
These factors combine to produce empty or unreadable configuration directories, which PostgreSQL treats as missing configuration files, causing the startup failure documented in the official PostgreSQL runtime‑config file locations (PostgreSQL Documentation).
Investigation and Debugging
Below is a reproducible debugging workflow that isolates the problem.
- Inspect pod description to verify volume mounts and security context.
kubectl describe pod pg-primary-0 -n ml-pipeline
Key fields to look for:
Volumes:ensure aconfigMapentry forconf.dand asecretentry forpg_hba.conf.Security Context:check forrunAsUser,runAsGroup, andfsGroup.
- Exec into the container before PostgreSQL starts (use an initContainer or
sleepentrypoint).
kubectl exec -it pg-primary-0 -n ml-pipeline -- /bin/bash
Run:
ls -l /etc/postgresql/conf.d
ls -l /run/secrets
stat -c "%a %U %G" /etc/postgresql/conf.d/custom.conf
Typical output from a failing pod:
-rw------- 0 root root 0 Jan 01 00:00 custom.conf
total 0
- Check pod events for mount errors.
kubectl get events -n ml-pipeline --field-selector involvedObject.name=pg-primary-0
Look for messages such as “Error mounting volume: permission denied”.
- Validate Docker Desktop file‑sharing settings.
On macOS/Windows, open Docker Desktop → Settings → Resources → File Sharing and confirm that the directory containing the secret files is shared. Missing entries cause the secret volume to be empty, as reported in the Docker Desktop documentation (Docker Desktop Kubernetes).
Resolution
The fix consists of three orthogonal adjustments:
1. Explicitly set file mode and group ownership
Add defaultMode: 0644 to the ConfigMap volume and define fsGroup: 999 (PostgreSQL’s primary group) in the pod’s securityContext. This ensures the postgres user can read the files.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: pg-primary
spec:
serviceName: pg
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
securityContext:
runAsUser: 999
runAsGroup: 999
fsGroup: 999 # <-- added
containers:
- name: postgres
image: postgres:15
volumeMounts:
- name: conf-d
mountPath: /etc/postgresql/conf.d
- name: pg-secret
mountPath: /run/secrets
volumes:
- name: conf-d
configMap:
name: pg-conf-d
defaultMode: 0644 # <-- added
- name: pg-secret
secret:
secretName: pg-secret
defaultMode: 0644 # <-- added
2. Use an initContainer to copy files with proper ownership
If the platform still enforces restrictive permissions (e.g., Docker Desktop on macOS), copy the files into an emptyDir volume owned by postgres before the main container starts.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: pg-primary
spec:
template:
spec:
initContainers:
- name: init-conf
image: busybox
command: ["/bin/sh", "-c"]
args:
- |
cp /src/* /dest/ && \
chown -R 999:999 /dest
volumeMounts:
- name: src-conf
mountPath: /src
- name: dest-conf
mountPath: /dest
containers:
- name: postgres
volumeMounts:
- name: dest-conf
mountPath: /etc/postgresql/conf.d
volumes:
- name: src-conf
configMap:
name: pg-conf-d
defaultMode: 0644
- name: dest-conf
emptyDir: {}
3. Avoid hostPath for secrets on Docker Desktop
Replace any hostPath volume used for /run/secrets with a native Secret volume. Docker Desktop does not reliably mount hostPath on macOS/Windows, leading to the empty directory observed in the “real incidents” evidence.
# Bad (hostPath)
volumes:
- name: pg-secret
hostPath:
path: /Users/me/k8s/secrets/pg_hba.conf
type: File
# Good (Secret)
volumes:
- name: pg-secret
secret:
secretName: pg-secret
defaultMode: 0644
Validation
After applying the changes, verify the pod starts cleanly:
kubectl rollout restart statefulset/pg-primary -n ml-pipeline
kubectl get pod -n ml-pipeline -w
Expected log snippet:
2024-07-15 10:15:02.123 UTC [1] LOG: database system was shut down at 2024-07-15 10:15:01 UTC
2024-07-15 10:15:02.124 UTC [1] LOG: autovacuum launcher started
2024-07-15 10:15:02.125 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
Additional checks:
kubectl exec -it pg-primary-0 -n ml-pipeline -- ls -l /etc/postgresql/conf.dshould list files with mode-rw-r--r--and ownerpostgres:postgres.kubectl exec -it pg-primary-0 -n ml-pipeline -- cat /run/secrets/pg_hba.confshould output the expected HBA rules.- Run a simple client connection:
psql -h localhost -U postgres -c "SELECT 1"
It should return 1 without errors.
Operational Experience
During the investigation we observed a few misleading cues:
- The pod status showed
CrashLoopBackOffwith no explicit “permission denied” in the first few log lines, leading some teams to suspect database corruption. - Running
kubectl logson the initContainer revealed “copying files” succeeded, yet the main container still failed because thefsGroupwas missing, a nuance highlighted in the GitHub issue postgres#1245. - On macOS, the Docker Desktop file‑sharing UI silently dropped the secret directory from the mount, which manifested as an empty
/run/secretsdirectory rather than a permission error.
Best Practices and Prevention
| Practice | Why it matters |
|---|---|
Declare defaultMode: 0644 for all ConfigMap/Secret volumes |
Prevents files from being created with 0000 permissions on Docker Desktop. |
Set securityContext.fsGroup to the PostgreSQL group (999) |
Ensures the postgres user can read mounted files regardless of owner. |
Avoid hostPath for secrets on local clusters |
Docker Desktop does not reliably expose hostPath on macOS/Windows. |
| Use an initContainer to enforce ownership when needed | Works around platform‑specific permission quirks without altering the main image. |
| Monitor volume mount events | Pod events surface mount failures early, reducing MTTR. |
Related Topic Hub: Data Infrastructure Troubleshooting Hub
FAQ
- Why does the pod work on a Linux‑based cluster but fail on Docker Desktop?
Docker Desktop’s file‑sharing layer can reset file permissions to0000and does not support arbitraryhostPathmounts, causing empty or unreadable ConfigMap/Secret files. - Do I need to set
runAsUserandfsGrouptogether?
Yes.runAsUserdefines the UID the process runs as;fsGroupchanges the group ownership of all volume files, allowing the process to read them. - Can I keep the default
defaultModeand still avoid permission errors?
Only if the underlying platform preserves the mode. On Docker Desktop you must explicitly setdefaultMode: 0644or copy files via an initContainer. - What if I need to mount a custom
postgresql.conf?
Mount the entire/etc/postgresqldirectory from a ConfigMap, setdefaultMode: 0644, and ensurefsGroupmatches the PostgreSQL group. - Is SELinux the cause of the “Permission denied” errors?
On Linux hosts with SELinux enforcing, you may need to addsecurityContext.seLinuxOptions.type: spc_tor use therunAsNonRootflag. However, the primary cause in Docker Desktop environments is the permission mode, not SELinux.