Install with Docker Compose
This is the supported install path for self-hosted deployments. The scripts/install.sh wizard pulls images, generates secrets, and creates the first super_admin user — typically in under 10 minutes on a warm Docker cache. Alembic migrations are applied automatically by the backend container on start (AUTO_MIGRATE, default true), so neither path below needs a manual alembic upgrade head.
Operators with sudo on a Linux host. Familiarity with docker-compose and basic shell. Not for end users — point them at the URL once the install completes.
Prerequisites
- Linux host (tested on Ubuntu 22.04 LTS, Debian 12, RHEL 9). macOS works for development but is not a supported production target.
- Docker Compose.
docker-compose(V1, hyphenated) is the project standard; theinstall.shwizard prefers it but falls back to thedocker compose(V2) plugin when V1 is absent — so a stock modern host works. See the V1/V2 note. openssl— used to generate the SECRET_KEY and database password.curl— used by the post-install health probe (and by the no-clone quick install above).- Outbound HTTPS to GitHub Container Registry (
ghcr.io, where the portal images and the Trivy DB are published). For air-gapped operation, mirror the Trivy DB to an internal OCI registry — see Vulnerability data — Air-gapped operation. - Disk: ≥ 20 GB free for images, the workspace mount, and at least seven days of backups.
- CPU/RAM: 4 vCPU / 8 GB RAM minimum. Real source scans (cdxgen + scancode) peak at ~6 GB on the worker — give it headroom.
Verify your environment:
docker-compose --version # prints Compose 1.x (preferred)
# …or, if you only have the V2 plugin, the wizard falls back to:
docker compose version # prints Compose v2.x
openssl version
curl --version
df -h / # at least 20 GB free
Evaluation install (dev stack)
Want to try TRUSCA before committing a production host? The
dev stack (docker-compose.dev.yml) stands the portal up from a clone and
seeds a realistic demo dataset. The steps — clone, migrate, up, seed, and the
sign-in accounts — are the Quickstart; it takes about
5 minutes on any 2 vCPU / 4 GB RAM host and ends with
your first real scan. This page keeps only
what the Quickstart does not cover: production sizing, TLS, and the install
wizard below.
A laptop, a throwaway cloud VM, or any 2 vCPU / 4 GB RAM host. For a real deployment use the install wizard instead — the dev stack trades production hardening (TLS, role separation, the full 6 GB scan worker) for a low-friction first look. Do not expose it to the public internet.
How vulnerabilities show up
The seeded demo dataset ships findings directly; the worker also downloads the Trivy DB on first boot if it has internet egress. The host does not need any external vulnerability engine — Trivy and its DB live entirely inside the worker container.
For air-gapped evaluation (no ghcr.io egress), see Vulnerability data — Air-gapped operation.
Real source scans (cdxgen + scancode) peak at ~6 GB on the worker. The dev stack is sized for browsing the seeded dataset, not for production scanning. It can run a small scan but will struggle on a large repository. The dev stack also skips TLS and the L1 DB role separation — do not expose it to the public internet. Use the install wizard for anything beyond a first look.
Tear down when you are done — see Quickstart — Stop the stack.
Prerequisites for HTTPS deployments
Before running the wizard, make sure your host meets these three conditions. The wizard does not validate them and Traefik will fail silently if any is missing.
- DNS: an
Arecord (orCNAME) on the domain you plan to use (e.g.oss.acme.com) must point at your host's public IP. Verify withdig +short oss.acme.com. - Firewall: ports
80and443must be reachable from the public internet. Traefik uses HTTP-01 challenge on:80to issue the Let's Encrypt certificate; once that succeeds it redirects all traffic to:443. UFW / cloud-provider firewall / security group all need both open. - TLS_EMAIL: the wizard collects this when the public URL is
https://.... Let's Encrypt sends expiry warnings and rate-limit escalation here; use a real mailbox you check.
For HTTP-only / localhost installs (development, air-gapped UAT),
none of the above applies — the wizard skips TLS_EMAIL and Traefik
does not enter the ACME flow.
Quick install (no clone)
If you just want the stack running and don't need the helper scripts, you can install directly from the published images without cloning the repository — a single-file install experience. The production images are published to GitHub Container Registry (ghcr.io/trustedoss/trusca-backend, …/trusca-backend-worker, …/trusca-frontend) and pull anonymously.
Fetch the three files the compose stack needs (the compose file, the env template, and the one-time Postgres role init script), edit .env, then start:
mkdir -p trustedoss && cd trustedoss
BASE=https://raw.githubusercontent.com/trustedoss/trusca/v0.22.6
# 1. The self-contained production compose file (no `build:` section — pulls
# images from ghcr.io) and the env template.
curl -fsSLO "$BASE/docker-compose.yml"
curl -fsSL "$BASE/.env.example" -o .env
# 2. The compose file mounts one repo file into Postgres for first-boot role
# provisioning. Fetch it to the path the compose file expects.
mkdir -p scripts
curl -fsSL "$BASE/scripts/postgres-init.sh" -o scripts/postgres-init.sh
chmod +x scripts/postgres-init.sh
# 3. Edit .env — at minimum set SECRET_KEY (openssl rand -hex 32), strong
# POSTGRES_PASSWORD / POSTGRES_APP_PASSWORD, DOMAIN, TLS_EMAIL, and
# CORS_ALLOWED_ORIGINS=https://<your-domain>. Set IMAGE_TAG to the release
# you want to run.
$EDITOR .env
# 4. Pull and start.
docker-compose -f docker-compose.yml pull
docker-compose -f docker-compose.yml up -d
Step 3 is not optional. .env.example ships SECRET_KEY empty, and outside APP_ENV=dev the backend refuses to start without one; it also refuses the placeholder string it used to ship and values built the same way, in case an older .env carries one. The startup error names what is wrong and the command to fix it. Earlier releases pinned APP_ENV=dev in this file, which overrode the production default once it was copied to .env, so a stack installed this way ran in dev mode and none of these checks applied.
The published backend image's entrypoint applies Alembic migrations automatically on start (AUTO_MIGRATE, default true) and only then starts uvicorn, so the schema is at HEAD by the time the backend reports healthy. You do not need to run alembic upgrade head by hand. Automatic migration does not create users, so you still bootstrap the first admin once:
# Read the password into the shell WITHOUT echoing it, then pass only the
# variable NAME to `-e` so the value is inherited from the calling shell and
# never lands in argv (visible in `ps -ef`) or in your shell history.
read -rs ADMIN_PASSWORD; export ADMIN_PASSWORD # type the 12+ char password, press Enter
# Create the first super_admin (the schema is already at HEAD).
docker-compose -f docker-compose.yml exec -T \
-e ADMIN_EMAIL=you@example.com \
-e ADMIN_PASSWORD \
backend python -m scripts.create_super_admin
unset ADMIN_PASSWORD # clear it from the shell once the user exists
Avoid -e ADMIN_PASSWORD='literal': the literal is visible to any user who
runs ps -ef while the command executes and is written to your shell history.
Passing the bare name (-e ADMIN_PASSWORD) makes Docker inherit the value
from the environment instead.
The single-role .env template ships AUTO_MIGRATE=true and it just works. If you run an L1 role-separated stack (separate DATABASE_URL_OWNER for DDL and DATABASE_URL_APP for runtime), the runtime container only holds the DML-only app DSN and cannot run DDL, so automatic migration must be off.
- With the wizard (Step 2):
install.shdetects L1 (DATABASE_URL_OWNERis set and differs from the runtime DSN) and writesAUTO_MIGRATE=falseto.envautomatically, then applies migrations as the owner role itself. You do not need to set anything. - On this no-clone path: there is no wizard, so you must set
AUTO_MIGRATE=falsein.envyourself for an L1 stack and runalembic upgrade headas the owner role (overrideDATABASE_URLwithDATABASE_URL_OWNERfor that one command). If you leave ittrueon an L1 stack the backend entrypoint fails fast (exit 1, no crash-loop) with a clear DDL-permission error in the logs.
Liveness vs. readiness: how the stack waits for the schema
The backend exposes two unauthenticated health endpoints. They answer different questions, and the Compose / Kubernetes startup gates depend on the distinction.
| Endpoint | Question it answers | Touches the DB? | Used by |
|---|---|---|---|
GET /health | Is the uvicorn process up and accepting requests? (pure liveness) | No | Kubernetes livenessProbe; liveness-only consumers |
GET /health/ready | Is the Postgres schema at the Alembic HEAD revision, i.e. is it safe to serve traffic and start workers? (readiness) | Yes (a read-only SELECT on alembic_version) | Compose backend healthcheck; Kubernetes readinessProbe |
/health/ready returns 200 {"status":"ready","redis":"ok"|"degraded"} only when the schema matches HEAD. Otherwise it returns 503 with an RFC 7807 application/problem+json body summarising the revision mismatch (it never leaks the DSN or credentials), also carrying the same redis field. That field is observational only: a Redis outage never turns a 200 into a 503, since the request-path controls that touch Redis (the login throttle, the rate limiter) are already designed to fail open through one. See the on-call runbook for what to do when it reads degraded.
Since (Track B), the backend service's Compose healthcheck probes /health/ready, so the worker and beat services — which declare depends_on: backend (condition: service_healthy) — start only after the schema is migrated, under both toggles:
AUTO_MIGRATE=true(single-role default): the backend container runsalembic upgrade headon start and/health/readyflips to200once it finishes. Workers then start against a migrated schema. This is the normal path and needs no operator action.AUTO_MIGRATE=false(L1 role-separated stack): uvicorn answers/healthimmediately, but/health/readystays503(the container stayshealth: starting) until your externalalembic upgrade head(run as the owner role,install.sh/upgrade.shdo this) brings the schema to HEAD. This is intended: the worker and beat wait for the schema instead of starting against a not-yet-migrated database. If you forget to run the migration on an L1 stack, the backend will simply never become healthy, checkdocker-compose logs backendand run the owner-role migration.
unhealthyThe backend healthcheck uses a generous start_period (60s). A large first migration on a big database can run for a while before /health/ready turns 200; the start_period keeps Docker from marking the container unhealthy (and restarting it) before that first migrate completes.
The install.sh wizard (Steps 1–3 below) does all of this for you — secret generation, the health-wait loop, the migration, and the admin bootstrap — and it also works with the Compose V2 plugin (docker compose) if your host doesn't have V1. Use the no-clone path when you want full control over each step or are baking your own automation.
Step 1 — Clone the repository
git clone https://github.com/trustedoss/trusca.git
cd trusca
If you maintain a fork, clone the fork instead. Pin to a release tag for reproducible installs:
git checkout v0.22.6