Skip to main content

Cloud & VPS

Overview

A cloud VPS is a Linux host you rent. The install is identical. What changes is that it is on the public internet, which turns three optional things into mandatory ones:

  1. A firewall. Expose 80/443 only — never 8080, never 4000, never 5000.
  2. HTTPS. No exceptions.
  3. Backups you keep somewhere else. A cloud instance can vanish.

And two things you must understand before you spend money:

  • Bandwidth is metered almost everywhere, and BitTorrent is bandwidth. This is where surprise bills come from.
  • Your provider's acceptable-use policy applies to you. Read it.
Read your provider's AUP first

Some providers prohibit BitTorrent traffic outright, some tolerate it, some will terminate your account on the first DMCA notice. This is a policy question, not a technical one, and it is entirely on you. UltraTorrent is a media acquisition platform — use it for content you have the right to.

Community-verified

Cloud providers are not among this project's own deployment targets. The UltraTorrent parts below are grounded in the repo; the provider specifics follow each vendor's standard practice and their consoles change constantly. Verify against current provider docs.

Watch this tutorial

Video coming soon.

Prerequisites

  • A cloud account and a domain name (you need one for HTTPS).
  • SSH key-based access to the instance.
  • A credit card you are watching.

Requirements

ResourceMinimumComfortableWhy
vCPU22–4The build; steady state is light
RAM4 GB4–8 GBThe build peaks around 2 GB free — a 1 GB droplet will fail to build
Boot disk20 GB40 GBOS + images
Data diskAttach block storageas big as your libraryBoot disks are small and expensive
Bandwidthcheck the capunmetered or generousThis is the real cost

Roughly, per provider:

ProviderSane starting instanceBandwidth notes
HetznerCX22 / CPX21 (2 vCPU, 4 GB)Generous included traffic — usually the cheapest sane option
DigitalOceanBasic 2 vCPU / 4 GBTransfer allowance pooled per account; overage billed
Vultr2 vCPU / 4 GB RegularSimilar model to DO
Oracle CloudAmpere A1 (ARM) — 4 OCPU / 24 GB free tierBig free tier, but capacity is often unavailable, and egress beyond the free allowance bills
AWS EC2t3.medium / t4g.mediumEgress is expensive. This is the classic surprise-bill platform
GCPe2-mediumEgress billed per GB
AzureB2sEgress billed per GB
Linode/Akamai2 GB shared → 4 GB2 GB is too small to build
Egress on the hyperscalers

On AWS/GCP/Azure, outbound data transfer is billed per gigabyte. A seeding torrent client is a machine for generating outbound data transfer. People have run up four-figure bills this way. If you use a hyperscaler, cap your upload rate, set a billing alarm, and know your egress price before you start.

ARM64 instances (Graviton, Ampere, Axion) work — the base images are multi-arch.

Ports

PortOpen to the internet?
22 (SSH)Key-only, and ideally restricted to your IP
80Yes — HTTP→HTTPS redirect and ACME
443Yes — the UI
8080No. Bind it to 127.0.0.1 and let the proxy reach it
4000 (backend)Never. Not published by default; keep it that way
5000 (SCGI)Never. Unauthenticated full remote control
Peer portsOptional, and only if you understand the exposure

Volumes

Use block storage for downloads, not the boot disk:

# after attaching the volume in the provider console
lsblk # find the device, e.g. /dev/sdb
sudo mkfs.ext4 /dev/sdb
sudo mkdir -p /mnt/downloads
echo '/dev/sdb /mnt/downloads ext4 defaults,nofail,discard 0 2' | sudo tee -a /etc/fstab
sudo mount -a
sudo chown -R 1000:1000 /mnt/downloads
# docker-compose.override.yml
volumes:
downloads:
driver: local
driver_opts:
type: none
o: bind
device: /mnt/downloads
nofail matters in the cloud

Without it, an instance that boots before the volume attaches drops to an emergency shell and never comes back — and you get to debug it over a serial console.

Permissions

Standard Linux — the downloads folder must be writable by uid 1000 (or your PUID/PGID). See Permissions.

The more important permission question on a VPS is who can log in: keys only, no password auth, no root login.

Step-by-step

1. Create the instance

  • Image: Ubuntu 24.04
  • Type: CX22 (2 vCPU, 4 GB) or larger
  • Volume: attach block storage for downloads
  • Firewall: create one allowing 22 (your IP), 80, 443 — nothing else
  • SSH key: add yours

Hetzner's included traffic allowance is generous, which is why it is the usual answer for this workload.

2. Harden the box — before you install anything

sudo apt update && sudo apt upgrade -y

# SSH: keys only
sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sudo systemctl restart ssh

# Automatic security updates
sudo apt install -y unattended-upgrades fail2ban

Host firewall, in addition to the cloud one:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Docker punches through ufw

Docker writes its own iptables rules and a published port bypasses ufw. ufw deny 8080 does not protect a container published on 0.0.0.0:8080. The only reliable fix is to not publish it publicly — bind it to localhost (next step) and use the cloud firewall as the outer layer.

3. Install Docker + UltraTorrent

curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER" # log out and back in

git clone https://github.com/damirabal/ultratorrent-core.git
cd ultratorrent-core
cp .env.example .env
for k in JWT_ACCESS_SECRET JWT_REFRESH_SECRET ENCRYPTION_KEY; do
sed -i "s|^$k=.*|$k=$(openssl rand -base64 48 | tr -d '\n')|" .env
done
nano .env
POSTGRES_PASSWORD=lettersAndNumbers123
ADMIN_PASSWORD=a-genuinely-strong-password # this one faces the internet
FRONTEND_PORT=8080
CORS_ORIGIN=https://torrents.example.com # your real public origin

4. Bind the UI to localhost

This is the step that keeps the raw HTTP port off the internet.

# docker-compose.override.yml
services:
frontend:
ports: !override
- "127.0.0.1:8080:8080"

volumes:
downloads:
driver: local
driver_opts:
type: none
o: bind
device: /mnt/downloads
Community-verified

!override needs a recent Compose v2. If yours rejects it, edit the ports: line in docker-compose.yml directly — a plain override appends ports rather than replacing them, which would leave 0.0.0.0:8080 published.

Verify afterwards. This command is the whole point of this section:

sudo ss -tlnp | grep 8080
# WANT: 127.0.0.1:8080
# NOT: 0.0.0.0:8080

5. Build, start, seed

docker compose --profile rtorrent up -d --build
docker compose exec backend npx prisma db seed

6. Put a proxy in front, with HTTPS

Point your domain's A record at the instance's IP, then — the shortest correct path:

sudo apt install -y caddy
# /etc/caddy/Caddyfile
torrents.example.com {
encode gzip
reverse_proxy 127.0.0.1:8080
}
sudo systemctl reload caddy

Caddy obtains the certificate, redirects HTTP→HTTPS, renews automatically, and proxies WebSockets transparently. Other proxies (NGINX, Traefik, HAProxy): Reverse proxy. Certificates in depth: TLS.

7. Log in

https://torrents.example.com → sign in as admin.

Immediately: change the admin password from the profile menu, and enable two-factor authentication. This login is on the public internet. See Security and Users.

Then add the engine: Infrastructure → Engines → Add engine → rTorrent · SCGI over TCP · host rtorrent · port 5000Test connectionAdd engine. And Settings → Default Root Path/downloads.

Verification

# Nothing dangerous is listening publicly
sudo ss -tlnp | grep -E '0\.0\.0\.0:(8080|4000|5000)'
# ^ this should print NOTHING

# The app answers over HTTPS
curl -s https://torrents.example.com/api/system/live
curl -s https://torrents.example.com/api/system/version

Confirm the WebSocket upgrades through the proxy — otherwise the UI will look fine and never update:

curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
https://torrents.example.com/ws/
HTTP/1.1 101 Switching Protocols

From another machine, prove the raw ports are shut:

curl -m 5 http://<public-ip>:8080 # must time out / refuse
curl -m 5 http://<public-ip>:4000 # must time out / refuse

The UltraTorrent dashboard served over HTTPS from a VPS

Reverse proxy

Mandatory. See Reverse proxy. The WebSocket upgrade is not optional — the live UI depends on it.

HTTPS

Mandatory. Let's Encrypt via Caddy is the shortest path; certbot + NGINX is the classic one. See TLS.

Updates

cd ultratorrent-core
docker compose exec -T postgres pg_dump -U ultratorrent ultratorrent > backup-$(date +%F).sql
# copy the dump OFF the instance:
scp backup-$(date +%F).sql you@home:/backups/

git pull
docker compose --profile rtorrent up -d --build
docker compose exec backend npx prisma db seed

Take a provider snapshot of the instance before an upgrade — it is the cloud equivalent of a Proxmox snapshot and gives you a whole-machine rollback. Full procedure: Upgrading.

Keep the OS patched: unattended-upgrades is doing that already if you installed it in step 2.

Backups

Off-instance or it does not count.

# Nightly: dump, then ship it somewhere else
docker compose exec -T postgres pg_dump -U ultratorrent ultratorrent \
| gzip > /mnt/downloads/backups/ut-$(date +%F).sql.gz

# then rclone/scp/s3 it off the box

Also back up .env — without ENCRYPTION_KEY, stored 2FA secrets are unrecoverable.

Provider snapshots are convenient but they live in the same account that a compromise or a billing failure would take out. Keep one copy elsewhere. See Backup & restore.

Troubleshooting

SymptomCauseFix
Build is OOM-killedThe instance is too small — 1–2 GB RAMResize to 4 GB, or add swap and rebuild slowly
Port opened in the cloud console, still unreachableA host firewall is also in the way (ufw, or Oracle's baked-in iptables)Open it locally too: ufw allow 443/tcp, or iptables -I INPUT ... && netfilter-persistent save
ufw deny 8080 does nothing — 8080 is still reachableDocker bypasses ufw by writing its own iptables rules for published portsDo not publish it: bind to 127.0.0.1:8080, and rely on the cloud firewall
Certificate issuance failsPort 80 is blocked, or DNS has not propagatedOpen 80 in both firewalls; check the A record actually resolves
Shocking bandwidth billSeeding, on a per-GB-egress providerCap upload rate, stop completed seeds, set a billing alarm, move to a flat-rate provider
Instance will not boot after adding a volumeAn fstab entry without nofailAdd nofail; recover via the serial/rescue console
Downloads disk full, stack degradesBoot disk used for downloads, or the volume filledDownloads belong on block storage; monitor free space
UI loads over HTTPS but never updatesThe WebSocket is not upgrading through the proxyRun the 101 Switching Protocols test above; see Reverse proxy
Brute-force attempts in the logsIt is a public login pageStrong ADMIN_PASSWORD, enable 2FA, fail2ban, and consider proxy-level auth
Account suspendedProvider AUP / DMCAA policy problem, not a technical one. Read the AUP before you deploy

More: Troubleshooting · Security.

Best practices

  • Never publish 8080, 4000 or 5000 to the internet. Bind to 127.0.0.1, front with a proxy.
  • Two firewalls: the cloud one and the host one. Remember Docker punches through ufw for published ports.
  • HTTPS from day one. A plain-HTTP login page on the public internet ships your password in cleartext.
  • Enable 2FA on the admin account immediately.
  • SSH: keys only, no root login, fail2ban.
  • Know your egress price and set a billing alarm before you seed anything.
  • Cap the upload rate on a metered provider.
  • Block storage for downloads, with nofail in fstab.
  • Ship backups off the instance.
  • Patch the OS automatically (unattended-upgrades).
  • Read the provider's AUP. It is the risk that ends the project, not a misconfigured port.
  • Prefer qBittorrent if you are running a large library on a remote box you will not babysit.

FAQ

Which provider is cheapest for this? Flat-rate/generous-traffic providers (Hetzner, and similar) beat per-GB-egress hyperscalers dramatically for a bandwidth-heavy workload. That is an arithmetic fact, not a preference.

Can I use Oracle's free tier? Yes — Ampere A1 is ARM64 and the images are multi-arch. Expect capacity headaches, and remember the baked-in iptables rules.

Is a 1 GB droplet enough? No. The build alone wants ~2 GB free. Use 4 GB.

Is this a seedbox? It can be one. Whether your provider is happy about that is a question only their AUP can answer.

Do I need a domain? For HTTPS, effectively yes. Let's Encrypt will not issue for a bare IP.

How do I stop the admin login being brute-forced? Strong password, 2FA, fail2ban, and optionally an auth layer at the proxy (Cloudflare Access, basic auth). See Security.

Can I put it behind Cloudflare? For the web UI, yes — a Tunnel even avoids opening any inbound port. See Reverse proxy. BitTorrent peer traffic does not go through Cloudflare.

Checklist

  • Provider AUP read and understood
  • Instance ≥ 2 vCPU / 4 GB RAM
  • Block storage attached for downloads, mounted with nofail
  • Cloud firewall: 22 (your IP), 80, 443 — nothing else
  • Host firewall enabled too (and Oracle's local iptables, if applicable)
  • SSH keys only; root login off; fail2ban installed
  • unattended-upgrades installed
  • .env: alphanumeric POSTGRES_PASSWORD, strong ADMIN_PASSWORD, three distinct secrets, CORS_ORIGIN = your HTTPS origin
  • Frontend bound to 127.0.0.1:8080 — verified with ss -tlnp
  • Reverse proxy + valid TLS certificate
  • /ws/ returns 101 Switching Protocols through the proxy
  • curl http://<public-ip>:8080 from outside fails (as it should)
  • Admin password changed and 2FA enabled
  • Billing alarm set; upload rate capped if egress is metered
  • Backups running and shipped off the instance

See also