Fix Nginx 'open() failed (13: Permission denied)' When Uploading Large Files

intermediateโšก Nginx2026-04-22| Nginx 1.18+ on Ubuntu 20.04/22.04, Debian, CentOS/RHEL โ€” typically triggered after upgrading Nginx, migrating servers, or changing the user Nginx runs as

Error Message

open() "/var/lib/nginx/tmp/client_body/0000000001" failed (13: Permission denied) while reading client request body
#nginx#permission#upload#client_body_temp

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-data to nginx (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 user directive in nginx.conf without 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

Related Error Notes