256 lines
7.4 KiB
Markdown
256 lines
7.4 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
|
|
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`). |