TorrenClou

Architecture

What runs inside the container, how a torrent becomes a file in your cloud storage, and how the pieces are built.

Why one container

TorrenClou is really nine processes and two datastores. Shipping them as one image is a deliberate trade: it is not how you would run this at scale, but it means a self-hoster runs one command instead of orchestrating a compose file, and there is no version skew between the parts.

If you would rather split it up, the pieces are ordinary .NET and Next.js applications and the repositories build independently.

What runs inside

Supervisord manages nine programs:

ProgramPortWhat it does
postgres5432PostgreSQL 15. Bound to 127.0.0.1, never published
redis6379Cache, job state, cancellation signals. Bound to localhost
api47200ASP.NET Core API, and the Hangfire dashboard at /hangfire
frontend47100Next.js, standalone output
torrent-workerDownloads torrents via MonoTorrent
gdrive-workerUploads to Google Drive
s3-workerUploads to S3-compatible storage
prometheus47600Scrapes metrics
grafana47500Dashboards

Only 47100, 47200, 47500 and 47600 are exposed. PostgreSQL and Redis listen on loopback inside the container and are not reachable from the host.

To see the live state of all nine:

docker exec torrencloud supervisorctl status

Startup

entrypoint.sh does the following before supervisord takes over:

  1. Generates secrets into /data/postgres/secrets.env if that file does not exist — the database password, the JWT signing key, the session secret and the Grafana password. Anything already in the environment wins.
  2. Initializes PostgreSQL if the data directory is empty, then syncs the user's password on every boot.
  3. Reads the settings the app owns — worker count and the observability toggles — from the SystemSettings table, so a change made in the Settings tab takes effect on restart.
  4. Exports the runtime configuration, translating friendly variable names into the Section__Key form .NET expects.

The API and the three workers each start behind wait-for-db.sh, which polls pg_isready before handing over.

The secrets file lives inside the PostgreSQL data directory rather than at /data, because /data itself is the container's writable layer — only its subdirectories are mounted. A file there is destroyed by docker rm, which every upgrade performs, and that would silently rotate the signing keys and log everyone out.

How a torrent becomes a file in your storage

  1. You upload a .torrent. The API parses it and scrapes trackers for health data.
  2. You pick files and a destination. The API creates a job and enqueues it on the torrents Hangfire queue.
  3. torrent-worker picks it up and downloads to /data/downloads/<jobId>, holding its worker for the whole transfer — which is why the worker count is a hard ceiling on concurrency.
  4. On completion the job moves to the googledrive or s3 queue.
  5. The upload worker streams each file to your storage, resuming from Redis state if it is retried.
  6. When every file has uploaded, the local directory is deleted — unless you turned that off.

If a destination fails partway through and rerouting is on, the job moves to another healthy drive rather than failing, up to the reroute cap.

Configuration model

Two tables, both read through the app:

  • SystemSettings — one row, instance-wide. Transfer concurrency, upload failover tuning, observability toggles, and the timestamp that records setup as complete.
  • UserSettings — per account. Currently just delete-after-upload.

Routing values are cached in-process for 30 seconds and refreshed by a background service, so a change is live across the API and both upload workers without a restart. The three values read only at process start — worker count and the two observability toggles — are labelled as needing a restart in the UI, and the entrypoint reads them back on boot.

Environment variables seed the row the first time it is created, then stop mattering. That is what lets an install originally configured through the environment keep its tuning across an upgrade. The full precedence order is on the configuration page.

Data

VolumeContents
torrencloud-pgdataPostgreSQL data and /data/postgres/secrets.env
torrencloud-redisRedis persistence
torrencloud-downloadsIn-flight torrent downloads

torrencloud-pgdata is the one that matters. See Updating for backups.

Stack

Backend — .NET 9, EF Core with Npgsql, Hangfire for background jobs, MonoTorrent for the BitTorrent client, Serilog, OpenTelemetry.

Frontend — Next.js App Router, TypeScript strict mode, Tailwind, TanStack Query, Zustand, Zod, NextAuth.

The backend follows a Clean Architecture split: Core holds entities and interfaces, Application holds services, Infrastructure holds implementations and data access, and API plus the three worker projects are the hosts.

The frontend exposes no NEXT_PUBLIC_* variables. The browser calls a relative /proxy/api path and the server resolves it to the API per request, so the backend address is never baked into client bundles and one image works against any backend.

Build and release

The deploy repository holds the Dockerfile; the application code lives in the backend and frontend repositories. Merging to main in either fires a repository_dispatch at the deploy repo, which checks out both, builds the combined image, and pushes it to ghcr.io/torrenclou/torrentclou as latest and as YYYY.MM.DD-<backend-sha>-<frontend-sha>.

Built for linux/amd64 and linux/arm64.

Repositories

RepositoryContents
backend.NET 9 API and the three workers
frontendNext.js web app
deployDockerfile, installer, CI
websiteThis documentation

On this page