The Error
Your file upload endpoint is throwing 500s. Pull up /var/log/nginx/error.log and you'll find something like this:
open() "/var/lib/nginx/tmp/client_body/0000000001" failed (13: Permission denied) while reading client request body, client: 203.0.113.45, server: example.com, request: "POST /upload HTTP/1.1"
What's weird: small uploads still work fine. It's only when the file is large enough โ usually anything past a few kilobytes โ that the 500 kicks in. That's the clue.
Why This Happens
Nginx doesn't stream large POST bodies directly to your app. It buffers them first โ writing to a temp file under client_body_temp_path (default: /var/lib/nginx/tmp/client_body/) before handing anything upstream. If the worker process doesn't own that directory, the write fails and you get permission denied.
It almost always traces back to one of four situations:
- You switched package sources โ e.g., moved from the distro's Nginx repo to the official Nginx repo. The default run user changed from
www-datatonginx(or the reverse), but the temp directory ownership didn't follow - The
/var/lib/nginx/directory was recreated or restored from backup with the wrong owner - You manually edited the
userdirective innginx.confwithout updating directory ownership - A server migration where the tmp directory was never initialized
Step 1 โ Confirm the Root Cause
Start by finding what user the Nginx workers actually run as:
ps aux | grep nginx | grep worker
Output will look like one of these:
www-data 1234 0.0 0.1 nginx: worker process
# or
nginx 1234 0.0 0.1 nginx: worker process
Now check who owns the temp directory:
ls -la /var/lib/nginx/tmp/
A mismatch between the two is your culprit. For example:
drwx------ 2 nginx nginx 40 Apr 21 02:14 client_body
# Worker is www-data โ that's the mismatch
Step 2 โ Fix the Permissions
Change ownership to match whatever your worker runs as. On Ubuntu/Debian that's usually www-data; on CentOS/RHEL it's typically nginx:
# Ubuntu/Debian
chown -R www-data:www-data /var/lib/nginx/
# CentOS/RHEL
chown -R nginx:nginx /var/lib/nginx/
If the tmp subdirectory is missing entirely, create it first:
mkdir -p /var/lib/nginx/tmp/client_body
chown -R www-data:www-data /var/lib/nginx/tmp/
chmod 700 /var/lib/nginx/tmp/client_body
Step 3 โ Check the User Directive in nginx.conf
A permission fix alone won't stick if the config file still points to the wrong user:
grep -E '^user' /etc/nginx/nginx.conf
That line must match reality. On Ubuntu, if it says user nginx; but Nginx was installed as www-data, fix it:
# /etc/nginx/nginx.conf
user www-data; # must match the actual system user
While you're there, explicitly setting the temp path is worth doing โ it makes the config self-documenting and easier to debug later:
http {
client_body_temp_path /var/lib/nginx/tmp/client_body 1 2;
client_max_body_size 100m;
# ... rest of config
}
Step 4 โ Reload Nginx
# Test config first
nginx -t
# Clean reload โ zero downtime
systemctl reload nginx
Step 5 โ Verify the Fix
Keep one terminal watching the error log:
tail -f /var/log/nginx/error.log
In another terminal, push a real-sized upload through:
# Generate a 50MB test file, then upload it
dd if=/dev/urandom of=/tmp/testfile bs=1M count=50
curl -X POST https://example.com/upload \
-F "file=@/tmp/testfile" \
-w "\nHTTP Status: %{http_code}\n"
A 200 response (or whatever your app returns on success) and silence in the error log means you're done.
Alternate Fix โ Redirect the Temp Path
Can't change ownership easily? Common in containers. Just point Nginx somewhere it can write:
http {
client_body_temp_path /tmp/nginx_client_body 1 2;
# ...
}
Then set it up:
mkdir -p /tmp/nginx_client_body
chown www-data:www-data /tmp/nginx_client_body
nginx -t && systemctl reload nginx
One caveat: /tmp gets wiped on reboot. For production, pick a path that survives restarts.
Tips
After Nginx Package Upgrades
Switching between the distro repo and the official Nginx repo silently changes the default run user. Make this a habit after any Nginx upgrade:
NGINX_USER=$(ps aux | grep 'nginx: worker' | grep -v grep | awk '{print $1}' | head -1)
echo "Nginx worker user: $NGINX_USER"
chown -R $NGINX_USER:$NGINX_USER /var/lib/nginx/
In Docker Containers
Custom Docker images often skip creating /var/lib/nginx/tmp/ entirely. Add these two lines to your Dockerfile and avoid the whole problem:
RUN mkdir -p /var/lib/nginx/tmp/client_body \
&& chown -R nginx:nginx /var/lib/nginx/
Not Sure What chmod Value to Use?
The Unix Permissions Calculator on ToolCraft lets you decode octal values visually โ paste in 700 and it shows exactly what gets granted to owner, group, and others. Runs entirely in the browser.
Lock It Down in Ansible/Terraform
If you provision servers automatically, make the ownership fix idempotent so it runs on every deploy:
# Ansible task
- name: Ensure nginx tmp directory has correct ownership
file:
path: /var/lib/nginx/tmp/client_body
state: directory
owner: "{{ nginx_user }}"
group: "{{ nginx_user }}"
mode: '0700'
recurse: yes

