Conformiti
ProductEditionsPricing ConsultingSelf-hostDocs Security GitHub Book a demo

Self-hosting

Your servers.
Your evidence.
One command.

AGPL-3.0 licensed, with no licence server, no telemetry and no phone-home. The only call you did not configure is the interface's two Google webfonts, which you can self-host.

The full documents ship in the repository: INSTALL.md, PREREQUISITES.md and .env.example.

Docker · recommended
$ git clone --branch v0.9.5oe https://github.com/dboudreau00/Conformiti.git
$ cd Conformiti
$ docker compose up -d --build

No .env needed. The install starts empty, so create your first account, then open http://localhost:8080.

shell
$ docker compose exec backend python manage.py createsuperuser
Want the worked example? Boot the first time with SEED_DEMO_DATA=true. The demo accounts are superusers with no second factor, so retire them before real data goes in (first-run checklist).

What you need

A modest VM is enough

Docker path

Docker Engine 24+ with Compose 2.24.0+, ports 8080 and 8000 free. 2 vCPU / 4 GB / 20 GB covers tens of users. Prebuilt amd64 and arm64 images are on ghcr.io/dboudreau00.

Local path

Python 3.11-3.14 and Node 20.19+ or 22.12+. SQLite and console email, for a trial.

Production

PostgreSQL 16, Redis 7, a TLS proxy, a mail sender and a backup target.

Optional

S3 for evidence, ClamAV, OIDC or SAML, Slack or Teams, Jira Cloud.

Three ways in

Pick the one that matches your intent.

Recommended

Docker Compose

DEBUG off, a generated secret key, rate limits in Redis, and only nginx on the network.

compose
$ docker compose up -d --build
$ docker compose ps
$ docker compose logs -f backend
Guided

The install script

The same, but it waits for a healthy stack and prints the URLs.

macOS · Linux · WSL
$./install.sh --docker
# or, without Docker:
$./install.sh
# validator, backend suite, frontend build:
$./install.sh --test
Windows PowerShell
$.\install.ps1 -Docker
$.\install.ps1 -Test
Trial / dev

Local, no Docker

SQLite and console email. Good for a look around, not for production.

local
$./install.sh
# backend  → 127.0.0.1:8000
# frontend → 127.0.0.1:5173
# mail printed to stdout

What comes up

Six containers,
five volumes,
one port.

Only nginx is published. It carries the CSP, the security headers and the 32 MB body cap.

  • db: PostgreSQL 16
  • redis: cache, throttles and the Celery broker
  • backend: gunicorn and Django, health-checked at /api/health/
  • worker: Celery, which runs the scheduled scans
  • beat: the scheduler that queues them, never more than one
  • frontend: nginx, which releases evidence only after the API authorises it

Topology

browser
  │:8080
  ▼
nginx ─┬─▶ gunicorn (backend, :8000) ─┬─▶ PostgreSQL
       │        ▲ /api/health/       └─▶ Redis
       ├─ /static, /media  (volumes)
       └─ CSP · security headers · 32 MB cap

beat ──▶ Redis ──▶ worker ──▶ PostgreSQL · email

volumes: pgdata · media · static · secrets · tree
secrets holds DJANGO_SECRET_KEY_FILE and the
package signing key: back both of them up.
Customising nginx? Never add Content-Disposition in an X-Accel location. The API sets it, and two copies make browsers refuse the file.

Do this before real data

First-run
checklist.

On Docker, run each command with docker compose exec backend python. Locally, use ../.venv/bin/python from backend/.

Create your administrator

The password policy applies: 12 characters by default.

shell
$ docker compose exec backend python manage.py createsuperuser

Retire the demo accounts and data

Only if you booted with SEED_DEMO_DATA=true. The five personas share one password, printed once in the backend log.

shell
$ docker compose exec backend python manage.py remove_demo_data
# or, to delete rather than deactivate:
$ docker compose exec backend python manage.py remove_demo_data --delete

Put your real hostname in the config

DJANGO_ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS, CORS_ALLOWED_ORIGINS and PUBLIC_URL.

Put TLS in front

Then set BEHIND_TLS=true for secure cookies and the HTTPS redirect, and add HSTS at the proxy.

Back up the secrets volume

It holds the secret key and the package signing key. Lose the signing key and old packages still verify, but you cannot sign as the same identity again.

Configuration

Everything is environment-driven.

Compose ships safe defaults and .env overrides them. These are the keys that matter most.

Key environment variables
SettingPurpose
Core
DJANGO_DEBUGfalse in Docker. The app refuses to boot in an obviously unsafe combination.
DJANGO_SECRET_KEY
DJANGO_SECRET_KEY_FILE
A key, or a path where one is generated and kept. Compose uses the file.
DJANGO_ALLOWED_HOSTS
CSRF_TRUSTED_ORIGINS
CORS_ALLOWED_ORIGINS
Your public hostnames. The most common cause of an install that refuses logins.
BEHIND_TLStrue behind a TLS proxy: secure cookies, HTTPS redirect, __Host- prefixes.
PUBLIC_URLThe base for vendor-questionnaire links.
ORGANISATION_NAMEYour name in outbound email and on the vendor's questionnaire page.
Data and mail
DATABASE_URLPostgreSQL 16 in Docker and production, SQLite locally.
EMAIL_PROVIDERconsole · smtp · mailbox (IMAP/POP3 + SMTP) · ses. Test with manage.py test_mailbox --to you@example.com.
REVIEW_SCAN_HOUR
REVIEW_ALERT_LEAD_DAYS
When the reminder scan runs, and its lead days (30, 14, 7, 1).
S3_*Optional S3 storage for evidence.
Identity
OIDC_*Issuer, client, scopes and a domain allow-list. Auto-provisioning is off by default and never grants a role that manages users.
SAML_*IdP metadata, entity id, ACS URL and signing certificate. Replays and HMAC signatures are refused.
SSO_STEP_UPoff · if_enrolled · required: whether SSO also needs a local second factor.
WEBAUTHN_RP_ID
WEBAUTHN_ORIGINS
A real domain, never an IP. Pin both behind a proxy that rewrites Host.
Assurance and alerting
SIGNING_KEY_FILE
SIGNING_KEY
The Ed25519 package-signing key. Rotate with manage.py rotate_signing_key.
CLAMAV_*Upload scanning, opt-in: docker compose --profile scanning up -d with CONFORMITI_SCANNING=true.
SLACK_WEBHOOK_URL
TEAMS_WEBHOOK_URL
Incoming webhooks, https only, set by an operator.
MAX_UPLOAD_MB
PASSWORD_MIN_LENGTH
THROTTLE_LOGIN
Upload cap (raise nginx's client_max_body_size to match), password policy and login throttle.
SEED_DEMO_DATAfalse by default. Only on something you do not intend to keep.

Every key, with defaults: .env.example.

Operations

Running it, week to week.

Scheduled work

Celery beat runs these in Docker, except scan_evidence: put that on a monthly cron. Without Docker, cron all of them.

management commands
$ manage.py send_review_reminders [--dry-run]
$ manage.py record_readiness
$ manage.py scan_evidence
$ manage.py flushexpiredtokens
$ manage.py send_digests

Administration

All idempotent. seed_frameworks picks up revised libraries after an upgrade.

management commands
$ manage.py createsuperuser
$ manage.py seed_frameworks --with-folders
$ manage.py seed_frameworks --roles-only
$ manage.py remove_demo_data [--delete] [--workspace slug]
$ manage.py rotate_signing_key
$ manage.py link_oidc_identity
$ manage.py test_mailbox --to you@example.com

Backup and restore

The database, the media volume (the evidence itself) and the secrets volume. All three.

example
$ docker compose exec -T db pg_dump -U compliance compliance | gzip > db.sql.gz
$ docker run --rm -v conformiti_media:/m -v "$PWD:/out" alpine \
      tar czf /out/media.tgz -C /m.

Upgrading

Back up, check out the new tag, rebuild. Migrations run on start.

upgrade
$ git fetch --tags && git checkout v0.9.5oe
$ docker compose pull && docker compose up -d --build

Each release's migrations and timing are in CHANGELOG.md. Coming from 0.8.x, budget a few seconds per hundred thousand rows.

Hardening

The list we would run through with you.

Put /admin/ behind a VPN

The Django admin sits outside the app's MFA, throttle and workspace rules, and its session reaches the whole API. Keep it behind an allow-list.

Second factor for every manager

Nothing enforces it, so make it policy. Backup codes stop a lost key from locking anyone out.

SSO with step-up

If your provider does not assert MFA, set SSO_STEP_UP=required.

Turn on malware scanning

Off by default. Enable it, watch the health probe, and re-scan stored evidence monthly.

Keep the signing key apart

On its own volume, backed up separately, ideally off the database host.

Publish a digest out of band

Email the manifest digest to your assessor when you seal. It binds the package to a moment you both saw.

Watch failed sign-ins

login_failed records a reason. A spike in one reason is a signal.

Retire demo data, then verify

Settings › About says whether demo data is present. Check it.

Restore once, on purpose

Into a scratch environment, before you need to.

Troubleshooting

The six things
that usually go wrong.

Not one of these? Open an issue with your /api/health/ output and the backend logs.

Open an issue
The site loads but I cannot sign in

Your hostname is missing from DJANGO_ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS or CORS_ALLOWED_ORIGINS, scheme and port included. The backend log names the header it refused. Behind a proxy, forward Host and X-Forwarded-Proto.

Downloads fail or the browser refuses the file

Something in your nginx config adds Content-Disposition under /protected-media/. Remove it and let the API own the header.

Passkeys will not enrol or will not verify

WEBAUTHN_RP_ID must be a domain, never an IP. Use localhost locally and match WEBAUTHN_ORIGINS exactly, port included.

Reminder emails are not arriving

Run send_review_reminders --dry-run to see what is due, and test_mailbox to test the transport. Each reminder is sent once, so a second run sending nothing is correct.

Everything is 403 after upgrading to 0.9.0

The account has no workspace. Assign one under Settings › Role & access, or re-run the migration if it stopped part-way.

A file is stuck in quarantine

New scanner definitions matched it. Nothing is deleted, and there is no manual release: a clean re-scan or a new version frees it, and either is recorded.

Need a hand with the first install?

Pro and Enterprise include support. Consulting puts someone beside you.