# 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-.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`.