Sửa lỗi Nginx 'open() failed (13: Permission denied)' Khi Upload File Lớn

intermediate Nginx2026-04-22| Nginx 1.18+ trên Ubuntu 20.04/22.04, Debian, CentOS/RHEL — thường xảy ra sau khi nâng cấp Nginx, di chuyển server, hoặc thay đổi user mà Nginx chạy dưới

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

Lỗi gặp phải

Endpoint upload file của bạn đang trả về lỗi 500. Mở /var/log/nginx/error.log ra và bạn sẽ thấy nội dung tương tự như sau:

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"

Điều kỳ lạ là: các file nhỏ vẫn upload bình thường. Chỉ khi file đủ lớn — thường là vài kilobyte trở lên — thì lỗi 500 mới xuất hiện. Đây chính là manh mối quan trọng.

Nguyên nhân

Nginx không stream trực tiếp các POST body lớn lên ứng dụng của bạn. Thay vào đó, nó buffer chúng trước — ghi vào file tạm trong thư mục client_body_temp_path (mặc định: /var/lib/nginx/tmp/client_body/) trước khi chuyển lên upstream. Nếu tiến trình worker không có quyền sở hữu thư mục đó, việc ghi sẽ thất bại và bạn nhận được lỗi permission denied.

Nguyên nhân hầu như luôn xuất phát từ một trong bốn tình huống sau:

  • Bạn đổi nguồn package — ví dụ, chuyển từ repo Nginx của distro sang repo Nginx chính thức. User chạy mặc định đã thay đổi từ www-data sang nginx (hoặc ngược lại), nhưng quyền sở hữu thư mục tạm chưa được cập nhật theo
  • Thư mục /var/lib/nginx/ bị tạo lại hoặc restore từ backup với owner sai
  • Bạn chỉnh thủ công directive user trong nginx.conf mà không cập nhật quyền sở hữu thư mục
  • Khi migrate server, thư mục tmp chưa được khởi tạo

Bước 1 — Xác định nguyên nhân gốc rễ

Đầu tiên, kiểm tra xem các worker của Nginx đang chạy với user nào:

ps aux | grep nginx | grep worker

Kết quả sẽ trông giống một trong hai dạng sau:

www-data  1234  0.0  0.1  nginx: worker process
# hoặc
nginx     1234  0.0  0.1  nginx: worker process

Tiếp theo, kiểm tra chủ sở hữu của thư mục tạm:

ls -la /var/lib/nginx/tmp/

Nếu hai thông tin này không khớp, đó chính là thủ phạm. Ví dụ:

drwx------ 2 nginx nginx 40 Apr 21 02:14 client_body
# Worker đang chạy bằng www-data — đây là sự không khớp

Bước 2 — Sửa phân quyền

Thay đổi quyền sở hữu để khớp với user mà worker đang chạy. Trên Ubuntu/Debian thường là www-data; trên CentOS/RHEL thường là nginx:

# Ubuntu/Debian
chown -R www-data:www-data /var/lib/nginx/

# CentOS/RHEL
chown -R nginx:nginx /var/lib/nginx/

Nếu thư mục tmp chưa tồn tại, hãy tạo trước:

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

Bước 3 — Kiểm tra directive user trong nginx.conf

Chỉ sửa phân quyền thôi chưa đủ nếu file config vẫn trỏ đến user sai:

grep -E '^user' /etc/nginx/nginx.conf

Dòng đó phải khớp với thực tế. Trên Ubuntu, nếu ghi user nginx; nhưng Nginx lại được cài để chạy bằng www-data, hãy sửa lại:

# /etc/nginx/nginx.conf
user www-data;  # phải khớp với user thực tế trên hệ thống

Nhân tiện, nên thiết lập rõ ràng đường dẫn temp — giúp config tự mô tả và dễ debug về sau:

http {
    client_body_temp_path /var/lib/nginx/tmp/client_body 1 2;
    client_max_body_size 100m;
    # ... phần còn lại của config
}

Bước 4 — Reload Nginx

# Kiểm tra config trước
nginx -t

# Reload không gián đoạn — zero downtime
systemctl reload nginx

Bước 5 — Xác nhận đã sửa xong

Mở một terminal theo dõi error log:

tail -f /var/log/nginx/error.log

Ở terminal khác, thử upload một file có kích thước thực:

# Tạo file test 50MB rồi upload
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"

Nếu nhận được response 200 (hoặc mã thành công mà app bạn trả về) và error log không có gì mới, tức là bạn đã xử lý xong.

Cách khắc phục thay thế — Chuyển hướng đường dẫn temp

Không thể thay đổi quyền sở hữu dễ dàng? Tình huống phổ biến trong container. Chỉ cần trỏ Nginx đến nơi nó có thể ghi được:

http {
    client_body_temp_path /tmp/nginx_client_body 1 2;
    # ...
}

Sau đó thiết lập thư mục:

mkdir -p /tmp/nginx_client_body
chown www-data:www-data /tmp/nginx_client_body
nginx -t && systemctl reload nginx

Lưu ý một điểm: /tmp sẽ bị xóa sau mỗi lần reboot. Với môi trường production, hãy chọn đường dẫn tồn tại qua các lần khởi động lại.

Mẹo thêm

Sau khi nâng cấp package Nginx

Chuyển đổi giữa repo của distro và repo Nginx chính thức sẽ âm thầm thay đổi user chạy mặc định. Hãy biến thao tác này thành thói quen sau mỗi lần nâng cấp Nginx:

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/

Trong Docker Container

Các Docker image tùy chỉnh thường bỏ qua việc tạo thư mục /var/lib/nginx/tmp/. Thêm hai dòng này vào Dockerfile để tránh hoàn toàn vấn đề:

RUN mkdir -p /var/lib/nginx/tmp/client_body \
    && chown -R nginx:nginx /var/lib/nginx/

Không chắc dùng giá trị chmod nào?

Unix Permissions Calculator trên ToolCraft cho phép bạn giải mã các giá trị octal một cách trực quan — dán 700 vào và nó hiển thị chính xác quyền được cấp cho owner, group và others. Chạy hoàn toàn trên trình duyệt.

Cố định bằng Ansible/Terraform

Nếu bạn provision server tự động, hãy làm cho thao tác sửa quyền sở hữu trở thành idempotent để nó chạy mỗi lần 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