Deploy › Docker¶
All-in-one setup: a single container running Redis + Unit3DWebUp + unit3dprep, started
by one entrypoint. The simplest, most robust way to stand the stack up — no sudo, systemd
or Node required.
┌─────────────────── unit3dprep container ───────────────────┐
│ │
│ unit3dprep-web (0.0.0.0:8765) │
│ │ HTTP + WS │
│ └─────────> Unit3DWebUp (127.0.0.1:8000) │
│ │ │
│ ├─> Redis (127.0.0.1:6379) │
│ └─> qBittorrent (external / host) │
│ │
│ shared .env + media + seedings under /data (one volume) │
└─────────────────────────────────────────────────────────────┘
Why a single container?
Unit3DWebUp hardcodes Redis at localhost:6379 and ignores REDIS_HOST/REDIS_PORT,
so Redis must share webup's network namespace. Keeping everything together also keeps
media and seedings on the same filesystem (hardlinks work) and lets unit3dprep
reach webup over loopback (HTTP and WebSocket).
1 — Prerequisites¶
- Docker Engine + Compose v2 (
docker compose version). Compose v2 is required (install below, if missing). The legacydocker-composev1 (1.29.2, Python — EOL) is not supported: it's incompatible with Docker Engine 25+ and crashesdocker compose upwithKeyError: 'ContainerConfig'(see Troubleshooting). - A reachable qBittorrent (on your host or another container) if you want real seeding. Not needed just to try the UI.
Install Compose v2 (if docker compose version fails) — user CLI plugin, no repo or sudo:
mkdir -p ~/.docker/cli-plugins
curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 \
-o ~/.docker/cli-plugins/docker-compose
chmod +x ~/.docker/cli-plugins/docker-compose
docker compose version
Alternatively, if you've set up the official Docker APT repo: sudo apt-get install docker-compose-plugin (the package is not in the standard Debian/Ubuntu repos).
2 — Clone the repo and prepare config¶
git clone https://github.com/davidesidoti/unit3dprep.git
cd unit3dprep
cp config.env.example config.env
Compose v2 required — read before continuing
The commands use docker compose (Compose v2). Check with docker compose version; if
missing, install it as shown in §1. Do not use the legacy
docker-compose v1 (1.29.2): with Docker Engine 25+ it crashes docker compose up with
KeyError: 'ContainerConfig'.
Generate the web UI password hash (interactive):
Copy the $2b$… value into config.env under U3DP_PASSWORD_HASH (no quotes), and fill in
the rest:
U3DP_PASSWORD_HASH=$2b$12$....................................................
U3DP_SECRET=<long-random-string>
TMDB_API_KEY=<your-tmdb-key>
U3DP_HTTPS_ONLY=0
PUID=1000
PGID=1000
Generate the secret
python -c "import secrets; print(secrets.token_urlsafe(48))"
Headless setup: pre-seed your keys
TMDB_API_KEY (plus optional ITT_APIKEY/ITT_PID/TVDB_APIKEY/QBIT_* — see
config.env.example) are injected into the .env on first boot, so Unit3DWebUp
reads them right away and the *_APIKEY not set warnings go away. They're read only
while the .env doesn't exist yet: afterwards the .env is authoritative and you edit
everything from Settings in the web UI (these env vars are ignored on later boots).
PUID/PGID — no sudo to manage media
The stack runs inside the container as PUID:PGID, so files written to the ./data
volume (config, db, torrents, hardlinks) are owned by those ids. Set them to your host
user so you can add/remove media under ./data/media without sudo: run id on the
host and copy uid/gid into config.env. Defaults to 1000:1000 (the first Linux/WSL
user). Use PUID=0 to run as root (legacy behaviour).
The bcrypt hash contains $
config.env is passed to the container via env_file: → values are literal, so the
hash with its $ characters is fine as-is. Do not use ${U3DP_PASSWORD_HASH}
interpolation in docker-compose.yml (that would require doubling every $ to $$).
3 — Start it¶
The docker-compose.yml already points at the published Docker Hub image
(hashdeveloper512/unit3dprep,
tags latest/X.Y.Z): docker compose up -d pulls it automatically — no build step needed.
Want to build the image locally?
Open docker-compose.yml, comment out image: and uncomment build: ., then run
docker compose build && docker compose up -d.
In the logs you should see, in order (prefixed [entrypoint]): starting redis on 127.0.0.1:6379,
seeding /data/.env, webup is up, and finally starting unit3dprep-web on 0.0.0.0:8765 followed
by uvicorn's Application startup complete.
Open http://127.0.0.1:8765 and log in with the password from step 2.
In Settings the Unit3DWebUp card must be green (online): unit3dprep reaches it at
127.0.0.1:8000 inside the container.
4 — External qBittorrent and path mapping¶
The container does not include qBittorrent. When webup runs /seed it hands qBittorrent
the file path as seen inside the container (by default /data/seedings/…). For
qBittorrent to actually find it, that path must be valid from its point of view too.
Recommended recipe — mount the same absolute host paths:
volumes:
- ./data:/data
- /srv/media:/srv/media:ro
- /srv/seedings:/srv/seedings
environment:
U3DP_MEDIA_ROOT: /srv/media
U3DP_SEEDINGS_DIR: /srv/seedings
Now the file in /srv/seedings/… has the same path inside the container and on the host where
qBittorrent runs. In Settings → Client point QBIT_HOST/QBIT_PORT at your qBittorrent
(for the host's qBit from inside the container use host.docker.internal, or the host IP).
Hardlinks = same filesystem
Hardlinking between media and seedings only works if both directories live on the
same filesystem. With the default (everything under /data) that's guaranteed. If you
mount separate host paths, make sure media and seedings are on the same host filesystem.
Try it without touching the tracker
Add U3DP_DRY_RUN_TRACKER=1 to config.env to run scan → maketorrent → seed while
skipping the tracker upload. Handy to validate the setup end-to-end without publishing.
5 — HTTPS / reverse proxy¶
Compose publishes the port on loopback only (127.0.0.1:8765). For remote access put a
TLS reverse proxy (Caddy, Traefik, nginx) in front pointing at 127.0.0.1:8765, and set
U3DP_HTTPS_ONLY=1 in config.env.
U3DP_HTTPS_ONLY=1 only behind HTTPS
With U3DP_HTTPS_ONLY=1 the session cookie becomes https-only: served over plain HTTP
the login appears to succeed but the session never persists → endless 401. Keep it
at 0 until you have a TLS proxy in front.
6 — Updating¶
Your data (config, db, media, seedings) lives in the ./data volume and survives the update.
This is the canonical method: it pulls the new image and recreates the container.
Using a local build?
If you uncommented build: . in docker-compose.yml, update with
git pull && docker compose build && docker compose up -d.
Updating from the in-UI button¶
In Docker you can also trigger the app and Unit3DWebUp updates from the
Settings → Version button when a new release is available. The update is applied
in place inside the container (pip install --upgrade) and then the container
restarts itself.
The in-UI update is temporary
The in-UI update lives in the container filesystem: it survives restarts but is
wiped on the next docker compose pull (which starts again from the image version).
It's a convenience for updating right away without touching the shell; the definitive
method remains docker compose pull && docker compose up -d.
The restart relies on the container restart policy: the bundled docker-compose.yml sets
restart: unless-stopped, so the container comes back on its own. If you run the image with
a bare docker run without --restart, the container will stay stopped after the update and
you'll have to start it manually.
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
KeyError: 'ContainerConfig' on docker compose up |
docker-compose v1 (1.29.2) incompatible with Docker Engine 25+ |
Install Compose v2 (§1), remove orphaned containers (docker rm -f unit3dprep), then docker compose up -d |
| Login "succeeds" but you stay logged out (401) | U3DP_HTTPS_ONLY=1 over plain HTTP |
Set U3DP_HTTPS_ONLY=0 (or put a TLS proxy in front) |
http://127.0.0.1:8765 doesn't respond |
container unhealthy | docker compose logs -f; check for starting unit3dprep-web on 0.0.0.0:8765 + Application startup complete |
| Unit3DWebUp card grey/red | webup didn't start | Check the logs; the .env is seeded automatically on first boot |
| webup logs "Field required" | DOCKER env var is set |
Never set DOCKER (the image doesn't — leave it that way) |
| Redis logs "Memory overcommit must be enabled" | host sysctl vm.overcommit_memory != 1 |
Harmless: Redis persistence is disabled (transient job store), no background save. To silence it on the host: sudo sysctl vm.overcommit_memory=1 |
Permission denied on ./data (mkdir/cp media) |
PUID/PGID ≠ your host user |
Set PUID/PGID to id -u/id -g in config.env, then docker compose up -d. For files already owned by root: sudo chown -R $(id -u):$(id -g) ./data |
| Seed fails / "InfoHash not found" | qBit paths not aligned | See §4 |