7.5 KiB
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
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.
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
# from your local machine (the project root):
rsync -avz --exclude node_modules --exclude storage --exclude .env \
./ filehost@your-server:/opt/filehost/
On the server:
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
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
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:
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)
sudo apt install nginx certbot python3-certbot-nginx
/etc/nginx/sites-available/files.example.com:
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;
}
}
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 offmatters: 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)
sudo apt install caddy
/etc/caddy/Caddyfile:
files.example.com {
reverse_proxy 127.0.0.1:3000
request_body {
max_size 6GB
}
}
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/uploadand/*(or at least the share links) so traffic hits your origin directly. - Enable
TRUST_PROXY=1; the app readsCF-Connecting-IPas the client IP.
9. Verification
curl https://files.example.com/api/health
./scripts/smoke-test.sh # BASE_URL=https://files.example.com
Then in a browser:
- Upload a file.
- Copy the private link; open it in a new private window → downloads.
- Open the link with a modified character → 404.
- Restart the service, confirm the link still works:
sudo systemctl restart filehost.
10. Backups
Stop-free snapshot of the storage directory (files + metadata):
tar -czf /var/backups/filehost-$(date +%F).tar.gz -C /opt/filehost/storage .
Nightly cron (crontab -e):
0 2 * * * tar -czf /var/backups/filehost-$(date +\%F).tar.gz -C /opt/filehost/storage .
Restore:
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
# 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=1must only be set when a proxy really setsX-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/:idwithX-Delete-Key). - Uploads are rejected with
503 try again laterwhen the storage filesystem is more than 50% full (configurable viaMAX_STORAGE_USAGE).