The pre-built public image still runs with one command, but after ADR-T-009 §D2 it requires two operator-supplied secrets at startup:
With Docker:
docker run -it \
--env TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN="MyAccessToken" \
--env TORRUST_INDEX_CONFIG_OVERRIDE_DATABASE__CONNECT_URL="sqlite:///var/lib/torrust/index/database/sqlite3.db?mode=rwc" \
torrust/index:latestor with Podman:
podman run -it \
--env TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN="MyAccessToken" \
--env TORRUST_INDEX_CONFIG_OVERRIDE_DATABASE__CONNECT_URL="sqlite:///var/lib/torrust/index/database/sqlite3.db?mode=rwc" \
torrust/index:latestA bare docker run -it torrust/index:latest (the
zero-config invocation that worked in earlier releases) now
fails to start with a serde missing field error — this is
intentional, see ADR-T-009 §D2.
- Tested with recent versions of Docker or Podman.
The Containerfile defines three volumes:
VOLUME ["/var/lib/torrust/index","/var/log/torrust/index","/etc/torrust/index"]When instancing the container image with the docker run or podman run command, we map these volumes to the local storage:
./storage/index/lib -> /var/lib/torrust/index
./storage/index/log -> /var/log/torrust/index
./storage/index/etc -> /etc/torrust/indexNOTE: You can adjust this mapping for your preference, however this mapping is the default in our guides and scripts.
Please run this command where you wish to run the container:
mkdir -p ./storage/index/lib/ ./storage/index/log/ ./storage/index/etc/It is important that the torrust user has the same uid $(id -u) as the host mapped folders. In our entry script, installed to /usr/local/bin/entry.sh inside the container, switches to the torrust user created based upon the USER_ID environmental variable.
When running the container, you may use the --env USER_ID="$(id -u)" argument that gets the current user-id and passes to the container.
USER_ID must be a non-negative integer and must not be 0
(the entry script refuses to run as root). Any positive UID
is accepted — including low-UID values produced by rootless
Podman with subuid remapping, low-UID CI runners, or
BSD-derived hosts. The previous USER_ID >= 1000 rule was
dropped because it rejected several of these legitimate
configurations without stating its intent.
Using the standard mapping defined above produces this following mapped tree:
storage/index/
├── lib
│ ├── database
│ │ └── index.sqlite3.db => /var/lib/torrust/index/database/index.sqlite3.db [auto populated, sqlite3 only]
│ └── tls
│ ├── localhost.crt => /var/lib/torrust/index/tls/localhost.crt [user supplied]
│ └── localhost.key => /var/lib/torrust/index/tls/localhost.key [user supplied]
├── log => /var/log/torrust/index (future use)
└── etc
├── auth
│ ├── private.pem => /etc/torrust/index/auth/private.pem [auto generated on first boot]
│ └── public.pem => /etc/torrust/index/auth/public.pem [auto generated on first boot]
└── index.toml => /etc/torrust/index/index.toml [auto populated]NOTE: you only need the
tlsdirectory and certificates in case you have enabled SSL.The
auth/directory and RSA key pair are auto-generated on first boot by the container entry script. Sessions persist across restarts as long as the/etc/torrust/indexvolume is retained. To use your own keys, either pre-populate the volume before first boot or overwrite the generated files and restart. Per ADR-T-009 §D3 the script is the single source of truth for the auth-key paths: when neither..._AUTH__*_PEMnor..._AUTH__*_PATHis configured, the script generates the pair at the locations above and exports the corresponding..._AUTH__*_PATHoverrides so the application sees the same paths.The SQLite database file is only auto-populated when the resolved
database.connect_url(viaTORRUST_INDEX_CONFIG_OVERRIDE_DATABASE__CONNECT_URLor the mountedindex.toml) names an absolute SQLite path under one of the managed volumes. MySQL/MariaDB connections are not seeded — the application connects directly. See Entry Script Contract.
The image build is gated on the workspace test suite passing.
The Containerfile defines intermediate test / test_debug
stages that compile the workspace's test archive and run it
with cargo nextest run before the runtime image stages
(runtime_release / runtime_debug) are assembled. A red
test run blocks image production until the underlying issue
is fixed.
This is a deliberate trade-off:
- What you get. A red
develop/maintest run cannot silently ship as a published image. There is no build-time--skip-testsescape hatch — and that omission is intentional. Any such hatch would, in time, end up wired into a release pipeline "just for this one urgent fix" and would defeat the gate's whole purpose. - What it costs. A flaky or environment-dependent test blocks image production even when the application is unaffected. Treat this as a forcing function, not as collateral damage: stabilise or quarantine the test rather than reaching for an opt-out.
- Escalation path. When a known-flaky test blocks an
urgent build, the supported response is to
#[ignore]the specific test on a tracked branch with a linked issue, rebuild, then follow up by fixing the test and removing the#[ignore]. There is no privileged rebuild path that bypasses the gate.
See ADR-T-009 §D9 for the design rationale.
# Inside your dev folder
git clone https://github.com/torrust/torrust-index.git; cd torrust-indexBefore starting, if you are using docker, it is helpful to reset the context to the default:
docker context use default# Release Mode
docker build --target release --tag torrust-index:release --file Containerfile .
# Debug Mode
docker build --target debug --tag torrust-index:debug --file Containerfile .Podman defaults to writing OCI-format manifests. The OCI image-spec
has no field for HEALTHCHECK, so building without --format docker
drops the directive (and prints a WARN[…] HEALTHCHECK is not supported for OCI image format line). Pass --format docker so the
healthcheck survives in the manifest:
# Release Mode
podman build --format docker --target release --tag torrust-index:release --file Containerfile .
# Debug Mode
podman build --format docker --target debug --tag torrust-index:debug --file Containerfile .docker build defaults to Docker-format manifests, so the flag is
only needed for Podman / Buildah. Running OCI-format images on Docker
or Podman works regardless of which format they were built with;
the format only matters for Docker-specific manifest extensions like
HEALTHCHECK.
The minimum invocation supplies the two mandatory overrides
introduced by ADR-T-009 §D2 (tracker.token and
database.connect_url). Without them, the config probe
fails the schema check and the entry script aborts startup
before privilege drop. See Entry Script Contract
for the full boot sequence.
# Release Mode
docker run -it \
--env TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN="MySecretToken" \
--env TORRUST_INDEX_CONFIG_OVERRIDE_DATABASE__CONNECT_URL="sqlite:///var/lib/torrust/index/database/index.sqlite3.db?mode=rwc" \
torrust-index:release
# Debug Mode
docker run -it \
--env TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN="MySecretToken" \
--env TORRUST_INDEX_CONFIG_OVERRIDE_DATABASE__CONNECT_URL="sqlite:///var/lib/torrust/index/database/index.sqlite3.db?mode=rwc" \
torrust-index:debug# Release Mode
podman run -it \
--env TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN="MySecretToken" \
--env TORRUST_INDEX_CONFIG_OVERRIDE_DATABASE__CONNECT_URL="sqlite:///var/lib/torrust/index/database/index.sqlite3.db?mode=rwc" \
torrust-index:release
# Debug Mode
podman run -it \
--env TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN="MySecretToken" \
--env TORRUST_INDEX_CONFIG_OVERRIDE_DATABASE__CONNECT_URL="sqlite:///var/lib/torrust/index/database/index.sqlite3.db?mode=rwc" \
torrust-index:debugThe arguments need to be placed before the image tag. i.e.
run [arguments] torrust-index:release
Environmental variables are loaded through the --env, in the format --env VAR="value".
The following environmental variables can be set:
TORRUST_INDEX_CONFIG_TOML_PATH- The in-container path to the index configuration file, (default:"/etc/torrust/index/index.toml").TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN- Required. Tracker admin token. Per ADR-T-009 §D2 the shipped TOMLs no longer carry a default value for this field, so the operator must supply it via this env var (or pre-populate the in-volumeindex.toml). Startup fails withmissing field 'token'otherwise.TORRUST_INDEX_CONFIG_OVERRIDE_DATABASE__CONNECT_URL- Required. Database connection URL (e.g.sqlite:///var/lib/torrust/index/database/index.sqlite3.db?mode=rwcor amysql://...URL). Same rule asTRACKER__TOKEN: shipped TOMLs no longer carry a default, so absent both env var and operator-supplied TOML the application fails to start withmissing field 'connect_url'.TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PRIVATE_KEY_PATH- Path to an RSA private key PEM file for JWT signing. Optional: without this, ephemeral auto-generated keys are used (sessions will not survive restarts).TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PUBLIC_KEY_PATH- Path to an RSA public key PEM file for JWT verification. Required whenPRIVATE_KEY_PATHis set.TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PRIVATE_KEY_PEM- Inline RSA private key PEM string (alternative to file path). Optional: for persistent sessions.TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PUBLIC_KEY_PEM- Inline RSA public key PEM string (alternative to file path). Required whenPRIVATE_KEY_PEMis set.TORRUST_INDEX_DATABASE_DRIVER- Input-validation gate only (options:sqlite3,mysql, defaultsqlite3). Per ADR-T-009 §7.4, this env var was originally introduced to choose which driver-suffixed default TOML to seed into/etc/torrust/index/on first boot. Phase 9 then collapsed those two driver-suffixed defaults into a single driver-agnosticindex.container.toml(Phase 5 had already madedatabase.connect_urlmandatory, so the file no longer encodes a driver choice). The env var is retained as an input-validation gate so a typo (postgres,Sqlite, …) fails fast at container start rather than silently propagating; both supported values now seed the same template. Runtime database decisions are taken from the config probe'sdatabase.driverfield, derived fromdatabase.connect_url's URL scheme. Note the taxonomy difference — the env var usessqlite3/mysql; the probe (and the application) emitsqlite/mysql. Operators who scripted around this env var expecting it to control runtime behaviour must update their scripts to supplyTORRUST_INDEX_CONFIG_OVERRIDE_DATABASE__CONNECT_URLinstead.TORRUST_INDEX_CONFIG_TOML- Load config from this environmental variable instead from a file, (i.e:TORRUST_INDEX_CONFIG_TOML=$(cat index-index.toml)).USER_ID- The user id for the runtime-createdtorrustuser. Must be a non-negative integer and must not be0. Should match the ownership of the host-mapped volumes (default1000).API_PORT- The port for the index API. This should match the port used in the configuration, (default3001).IMPORTER_API_PORT- The port for the importer API. This should match the port used in the configuration, (default3002).TZ- Container time zone passed through to the runtime (defaultEtc/UTC). Set in the ContainerfileENVblock; override atdocker runtime if you need wall-clock log timestamps in a specific zone.
NOTE:
API_PORTandIMPORTER_API_PORTare runtimeENVvalues, not build-timeARGs. Overriding them atdocker run/podman runtime with--env API_PORT=…correctly reaches the application listener and the in-containerHEALTHCHECK, but theEXPOSEdirective in theContainerfileis evaluated at build time and bakes the defaults (3001,3002) into image metadata. Tools that read that metadata (docker inspect,docker port) will continue to report the defaults regardless of any--envoverride. Use--publish host:containerto map whichever container port the application is actually listening on.
Socket ports used internally within the container can be mapped to with the --publish argument.
The format is: --publish [optional_host_ip]:[host_port]:[container_port]/[optional_protocol], for example: --publish 127.0.0.1:8080:80/tcp.
The default ports can be mapped with the following:
--publish 0.0.0.0:3001:3001/tcpNOTE: Inside the container it is necessary to expose a socket with the wildcard address
0.0.0.0so that it may be accessible from the host. Verify that the configuration that the sockets are wildcard.
By default the container will install volumes for /var/lib/torrust/index, /var/log/torrust/index, and /etc/torrust/index, however for better administration it good to make these volumes host-mapped.
The argument to host-map volumes is --volume, with the format: --volume=[host-src:]container-dest[:<options>].
The default mapping can be supplied with the following arguments:
--volume ./storage/index/lib:/var/lib/torrust/index:Z \
--volume ./storage/index/log:/var/log/torrust/index:Z \
--volume ./storage/index/etc:/etc/torrust/index:Z \Please not the :Z at the end of the podman --volume mapping arguments, this is to give read-write permission on SELinux enabled systemd, if this doesn't work on your system, you can use :rw instead.
## Setup Docker Default Context
docker context use default
## Build Container Image
docker build --target release --tag torrust-index:release --file Containerfile .
## Setup Mapped Volumes
mkdir -p ./storage/index/lib/ ./storage/index/log/ ./storage/index/etc/
## Run Torrust Index Container Image
## Note: Without key path env vars, ephemeral auto-generated keys are used.
## For persistent sessions, supply your own RSA key pair:
## --env TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PRIVATE_KEY_PATH="/var/lib/torrust/index/jwt/private.pem" \
## --env TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__PUBLIC_KEY_PATH="/var/lib/torrust/index/jwt/public.pem" \
docker run -it \
--env TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN="MySecretToken" \
--env TORRUST_INDEX_CONFIG_OVERRIDE_DATABASE__CONNECT_URL="sqlite:///var/lib/torrust/index/database/index.sqlite3.db?mode=rwc" \
--env USER_ID="$(id -u)" \
--publish 0.0.0.0:3001:3001/tcp \
--volume ./storage/index/lib:/var/lib/torrust/index:Z \
--volume ./storage/index/log:/var/log/torrust/index:Z \
--volume ./storage/index/etc:/etc/torrust/index:Z \
torrust-index:release## Build Container Image
podman build --format docker --target release --tag torrust-index:release --file Containerfile .
## Setup Mapped Volumes
mkdir -p ./storage/index/lib/ ./storage/index/log/ ./storage/index/etc/
## Run Torrust Index Container Image
podman run -it \
--env TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN="MySecretToken" \
--env TORRUST_INDEX_CONFIG_OVERRIDE_DATABASE__CONNECT_URL="sqlite:///var/lib/torrust/index/database/index.sqlite3.db?mode=rwc" \
--env USER_ID="$(id -u)" \
--publish 0.0.0.0:3001:3001/tcp \
--volume ./storage/index/lib:/var/lib/torrust/index:Z \
--volume ./storage/index/log:/var/log/torrust/index:Z \
--volume ./storage/index/etc:/etc/torrust/index:Z \
torrust-index:releasePer ADR-T-009 §8, the repository ships two Compose files with a clear separation between production-shaped baseline and dev sandbox:
compose.yaml— deployment template. Production-shaped: nomailcatchersidecar, notty, external ports bound to127.0.0.1(except the index API on:3001), and credentials referenced as bare${VAR}with no defaults. This is the file operators copy as a starting point for real deployments.compose.override.yaml— for development. Auto-loaded by Compose v2 (i.e. by a plaindocker compose upand bymake up-dev). Adds themailcatchersidecar, allocates TTYs onindex/tracker, and supplies permissive${VAR:-default}defaults for the credentials the baseline leaves blank.
Two top-level Makefile targets wrap the two
documented invocation paths:
# Dev sandbox: auto-loads compose.override.yaml.
make up-dev
# Production-shaped: validates required credentials, then
# runs `docker compose --file compose.yaml up -d --wait`
# (override excluded). Required env vars:
#
# USER_ID (numeric host UID owning ./storage)
# TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN
# TORRUST_INDEX_CONFIG_OVERRIDE_DATABASE__CONNECT_URL
# TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN
# MYSQL_ROOT_PASSWORD (only if the local mysql sidecar is in use)
make up-prodmake up-prod is fail-fast convenience — defence in depth,
not the only line. The container's config probe (ADR-T-009
§6) is the authoritative gate: it rejects empty
connect_url (exit 3) and empty tracker.token (exit 4)
regardless of how Compose was invoked.
A developer running docker compose -f compose.yaml up
(deliberately bypassing the override) gets bare ${VAR}
substitution to empty strings; the config probe catches this
inside the container, so the worst case is a clear startup
failure rather than silent misbehaviour.
Both release and debug ship the same two-probe HEALTHCHECK
block, invoking torrust-index-health-check against the index API
and the importer API in turn. The healthcheck binary is itself
0500 root:root; the HEALTHCHECK directive runs as root (no
--user flag in the directive), so the unprivileged torrust user
cannot invoke it directly.
If you build with Podman without --format docker, the directive is
silently dropped at build time (see the build section above) and the
image will report no health status. docker build is unaffected.
The two build targets ship deliberately different shell footprints.
release target. Built on the lean
gcr.io/distroless/cc-debian13 base. Ships a single
/bin/busybox binary (mode 0700 root:root) plus a curated
set of applet symlinks pointing at it. The unprivileged
torrust user that the application runs as gets EACCES
on /bin/busybox (and therefore on every applet symlink)
after privilege drop — the busybox tree is reachable only
by root. The curated symlink set covers exactly the applets
the entry script needs at first boot:
sh,adduser,addgroup,install,mkdir,dirname,chown,chmod,tr,mktemp,cat,printf,rm,echo,grep
su-exec is a separate root-only binary at /bin/su-exec,
not a busybox applet. jq is a separate root-only binary at
/usr/bin/jq used by the entry script's auth-keypair
bootstrap. None of these are reachable by the unprivileged
torrust user.
There is no /busybox/ directory in the release image — the
full busybox applet tree from the upstream :debug
distroless image is deliberately not present, so absolute-path
invocations like /busybox/ls cannot bypass the curated
subset.
For emergency operational debugging, docker exec -u root … sh still works on the release image (the curated /bin/sh
resolves through PATH to /bin/busybox, and 0700 root:root
permits root invocation). This is the documented break-glass
procedure.
debug target. Built on gcr.io/distroless/cc-debian13:debug,
which ships the upstream full busybox tree at /busybox/
with default world-executable permissions. The debug image
leaves that tree in place and puts /busybox/ on PATH so
the unprivileged user retains access to the complete applet
set (id, whoami, ps, grep, wget, …). This is the
debug image's purpose; use it whenever you need an
interactive shell as the application user.
The container entry script does not produce verbose output by default.
To enable shell tracing (set -x) for startup troubleshooting, set the
DEBUG environment variable:
--env DEBUG=1The entry script also runs under set -eu (POSIX errexit +
nounset): any unchecked command failure aborts startup
immediately, and references to unset variables are treated as
errors. This converts a class of silent-misconfiguration bugs
into loud, actionable startup failures.
Per ADR-T-009 §7, the entry script reads its configuration in the following order. Each step depends on the values resolved by previous steps.
The complete list of environment variables the entry script
consults is maintained as a canonical manifest comment block
in share/container/entry_script_sh,
delimited by the # ENTRY_ENV_VARS: and
# END_ENTRY_ENV_VARS sentinel lines. CI verifies that
every variable named between those sentinels is documented
in this file (see ADR-T-009 Acceptance Criterion #7);
when adding or removing a variable from the entry script,
update both the manifest block and the env-var section
above.
USER_ID— numeric, non-zero (refuses to run as root). Default1000. Used to create the unprivilegedtorrustuser viaadduserand to chown the volume directories.TORRUST_INDEX_DATABASE_DRIVER— validated as an input-only gate (sqlite3ormysql); both values now seed the same driver-agnosticindex.container.tomltemplate into/etc/torrust/index/index.tomlon first boot. Unset or unrecognised values abort startup with an explicit error. See the env-var entry above for full scope and migration notes.RUNTIME— selects the message-of-the-day banner (runtime,debug, orrelease). Set by the Containerfile per image variant.- Config probe (
/usr/bin/torrust-index-config-probe) — invoked as root after the default TOML is in place. The probe is the same loader the application uses, so it sees the operator's full TOML + env-var stack. Its JSON output is consumed byjqand drives the remaining steps. The probe runs before the script exports anyTORRUST_INDEX_CONFIG_OVERRIDE_*of its own, so its output reflects only operator-supplied values. TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__{PRIVATE,PUBLIC}_KEY_{PEM,PATH}— the probe reports raw presence and resolved source for each key. The script enforces three invariants post-probe: PEM and PATH are mutually exclusive within a single key; both keys must be configured or neither (no half-pair); and both keys must use the same delivery mechanism (no mixed PEM/PATH across the pair). When neither key is configured anywhere (probesource=none), the script applies the container defaults (/etc/torrust/index/auth/private.pemand.../public.pem) and exports the corresponding..._AUTH__*_PATHoverride env var so the application sees the same path the script materialises. The script is the single source of truth for these defaults; there is no constant duplicated between two files to drift apart.database.connect_url(resolved by the probe) — the probe'sdatabase.driverfield selects the seeding dispatch (sqliteseeds the default DB file;mysqlis a no-op since the application connects directly). For SQLite the seed is materialised at the resolved path only when the parent directory lives under one of the managed volumes (/etc/torrust/index/,/var/lib/torrust/index/,/var/log/torrust/index/); paths outside those roots must be pre-created by the operator.exec /bin/su-exec torrust ...— drops privileges and execs the application (or the suppliedCMD).
The default TOML shipped at
/usr/share/torrust/default/config/index.container.toml
intentionally leaves [tracker] and [database] empty so
operators must supply real values. Per ADR-T-009 §D2 the
schema requires tracker.token and
database.connect_url — the config probe will exit non-zero
(codes 3/4) and the entry script will abort startup if
either is missing. Supply them via:
--env TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN=...
--env TORRUST_INDEX_CONFIG_OVERRIDE_DATABASE__CONNECT_URL=sqlite:///var/lib/torrust/index/database/index.sqlite3.db?mode=rwcor by mounting a populated index.toml at
/etc/torrust/index/index.toml. The
compose.override.yaml shipped
in the repo wires both overrides for the local dev workflow
(make up-dev); see Compose Split.
Both runtime images ship a root-only /usr/bin/jq (mode
0500 root:root, sourced from a pristine rust:slim-trixie
jq_donor build stage in the Containerfile). It is invoked
only during the entry script's pre-su-exec phase to parse
the config probe's JSON output and the auth-keypair helper's
JSON output. The unprivileged torrust user has no access
to /usr/bin/jq after privilege drop.
The entry script's pure helper functions (inst,
key_configured, validate_auth_keys, seed_sqlite) live
in a separate POSIX sh library shipped at
/usr/local/lib/torrust/entry_script_lib_sh (mode
0444 root:root, sourced — not exec'd). Splitting them
out lets the workspace test crate
packages/index-entry-script/
drive each helper through a host sh subprocess and assert
the exit-code / stderr contracts of every branch of
ADR-T-009 §7.1's auth-key invariants and §7.2's seeding
outcomes. The library has no top-level side effects, so
sourcing it from either the entry script or a test harness
is safe.