The Container Is Healthy, The Query Is Not
Your Postgres container has been up for weeks. The healthcheck is green. Then somebody runs a report at 2 AM and gets this:
ERROR: could not resize shared memory segment "/PostgreSQL.1363081192" to 67244032 bytes: No space left on deviceThe disk has plenty of space. The culprit is /dev/shm, which Docker caps at 64MB for every container unless you say otherwise. Set shm_size (or min_dynamic_shared_memory) on every Postgres container you run, and keep it under the container’s memory limit. The second half of that sentence is the part that bites.
Full example: Clone the working files at github.com/KingPin/sumguy-examples/docker/postgres-shm-size
Why It Only Breaks Later
Most people assume shared_buffers lives in /dev/shm. It doesn’t. Since Postgres 9.3, the main shared memory is an anonymous mmap. So the server starts fine with the default shared_buffers = 128MB inside a 64MB /dev/shm, and pg_isready never complains.
What does use /dev/shm is dynamic shared memory (DSM). With the default dynamic_shared_memory_type = posix, parallel query workers allocate segments there. Parallel Hash joins are the heavy users, because the hash table is shared between workers.
Small tables never trigger it. The failure arrives when your data grows, or when someone raises work_mem. That makes it a time bomb: the container passes every test on day one and detonates on the first big analytical query.
Check your own container right now:
docker exec my-postgres df -h /dev/shmFilesystem Size Used Avail Use% Mounted onshm 64M 1.1M 63M 2% /dev/shmThat 64M is the whole budget. And check the setting Postgres uses:
SHOW dynamic_shared_memory_type;Reproducing It
The companion kit builds a 5M-row table and runs a self-join:
CREATE TABLE big AS SELECT g AS id, md5(g::text) AS payload FROM generate_series(1, 5000000) g;ANALYZE big;
SET work_mem = '64MB';SELECT count(*) FROM big a JOIN big b USING (id);I ran this on postgres:18.6 with Docker 29.9.2. The defaults there are work_mem = 4MB and max_parallel_workers_per_gather = 2. With those defaults, the join succeeds inside 64MB. Good news, and also the trap.
Raise work_mem to 16MB (with 4 workers forced) and it fails. 32MB, 64MB and 256MB fail too. Even with the default 2 workers, work_mem = 64MB fails. So the gotcha fires the day you follow a tuning guide, and every tuning guide tells you to raise work_mem.
Fix 1: shm_size
Give the container a bigger /dev/shm:
services: db: image: postgres:18.6 shm_size: 1gb environment: POSTGRES_PASSWORD: exampleOr with plain Docker:
docker run -d --shm-size=1g -e POSTGRES_PASSWORD=example postgres:18.6With shm_size: 1gb the same query succeeds. I polled df while it ran, and peak /dev/shm use was about 317MB (324,284 KB). That number was the same for 4 workers at 256MB work_mem and for 2 workers at 64MB.
The official image docs on Docker Hub suggest --shm-size=256MB. That would still have failed for this query, which needed roughly 317MB. Size the value from your workload. Run your worst report, watch df -h /dev/shm, then add headroom.
Fix 2: min_dynamic_shared_memory
Postgres 14 added min_dynamic_shared_memory. It reserves memory for parallel queries at server start, and it takes that memory from the main shared memory segment, not from /dev/shm. The default is 0. You can only change it at server start.
services: db: image: postgres:18.6 command: ["postgres", "-c", "min_dynamic_shared_memory=512MB"] environment: POSTGRES_PASSWORD: exampleOr in postgresql.conf:
min_dynamic_shared_memory = 512MBI left /dev/shm at the default 64MB and set 512MB here. The query succeeded, and /dev/shm sat at 32K used the whole time. The parallel workers never touched it.
The tradeoff is that Postgres holds that memory from startup whether any query uses it or not. Once concurrent queries exhaust the region, the overflow goes back to /dev/shm through the usual path. The Postgres docs also say this region benefits from huge_pages.
Pick one fix, or run both. I lean on shm_size as the baseline because it costs nothing until a query actually needs the space.
One option to skip: dynamic_shared_memory_type = mmap. The Postgres docs say it “is generally discouraged because the operating system may write modified pages back to disk repeatedly, increasing system I/O load”. You would be trading a clean error for slow disk churn.
The Memory Limit Trap
Now the part that makes this more than a one-line fix. /dev/shm is tmpfs, and tmpfs pages count against the container’s memory limit through its memory cgroup. A big shm_size is a promise your memory limit may not be able to keep.
I tested exactly that: shm_size: 1gb with a 256MB limit.
services: db: image: postgres:18.6 shm_size: 1gb mem_limit: 256m memswap_limit: 256m environment: POSTGRES_PASSWORD: exampleThe same join now gets a Postgres process SIGKILLed. Which one varies by run: sometimes the backend running the query, sometimes one of its parallel workers.
client backend (PID 101) was terminated by signal 9: Killedall server processes terminated; reinitializingdocker inspect reports OOMKilled=true. The postmaster can’t trust shared memory after a child dies that way, so it restarts every backend, and every connection drops.
Compare the two failures. With the 64MB default, one query gets a clean SQL error and everything else keeps running. With shm_size above mem_limit, you swap that for a crash-restart of the whole database, and every app connected to it reconnects at once.
The rule: peak /dev/shm use plus shared_buffers plus per-backend memory must fit under the memory limit. tmpfs only charges the pages a query writes, so the shm_size cap itself costs nothing. The real use does. If your worst query plus the rest of Postgres won’t fit, raise the limit or cap parallelism.
Capping Usage Instead
Sometimes the right answer is to ask for less shared memory. Lower max_parallel_workers_per_gather or work_mem. For one stubborn query, turn parallelism off in the session:
SET max_parallel_workers_per_gather = 0;The plan loses its parallel workers, so no DSM segments are needed, and /dev/shm stays out of it.
Same Gotcha On Kubernetes
Pods also get a 64MB /dev/shm by default. The usual fix is an emptyDir volume with medium: Memory and a sizeLimit, mounted at /dev/shm:
volumes: - name: dshm emptyDir: medium: Memory sizeLimit: 1Gicontainers: - name: postgres volumeMounts: - name: dshm mountPath: /dev/shmCommon Questions
How big should shm_size be for Postgres in Docker?
Size shm_size from your largest parallel query, then add headroom. A parallel hash join over a 5M-row self-join peaked at about 317MB of /dev/shm. Run your worst report, watch df -h /dev/shm, and start at 1GB if you are unsure. Keep the total under the container memory limit.
Do I need to recreate the Postgres container to change shm_size?
Yes. Docker sets shm_size when it creates the container, and docker update has no flag for it. Edit compose.yaml and run docker compose up -d: Compose recreates the Postgres container with the new /dev/shm. Data on a named volume survives the recreate, but every open connection drops during the restart.
Does shm_size count against the Docker memory limit?
Yes. /dev/shm is tmpfs, and its pages count against the container memory cgroup. With shm_size: 1gb and mem_limit: 256m, a Postgres process was OOM-killed and docker inspect showed OOMKilled=true. The Postgres postmaster then restarts all backends, which drops every open connection to the database.
Can I fix the error without changing the Docker run flags?
Yes. Set min_dynamic_shared_memory in Postgres 14 or later. Postgres reserves that memory inside its main shared segment at startup, so parallel queries skip /dev/shm. A 512MB setting let the failing join succeed inside the default 64MB /dev/shm. The setting needs a server restart.