在 Ubuntu 上使用 nginx 搭建自托管文件服务器的实用指南,适用于局域网共享和受控的公网暴露。
/srv/fileserver/
├── data/ # 所有用户上传的文件存放于此
│ ├── public/ # 公开可读(无需认证)
│ ├── shared/ # 局域网认证用户
│ ├── private/ # 仅限所有者
│ └── temp/ # 暂存区,每小时自动清理
├── logs/ # 访问日志 + 错误日志
│ ├── access.log
│ └── error.log
├── config/
│ ├── nginx.conf # nginx 站点配置片段
│ └── htpasswd # Basic 认证凭据(shared/ 区域)
├── uploads/ # PHP / Python 上传处理程序
│ └── upload.php
└── scripts/
├── cleanup.sh # 清理 /temp/ 目录下超过 1 小时的文件
└── stats.sh # 每日使用报告
# 创建目录树
sudo mkdir -p /srv/fileserver/{data/{public,shared,private,temp},logs,config,uploads,scripts}
# 所有者:Web 服务器用户(Ubuntu 中为 www-data)
sudo chown -R www-data:www-data /srv/fileserver
# 访问规则
sudo chmod 755 /srv/fileserver/data/public # 所有人可列出和读取
sudo chmod 750 /srv/fileserver/data/shared # 组可读(htpasswd 用户)
sudo chmod 700 /srv/fileserver/data/private # 仅限所有者
sudo chmod 770 /srv/fileserver/data/temp # 上传处理程序可写
# 日志 — 仅管理员可读
sudo chmod 640 /srv/fileserver/logs/*
| 层次 | 选择 | 理由 |
|---|---|---|
| Web 服务器 | nginx | 轻量、久经考验的反向代理 |
| 上传处理程序 | PHP-FPM 或 Python Flask | 轻松处理 multipart 上传 |
| 认证 | Basic 认证 + IP 白名单 | 简单,无需 session cookie |
| HTTPS | Let's Encrypt (certbot) | 免费、自动续期 |
| 监控 | GoAccess | 基于日志的实时访问仪表盘 |
/etc/nginx/sites-available/fileserver
# /etc/nginx/sites-available/fileserver
# --- 速率限制区域 ---
limit_req_zone $binary_remote_addr zone=upload:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone=download:10m rate=20r/s;
# --- 每 IP 连接数限制 ---
limit_conn_zone $binary_remote_addr zone=connperip:10m;
limit_conn connperip 10;
server {
listen 80;
server_name files.mydomain.com;
# 生产环境中重定向到 HTTPS
# return 301 https://$host$request_uri;
root /srv/fileserver/data;
index index.html index.htm;
# --- 带缓存的静态文件服务 ---
location /public/ {
alias /srv/fileserver/data/public/;
autoindex on; # 目录列表
autoindex_exact_size off; # 显示人类可读的大小
autoindex_localtime on;
# 浏览器缓存头
expires 7d;
add_header Cache-Control "public, immutable";
# 安全头
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
}
# --- 受保护的共享区域(Basic 认证) ---
location /shared/ {
alias /srv/fileserver/data/shared/;
autoindex on;
autoindex_exact_size off;
auth_basic "Restricted Access";
auth_basic_user_file /srv/fileserver/config/htpasswd;
# 仅局域网客户端可跳过认证
satisfy any;
allow 192.168.0.0/16;
allow 10.0.0.0/8;
deny all;
}
# --- 私有区域(仅限所有者) ---
location /private/ {
alias /srv/fileserver/data/private/;
autoindex on;
# 仅限 localhost 访问;使用 SSH 隧道进行远程访问
allow 127.0.0.1;
allow ::1;
deny all;
}
# --- 上传端点(代理到 Python/FastCGI) ---
location /upload {
limit_req zone=upload burst=3 nodelay;
# 限制上传大小
client_max_body_size 500M;
client_body_timeout 300s;
# 代理到内部上传服务(例如 Flask 监听 :5000)
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
}
# --- 带速率限制的下载 ---
location /download/ {
alias /srv/fileserver/data/public/;
limit_req zone=download burst=10 nodelay;
limit_rate 2M; # 每连接 2 MB/s
}
# --- 日志 ---
access_log /srv/fileserver/logs/access.log combined;
error_log /srv/fileserver/logs/error.log warn;
}
sudo ln -sf /etc/nginx/sites-available/fileserver /etc/nginx/sites-enabled/
sudo nginx -t # 测试语法
sudo systemctl reload nginx
# 在 server {} 或 location {} 块中
client_max_body_size 2G; # 全局最大值
client_body_buffer_size 128k; # 写入磁盘前的缓冲区大小
client_body_timeout 600s; # 慢速客户端的超时时间
# /srv/fileserver/uploads/upload.py
import os
from flask import Flask, request, jsonify
from werkzeug.utils import secure_filename
app = Flask(__name__)
ALLOWED_EXTENSIONS = {
# 文档
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'csv', 'md',
# 图片
'jpg', 'jpeg', 'png', 'gif', 'svg', 'bmp', 'webp', 'tiff',
# 压缩包
'zip', 'tar', 'gz', 'bz2', 'xz', '7z', 'rar',
# 代码/数据
'v', 'sv', 'vhd', 'vhdl', 'bit', 'bin', 'hex', 'json', 'xml', 'yaml',
# 媒体
'mp4', 'mkv', 'avi', 'mov', 'mp3', 'wav', 'flac',
}
BLOCKED_EXTENSIONS = {
'sh', 'bash', 'exe', 'com', 'bat', 'ps1', 'py', 'pl', 'rb',
'php', 'jsp', 'asp', 'cgi',
}
MAX_FILE_SIZE = 500 * 1024 * 1024 # 500 MB
UPLOAD_FOLDER = '/srv/fileserver/data/temp'
def allowed_file(filename: str) -> bool:
if '.' not in filename:
return False
ext = filename.rsplit('.', 1)[1].lower()
if ext in BLOCKED_EXTENSIONS:
return False
return ext in ALLOWED_EXTENSIONS
@app.route('/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
return jsonify({'error': 'No file part'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': 'No selected file'}), 400
if not allowed_file(file.filename):
return jsonify({'error': 'File type not allowed'}), 415
# 先检查 Content-Length,再读取文件内容
if request.content_length and request.content_length > MAX_FILE_SIZE:
return jsonify({'error': 'File too large'}), 413
filename = secure_filename(file.filename)
file.save(os.path.join(UPLOAD_FOLDER, filename))
return jsonify({'message': f'Uploaded {filename}'}), 201
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000)
| 区域 | 速率 | 突发 | 用途 |
|---|---|---|---|
| upload | 5 r/s | 3 | 防止上传洪水攻击 |
| download | 20 r/s | 10 | 公平带宽共享 |
| connperip | 10 连接 | - | 防止连接耗尽 |
# 添加到 /etc/nginx/nginx.conf 的 http{} 块中,用于详细审计
log_format audit '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time" '
'pipe=$pipe connection=$connection '
'request_length=$request_length '
'gzip_ratio=$gzip_ratio';
access_log /srv/fileserver/logs/access.log audit;
# 安装
sudo apt install goaccess
# 终端仪表盘
goaccess /srv/fileserver/logs/access.log --log-format=COMBINED
# 生成静态 HTML 报告(cron 每 5 分钟执行一次)
goaccess /srv/fileserver/logs/access.log \
--log-format=COMBINED \
-o /srv/fileserver/data/public/report.html \
--real-time-html
#!/bin/bash
# /srv/fileserver/scripts/audit.sh
LOG=/srv/fileserver/logs/access.log
echo "=== Top 10 IPs by requests ==="
awk '{print $1}' "$LOG" | sort | uniq -c | sort -rn | head -10
echo "=== 4xx/5xx errors today ==="
grep "$(date +%d/%b/%Y)" "$LOG" | awk '$9 ~ /^[45]/ {print $1, $7, $9}' | tail -20
echo "=== Large uploads (>100 MB) ==="
awk '$10 > 104857600 {print $1, $7, $10/1048576 "MB"}' "$LOG"
echo "=== Suspicious extensions accessed ==="
grep -iE '\.(sh|exe|php|bak|sql|swp)$' "$LOG"
# /etc/fail2ban/filter.d/nginx-fileserver.conf
[Definition]
failregex = ^<HOST> -.* "(GET|POST).*(\.sh|\.exe|\.php|\.env|wp-admin).*" 40[034]
^<HOST> -.* "(GET|POST).*/\.git/.*" 40[034]
ignoreregex =
# /etc/fail2ban/jail.local
[nginx-fileserver]
enabled = true
port = http,https
filter = nginx-fileserver
logpath = /srv/fileserver/logs/access.log
maxretry = 5
bantime = 3600
findtime = 600
server {
listen 80;
server_name files.lan;
# 仅本地子网
allow 192.168.0.0/16;
allow 10.0.0.0/8;
allow 172.16.0.0/12;
deny all;
# ... 其余配置
}
| 层级 | 工具 | 作用 |
|---|---|---|
| 1 | Cloudflare 代理(橙色云) | 隐藏源站 IP、DDoS 防护 |
| 2 | Cloudflare WAF 规则 | 按国家、ASN 或 UA 拦截 |
| 3 | nginx allow/deny |
二次 IP 过滤 |
| 4 | Basic 认证 | /shared/ 区域的密码网关 |
| 5 | fail2ban | 自动封禁扫描器 |
| 6 | ufw 防火墙 | 仅开放 80/443 端口 |
# ufw — 最小化开放端口
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
为获得最高安全性,将文件服务器保持在局域网内,通过 WireGuard 对外暴露:
# /etc/wireguard/wg0.conf(服务器端)
[Interface]
Address = 10.100.0.1/24
ListenPort = 51820
PrivateKey = <server-private-key>
[Peer] # 你的笔记本电脑
PublicKey = <peer-public-key>
AllowedIPs = 10.100.0.2/32
[Peer] # 你的手机
PublicKey = <phone-public-key>
AllowedIPs = 10.100.0.3/32
然后通过 http://10.100.0.1 访问文件服务器,无需向互联网暴露任何端口。
#!/bin/bash
# /srv/fileserver/scripts/cleanup.sh
# 删除 /temp/ 目录下超过 60 分钟的文件
find /srv/fileserver/data/temp -type f -mmin +60 -delete
# /etc/cron.d/fileserver
0 * * * * www-data /srv/fileserver/scripts/cleanup.sh
#!/bin/bash
# /srv/fileserver/scripts/stats.sh
LOG=/srv/fileserver/logs/access.log
YESTERDAY=$(date -d yesterday +%d/%b/%Y)
echo "=== File Server Daily Report: $(date -d yesterday +%Y-%m-%d) ==="
echo "Total requests: $(grep "$YESTERDAY" "$LOG" | wc -l)"
echo "Unique IPs: $(grep "$YESTERDAY" "$LOG" | awk '{print $1}' | sort -u | wc -l)"
echo "Data served: $(grep "$YESTERDAY" "$LOG" | awk '{sum+=$10} END {printf "%.1f MB\n", sum/1048576}')"
0 8 * * * root /srv/fileserver/scripts/stats.sh | mail -s "FileServer Daily" admin@localhost
| 症状 | 可能原因 | 修复方法 |
|---|---|---|
413 Request Entity Too Large |
client_max_body_size 太小 |
在 nginx 配置中增大 |
| 上传卡住 | client_body_timeout 太短 |
设为 600s |
静态文件返回 403 Forbidden |
文件权限不包含 www-data | chown www-data:www-data |
/upload 返回 502 Bad Gateway |
上传后端未运行 | systemctl start fileserver-upload |
| 目录列表为空 | autoindex off 或缺少索引文件 |
设置 autoindex on |
| nginx CPU 占用高 | 未启用 sendfile |
添加 sendfile on; tcp_nopush on; |