Files
downloadwf/HOSTING.md
T
2026-08-25 12:57:54 +02:00

261 lines
7.8 KiB
Markdown

# Hosting Guide
Step-by-step guide to run **File Drop** on a VPS behind a reverse proxy with
TLS. The app itself does not terminate TLS — nginx or Caddy does.
Assumes a fresh Ubuntu/Debian VPS with a domain pointing at it. Adjust paths and
commands for your distro.
---
## 1. Prerequisites
- A VPS (any distro Bun supports).
- A domain (or subdomain) you control, e.g. `files.example.com`.
- DNS A record: `files.example.com` → your server's public IP.
## 2. Install Bun
```bash
curl -fsSL https://bun.sh/install | bash
export PATH="$HOME/.bun/bin:$PATH" # add to ~/.bashrc
bun --version
```
## 3. Create a dedicated user and directory
Running as a non-root user is strongly recommended.
```bash
sudo useradd --system --shell /usr/sbin/nologin --create-home filehost
sudo mkdir -p /opt/filehost
sudo chown filehost:filehost /opt/filehost
```
## 4. Put the code in place
```bash
# from your local machine (the project root):
rsync -avz --exclude node_modules --exclude storage --exclude .env \
./ filehost@your-server:/opt/filehost/
```
On the server:
```bash
cd /opt/filehost
sudo -u filehost bun install # dev deps (typecheck only), no runtime deps
sudo -u filehost mkdir -p storage
sudo chown -R filehost:filehost /opt/filehost
```
## 5. Configure
```bash
sudo -u filehost cp .env.example /opt/filehost/.env
sudo -u filehost nano /opt/filehost/.env
```
Relevant variables:
```
HOST=127.0.0.1 # bind loopback only; the proxy talks to it
PORT=3000
STORAGE_DIR=/opt/filehost/storage
MAX_FILE_SIZE=10GB # must be <= proxy upload limit
FILE_TTL_MS=172800000 # expire files after 48 hours (hard maximum)
MAX_STORAGE_USAGE=0.5 # block uploads when >50% of the disk is used
KEY_LENGTH=8 # length of the upload/download key token
RATE_LIMIT_ENABLED=1
TRUST_PROXY=1 # proxy sets X-Forwarded-For
```
Binding `HOST=127.0.0.1` (instead of `0.0.0.0`) keeps the app private on the
loopback interface so only the reverse proxy can reach it.
## 6. Install the systemd service
```bash
sudo cp /opt/filehost/deploy/filehost.service /etc/systemd/system/filehost.service
sudo systemctl daemon-reload
sudo systemctl enable --now filehost
sudo systemctl status filehost # expect "active (running)"
journalctl -u filehost -f # follow logs
```
The unit runs as `filehost`, reads `/opt/filehost/.env`, and is hardened with
`ProtectSystem=full`, `ProtectHome=true`, `NoNewPrivileges=true`, and read-write
access only to the storage directory.
## 7. Firewall
Open only the ports the proxy needs:
```bash
sudo ufw allow 80,443/tcp
sudo ufw allow OpenSSH
sudo ufw enable
```
Do not open port 3000 to the public (it's bound to loopback).
## 8. Reverse proxy
### nginx (with TLS via certbot)
```bash
sudo apt install nginx certbot python3-certbot-nginx
```
`/etc/nginx/sites-available/files.example.com`:
```nginx
server {
listen 80;
server_name files.example.com;
location /.well-known/acme-challenge/ {
root /var/www/html;
}
location / {
client_max_body_size 11g; # >= MAX_FILE_SIZE
proxy_request_buffering off; # stream uploads (critical for large files)
proxy_http_version 1.1;
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
}
}
```
```bash
sudo ln -s /etc/nginx/sites-available/files.example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d files.example.com # obtains + wires TLS, auto-renews
```
> `proxy_request_buffering off` matters: with it on, nginx buffers the whole
> upload to a temp file before forwarding, defeating streaming and doubling disk
> usage for large files.
### Caddy (auto TLS, minimal config)
```bash
sudo apt install caddy
```
`/etc/caddy/Caddyfile`:
```
files.example.com {
reverse_proxy 127.0.0.1:3000
request_body {
max_size 6GB
}
}
```
```bash
sudo systemctl reload caddy
```
Caddy obtains and renews TLS automatically.
### Cloudflare
If you proxy through Cloudflare:
- Set the DNS record to **Proxied**.
- Uploads/downloads are subject to Cloudflare's size caps (free plan ~100MB).
For larger files, add a **Cache rule** or **Origin rule** that bypasses
Cloudflare for `/api/upload` and `/*` (or at least the share links) so traffic
hits your origin directly.
- Enable `TRUST_PROXY=1`; the app reads `CF-Connecting-IP` as the client IP.
## 9. Verification
```bash
curl https://files.example.com/api/health
./scripts/smoke-test.sh # BASE_URL=https://files.example.com
```
Then in a browser:
1. Upload a file.
2. Copy the private link; open it in a new private window → downloads.
3. Open the link with a modified character → 404.
4. Restart the service, confirm the link still works:
`sudo systemctl restart filehost`.
## 10. Backups
Stop-free snapshot of the storage directory (files + metadata):
```bash
tar -czf /var/backups/filehost-$(date +%F).tar.gz -C /opt/filehost/storage .
```
Nightly cron (`crontab -e`):
```cron
0 2 * * * tar -czf /var/backups/filehost-$(date +\%F).tar.gz -C /opt/filehost/storage .
```
Restore:
```bash
sudo systemctl stop filehost
sudo rm -rf /opt/filehost/storage
sudo tar -xzf /var/backups/filehost-<date>.tar.gz -C /opt/filehost/storage
sudo chown -R filehost:filehost /opt/filehost/storage
sudo systemctl start filehost
```
The startup sweep repairs any half-written records automatically.
## 11. Updating
```bash
# pull/push the new code to /opt/filehost, then:
sudo -u filehost bun install
sudo systemctl restart filehost
```
Files in `storage/` are untouched by updates and keep working.
## 12. Troubleshooting
| Symptom | Likely cause / fix |
| --- | --- |
| `413 Request Entity Too Large` | `MAX_FILE_SIZE` bigger than the proxy's `client_max_body_size` / `request_body.max_size`. Raise the proxy limit. |
| `502 Bad Gateway` on large uploads | Proxy buffering on. Set `proxy_request_buffering off` (nginx). |
| Slow uploads / high memory | Proxy buffering uploads; disable it so the app streams to disk. |
| Download link works locally but not through the proxy | Wrong `Host`/`proxy_pass`; check the proxy config and that `HOST` in `.env` is `127.0.0.1`. |
| Rate-limited (`429`) through a proxy | Real IP not seen; enable `TRUST_PROXY=1`. |
| Disk fills up | Uploads are permanent by design. Monitor disk; see Backups and the delete API. |
| Keys appear in logs | They shouldn't. Only ids/sizes/names are logged; if you see one, you added logging. |
| After restart files 404 | Metadata or file missing/renamed; check `storage/metadata` and the startup sweep log line. |
## Security recap
- The app binds to loopback; only the proxy is exposed.
- Download/deletion require the unguessable key; everything else is `404`.
- Files uploaded with a password store only its scrypt hash; entry mints a
short-lived HMAC-signed cookie (server secret in `storage/.secret`).
Password-less files stream immediately to anyone with the key.
- Rate limiting protects the keyed endpoints; `TRUST_PROXY=1` must only be set
when a proxy really sets `X-Forwarded-For`/`CF-Connecting-IP`.
- Files auto-expire after 48 hours (configurable via `FILE_TTL_MS`, hard-capped
at 48h) and are swept hourly. Deletion also requires possession of the key
(`DELETE /api/files/:id` with `X-Delete-Key`).
- Uploads are rejected with `503 try again later` when the storage filesystem is
more than 50% full (configurable via `MAX_STORAGE_USAGE`).
- Embedded metadata (EXIF/GPS/author/comments, video `moov`/`udta`, EBML
`Tags`, AVI `INFO`) is stripped server-side by default for recognized image
and video formats. Unparseable or unrecognized files are stored untouched
(never corrupted). Disable with `STRIP_METADATA=0`.