Skip to content

Technical Installation

github-actions[bot] edited this page Aug 28, 2026 · 2 revisions

Installation

Cairn runs as a Docker stack: the Django application (ASGI/Uvicorn), PostgreSQL 16 and Redis 7 (real-time features). For local development and debugging, it can also run in pure Python with no Docker (see Option 3).

Prerequisites

Option 1 : run from source

# 1. Copy the environment file
cp .env.example .env

# 2. Start the services
docker compose up --build

# 3. Apply migrations (in another terminal)
docker compose exec web python manage.py migrate

# 4. Create a superuser
docker compose exec web python manage.py createsuperuser

The application is available at http://localhost:8000. The admin interface is at http://localhost:8000/admin/.

Option 2 : run the published image

Run Cairn directly from the published Docker Hub image (frousselet/cairn) without cloning the repository.

Create a docker-compose.yml file:

services:
  web:
    image: frousselet/cairn:latest
    ports:
      - "8000:8000"
    environment:
      SECRET_KEY: change-me-to-a-random-secret-key
      DEBUG: "False"
      ALLOWED_HOSTS: localhost,127.0.0.1
      POSTGRES_DB: open_grc
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_HOST: db
      POSTGRES_PORT: "5432"
      REDIS_HOST: redis
      REDIS_PORT: "6379"
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5

  db:
    image: postgres:16
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: open_grc
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:

Then start the stack:

docker compose up -d
docker compose exec web python manage.py migrate
docker compose exec web python manage.py createsuperuser

The published image already carries the front-end libraries (the build runs manage.py vendor_assets), so the container needs no outbound network to render the interface.

Option 3 : run in pure Python for debugging (mise)

For local development and step-by-step debugging, you can run the whole stack in pure Python, with no Docker and no external service. The dev settings module core.settings_local replaces PostgreSQL with a file-based SQLite database (db.sqlite3) and Redis with an in-memory channel layer, so nothing needs to be installed or started on the side.

Python is managed by mise, pinned in mise.toml (python = "latest").

Prerequisites

  • mise
  • gettext - provides the msgfmt binary used to compile the translation catalogs so the UI renders in French (apt install gettext on Debian/Ubuntu). Without it compilemessages fails with CommandError: Can't find msgfmt and the UI falls back to English.
    • On macOS, Homebrew's gettext is keg-only: brew install gettext does not put msgfmt on the PATH. Add it explicitly, otherwise compilemessages won't find it:
      echo 'export PATH="/opt/homebrew/opt/gettext/bin:$PATH"' >> ~/.zshrc  # Apple Silicon
      # Intel Macs: /usr/local/opt/gettext/bin   - or: brew link gettext --force

Setup

# 1. Install the Python version declared in mise.toml
mise install

# 2. Create and activate a virtual environment, then install dependencies
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Point the VS Code Python extension at the mise-managed interpreter (Python: Select Interpreter${env:HOME}/.local/share/mise/installs/python/latest/bin/python); debugpy and the tasks then use it via ${command:python.interpreterPath}.

Start the application

All commands use DJANGO_SETTINGS_MODULE=core.settings_local:

export DJANGO_SETTINGS_MODULE=core.settings_local

# Compile the translation catalogs (.po -> .mo) so the UI renders in French
# (requires gettext / msgfmt - see prerequisites)
python manage.py compilemessages

# Create the SQLite schema (applies all migrations, including the data
# migrations that seed permissions, system groups and risk criteria)
python manage.py migrate

# Either create a superuser...
python manage.py createsuperuser

# ...or load the demo dataset (see below), then start the dev server
python manage.py runserver 0.0.0.0:8000

The application is available at http://localhost:8000. The defaults are dev-friendly (DEBUG=True, SECRET_KEY and ALLOWED_HOSTS fall back to safe local values), so no .env is required.

The first launch downloads the front-end libraries (Bootstrap, htmx, Leaflet, the interface font and the rest, about 3 MB) into static/vendor/, printing a line per file. They are not committed, because the pins in core/dependencies.py are the source of truth; every later start finds them in place and downloads nothing. To fetch them ahead of time, or after changing a pin :

python manage.py vendor_assets          # download whatever is missing
python manage.py vendor_assets --check  # verify every file against its digest
python manage.py vendor_assets --force  # re-download after a version bump

An install with no route out can put the files there itself and set VENDOR_ASSETS_AUTO_DOWNLOAD=False. Until they are present the interface renders unstyled - the application still starts, and says what is missing.

Debugging in VS Code

The repository ships ready-to-use VS Code configurations:

  • .vscode/launch.json : two launch configurations, Cairn: runserver (Django) and Cairn: uvicorn ASGI (Channels). Press F5 to start the server under the debugger with breakpoints. Both set DJANGO_SETTINGS_MODULE=core.settings_local for you.
  • .vscode/tasks.json : support tasks (Terminal > Run Task...):
    • stack: bootstrap : compiles the translation catalogs (non-fatal if gettext is missing), runs migrate, then seeds the demo dataset (guarded - only on an uninitialized database). It is wired as the preLaunchTask, so a plain F5 prepares the database and the French UI automatically.
    • stack: seed-demo : reloads the Voltara Energy demo dataset on demand.
    • stack: compile messages : recompiles the .po -> .mo translation catalogs on demand (after editing a locale/*.po file).
    • stack: createsuperuser : creates an additional superuser interactively.

To start from a clean dataset, delete db.sqlite3 and run the bootstrap task again.

Demo data (optional)

To explore Cairn with realistic sample content, load the fictional Voltara Energy dataset (a mid-size renewable energy operator: ISO 27001 / NIS2 / GDPR frameworks, audits, risks, EBIOS RM study, indicators, management reviews) on a fresh, empty database:

docker compose exec -T web python manage.py shell -c "exec(open('scripts/seed_demo_data.py').read())"

Then sign in with elise.moreau@voltara.example / VoltaraDemo!2026 (superuser). All seeded accounts share the same password. This script is intended for development and demo environments only.

AI assistant (optional)

"Ask Cairn" answers simple natural-language questions from the command palette (Ctrl+K), e.g. "Quelles décisions ont été prises lors de la dernière revue de direction ?". Every data access enforces the asking user's permissions, and the answer cites the real matching records. The LLM backend is a pluggable provider (AI_ASSISTANT_PROVIDER); the feature is off by default and the palette works unchanged when it is disabled or the backend is unreachable.

Default (Mistral AI, third-party EU-hosted API): no sidecar, no model download, no GPU.

# In .env, then restart web:
AI_ASSISTANT_ENABLED=True
AI_ASSISTANT_PROVIDER=mistral
AI_ASSISTANT_API_KEY=your-mistral-api-key
AI_ASSISTANT_MODEL=mistral-small-latest

Other backends are configured the same way: openai for OpenAI (ChatGPT) or any OpenAI-compatible endpoint (vLLM, LiteLLM, LocalAI...), anthropic for Claude, and ollama for a self-hosted, no-egress deployment pointed at your own Ollama instance. With a third-party provider, the question text and the compact record fields used for routing leave the platform. Provider setup, model guidance, the data-egress detail and semantic search are all documented in docs/specs/assistant/.

Scheduled lifecycle commands

Two management commands keep the risk register in sync with time and are intended to be run once a day by a cron job (host or container side):

# Set RiskAcceptance.status = EXPIRED for any active acceptance past its
# valid_until date; print upcoming expirations within --reminder-days
# (default 30) for operators to act on.
docker compose exec web python manage.py expire_risk_acceptances

# Set RiskTreatmentPlan.status = OVERDUE for any in-flight plan whose
# target_date has passed (skips COMPLETED, CANCELLED and already-OVERDUE).
docker compose exec web python manage.py mark_overdue_treatment_plans

Both accept --dry-run to preview changes. A typical host cron entry:

# /etc/cron.d/cairn-lifecycle
15 2 * * * cd /opt/cairn && docker compose exec -T web python manage.py expire_risk_acceptances
20 2 * * * cd /opt/cairn && docker compose exec -T web python manage.py mark_overdue_treatment_plans

If the semantic search of the Ask Cairn assistant is enabled (AI_ASSISTANT_SEMANTIC_ENABLED), schedule the index refresh the same way. The command is idempotent (it only re-embeds changed requirements and prunes deleted ones), so a daily run is cheap. The index is also refreshed automatically when the app starts, a deleted requirement is pruned immediately, and an administrator can force a refresh from the Administration -> Semantic index page; the daily cron is the reliable, self-healing backstop.

25 2 * * * cd /opt/cairn && docker compose exec -T web python manage.py rebuild_semantic_index

Clone this wiki locally