The error appears in a few shapes depending on what hit the limit:
accept tcp [::]:3000: accept4: too many open files
Error: EMFILE: too many open files, open '/app/uploads/tmp-91a2'
OSError: [Errno 24] Too many open files
dockerd: failed to start containerd: ... too many open filesAll of them mean one process reached its file descriptor limit. Sockets, pipes and inotify instances all count as descriptors, not just files on disk — which is why a service that opens no files at all can still hit this.
Work out which limit before changing anything. Raising the wrong one wastes an outage.
Find the process and its actual limit
docker inspect --format '{{.State.Pid}}' my-appWith the PID, read the limit the kernel is applying:
grep 'open files' /proc/1234/limitsMax open files 1024 1048576 filesThe first column is the soft limit — the one that produces the error. Now count what the process actually has open:
sudo ls /proc/1234/fd | wc -lIf that number is at or just under the soft limit, the limit is too low. If it is far below, something else is failing and this is not your problem.
You can also read it from inside:
docker exec my-app sh -c 'ulimit -n; ls /proc/self/fd | wc -l'Where the limit comes from
Understanding the chain saves you from raising the wrong thing twice.
Every process has a soft and a hard RLIMIT_NOFILE. The soft limit is what the
process gets and what produces EMFILE; the hard limit is the ceiling it may
raise itself to without privileges. A process inherits both from its parent.
For a container the parent chain is systemd, then dockerd, then the container
runtime, then your application’s PID 1. Anything you set in
/etc/security/limits.conf applies to login sessions and never reaches that
chain, which is why editing it appears to do nothing.
Above all of that sits fs.file-max, the kernel-wide ceiling. On a modern
kernel it is effectively unbounded and is almost never the constraint.
There is also a reason not to set the limit absurdly high. Some programs
allocate arrays sized by RLIMIT_NOFILE at startup, and older code loops from
0 to the limit closing descriptors before exec. With a soft limit of
1,048,576 that turns into measurable startup delay and memory use. A value in
the tens of thousands is enough for almost every workload and avoids the
pathology.
Raise the limit for one service
The narrow fix, applied per service in Compose:
services:
app:
image: ghcr.io/example/app:1.4.2
ulimits:
nofile:
soft: 65535
hard: 65535docker compose up -d app
docker compose exec app sh -c 'ulimit -n'65535The equivalent for a one-off container is docker run --ulimit nofile=65535:65535.
A limit change only applies to a newly created process. Restarting the
container is required; docker compose restart is enough, but recreating with
up -d is what picks up the file change.
Raise the default for every container
If several services need it, set the daemon default instead of repeating yourself:
{
"default-ulimits": {
"nofile": { "Name": "nofile", "Soft": 65535, "Hard": 65535 }
}
}sudo systemctl restart dockerThat restarts containers unless you have "live-restore": true set. Plan for
it.
If the Docker daemon itself is the process running out — the
failed to start containerd variant — the limit belongs to its systemd unit:
sudo systemctl edit docker.service[Service]
LimitNOFILE=1048576sudo systemctl daemon-reload
sudo systemctl restart dockerRecent Docker packages already ship LimitNOFILE=infinity, so check before
adding an override that lowers it:
systemctl show docker.service -p LimitNOFILECheck the kernel-wide ceiling
Rarely the constraint, but worth ruling out in one command:
cat /proc/sys/fs/file-nr9856 0 9223372036854775807Allocated, free, maximum. On a modern kernel the maximum is effectively
unbounded, so if the first number is nowhere near the third, fs.file-max is
not your problem.
When it is a leak, not a limit
Raising the limit is the right fix when the workload legitimately needs more descriptors — a proxy holding thousands of keep-alive connections, for example. It is the wrong fix when the count only ever goes up.
Sample it:
for i in $(seq 1 10); do
printf '%s %s\n' "$(date +%T)" "$(sudo ls /proc/1234/fd | wc -l)"
sleep 60
doneA count that climbs monotonically under steady traffic is a leak. Find out what kind:
sudo ls -l /proc/1234/fd | awk '{print $NF}' | sed 's/[0-9]*$//' | sort | uniq -c | sort -rn | head 4812 socket:[
37 /app/logs/app.log
9 anon_inode:[eventpoll]
6 pipe:[Thousands of sockets points at HTTP clients created per request instead of reused, or connections never closed on error paths. Thousands of the same file path points at a handle opened in a loop. Either way the fix is in the application, and a higher limit only moves the failure later.
Two more things narrow it down. ss -tanp | grep <pid> shows the state of those
sockets — a pile in CLOSE_WAIT means your process is not calling close after
the peer hung up, which is a code bug rather than a tuning problem. And
comparing the count before and after a period of no traffic tells you whether
the descriptors are ever released at all.
Raise the limit anyway as a stopgap so you are not firefighting, but open the bug. A leak with a higher ceiling fails at 3 a.m. next month instead of this afternoon, and the diagnosis is harder when you have forgotten the context.
The inotify variant
Different resource, similar-looking error:
Error: ENOSPC: System limit for number of file watchers reached
inotify_add_watch: No space left on deviceWatchers are counted per user across the whole host, and dev servers, log shippers and file-sync tools consume them quickly. Check and raise:
cat /proc/sys/fs/inotify/max_user_watches
cat /proc/sys/fs/inotify/max_user_instancesfs.inotify.max_user_watches = 524288
fs.inotify.max_user_instances = 512sudo sysctl --systemPersist it in /etc/sysctl.d/. A value set with sysctl -w is gone after
reboot, and this failure is annoying enough to debug twice.
Verify
After any change, confirm the running process — not the config file — has the new limit:
docker inspect --format '{{.State.Pid}}' my-app | xargs -I{} grep 'open files' /proc/{}/limitsThen leave a check in monitoring. Descriptor usage as a percentage of the soft limit is a leading indicator: it climbs for hours before anything breaks, which is enough warning to act during working hours rather than at 3 a.m.