Files
downloadwf/HOSTING.md
T
2026-08-16 16:04:26 +02:00

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 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)

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/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

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):

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=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).