You run VACUUM FULL and it dies with ERROR: could not open file for writing: Permission denied. Same thing every time. You check the obvious stuff — file ownership, user context — and it still fails. Let's cut through the noise.
This error means PostgreSQL tried to create or rewrite a table file in your data directory and the OS said no. The cause is almost always one of three things: the PGDATA directory or its subdirectories have wrong ownership, SELinux is blocking writes, or your systemd service has ProtectSystem or ReadOnlyPaths set. I'll go from most common to least.
Cause 1: Data directory ownership is wrong
The most common culprit. Someone (maybe you, maybe a script) changed ownership of PGDATA or a subdirectory. PostgreSQL needs to write to base/, pg_wal/, and global/. If any of those are owned by root or another user, VACUUM FULL bombs out.
Here's the trigger: you moved PGDATA to a new mount point, or restored from backup using tar with default permissions. The files land with UID/GID of whatever user ran the extraction.
Check ownership:
ls -la $PGDATA | head -20
You want everything owned by the postgres user (or whatever user the server runs as). If not, fix it:
chown -R postgres:postgres $PGDATA
chmod 700 $PGDATA
Don't bother with chmod beyond 700. The data directory doesn't need group or other read access. If groups need to read for monitoring, use ACLs, but keep the base perms tight.
But wait — sometimes the ownership looks right and it still fails. That's when you check the actual table file. Use pg_relation_filepath() to see where the table lives:
SELECT pg_relation_filepath('your_table');
Then check that directory's ownership. If the parent dir is owned by someone else, you'll get the error even if the table file itself is fine.
Cause 2: SELinux blocking writes
Second most common in my experience. You're on RHEL, CentOS, or Fedora. SELinux is enforcing, and the file context on the data directory is wrong. Maybe you moved the data directory to a non-standard path like /data/postgres and didn't set the correct context.
The error message doesn't mention SELinux — it just says permission denied. Classic. And yes, ls -la will show correct ownership, but SELinux is separate.
Check if SELinux is blocking:
ausearch -m avc -ts recent | grep postgres
If you see denials, you've got two options:
- Set the correct context (the real fix).
- Turn off SELinux (don't do this unless you have to).
For a standard data directory, the context is already set by the RPM package. But if you moved things, run:
semanage fcontext -a -t postgresql_db_t "/data/postgres(/.*)?"
restorecon -Rv /data/postgres
Replace /data/postgres with your actual PGDATA path. If semanage isn't installed, install policycoreutils-python-utils (RHEL 8+) or policycoreutils-python (RHEL 7).
After that, check with ls -Z $PGDATA — you should see postgresql_db_t on the directories.
Don't bother checking getsebool for postgresql_can_network — that's for network access, not file writes. I've seen people waste an hour on that.
Cause 3: systemd hardening directives
Less common, but growing because more folks deploy with hardened systemd units. If you're on PostgreSQL 12+ and using a custom systemd service (or you've added hardening to the stock one), check for these:
systemctl cat postgresql
Look for ProtectSystem=, ReadOnlyPaths=, or ReadWritePaths=. If ProtectSystem=full or strict, it makes /usr, /boot, /etc read-only. But your data directory is usually under /var/lib/postgresql, which is still writable unless you set ReadOnlyPaths to include it.
The error shows up specifically with VACUUM FULL because it creates a new file and then removes the old one. A plain UPDATE might write to an existing file without needing to create a new one — that's why you only see it during VACUUM FULL or CLUSTER.
Fix: Edit your unit file and add the data directory to ReadWritePaths:
ReadWritePaths=/var/lib/postgresql/15/main
Then reload and restart:
systemctl daemon-reload
systemctl restart postgresql
If you're not sure if your distro's unit file has these directives, check. Debian and Ubuntu ship fairly plain unit files, but RHEL's is basic too. It's more common in containers or hardened setups.
Quick-reference summary
| Cause | Check | Fix |
|---|---|---|
| Wrong ownership | ls -la $PGDATA | chown -R postgres:postgres $PGDATA |
| SELinux context | ausearch -m avc | grep postgres | semanage fcontext -a -t postgresql_db_t + restorecon |
| systemd hardening | systemctl cat postgresql | Add ReadWritePaths |
One more thing — if you've checked all three and still see the error, look at disk space. A full disk can produce permission denied messages on some filesystems because writes fail with ENOSPC being masked. df -h and df -i to rule that out.
That covers the real-world cases I've hit. Start with ownership, then SELinux, then systemd. You'll fix it fast.