Port Igniter
Get Started

Getting Started

Install Reef locally with docker compose, log in, and run your first scan.

Reef Support Home

Prerequisites

Docker and Docker Compose are the only requirements — everything else (Django, Celery, Redis, the scan tools) ships inside the images.

1. Configure your environment

# Copy over the provided .env.example to your machine
cp .env.example .env

Edit .env and set at minimum:

Variable What it’s for
DJANGO_SECRET_KEY generate one
DJANGO_API_KEYS comma-separated list of accepted X-API-Key values
REEF_AGENT_API_KEY the key the agent uses — must equal one of the values in DJANGO_API_KEYS
DJANGO_DEBUG true for local dev
DJANGO_SUPERUSER_* the login created automatically on first start

You can generate a decently secure key with: openssl rand -hex 32. Below is the example ENV file provided to you out of the box. Not all of these are required. For more advanced setups with the optional Ollama service, checkout the Configuration page.

# Copy to .env and fill in real values:  cp .env.example .env
# .env is gitignored and is read by both docker compose and the app container.

# --- Django ---------------------------------------------------------------
# Generate with:
#   python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'
DJANGO_SECRET_KEY=replace-me
# "true" enables debug mode. Leave false for anything shared/deployed.
DJANGO_DEBUG=false
# Required when DJANGO_DEBUG=false. Comma-separated, no spaces.
# The compose service name `web` is added automatically (the agent, worker and
# beat containers reach the web service by that name); override with
# REEF_INTERNAL_WEB_HOST if you rename the service.
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
# Any host/IP you open the UI on. Reaching it by LAN IP or hostname without
# adding it here gives a bare "Bad Request (400)" page.
#   e.g. DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,192.168.1.50,reef.example.com
#
# CSRF trust for POST actions (login, acknowledge, run scan, ...). Leave BLANK
# and every concrete host above is trusted automatically on http+https, bare and
# :8000. Set it explicitly (scheme required) only to override that:
#   e.g. DJANGO_CSRF_TRUSTED_ORIGINS=https://reef.example.com
DJANGO_CSRF_TRUSTED_ORIGINS=
# "true" when a TLS-terminating proxy / load balancer sits in front (it forwards
# plain http to the app). Makes Django trust X-Forwarded-Proto / -Host.
DJANGO_TRUST_PROXY=false

# --- Superuser -----------------------------------------------------------
# Created on first container start by entrypoint.sh (manage.py ensure_superuser).
DJANGO_SUPERUSER_USERNAME=admin
DJANGO_SUPERUSER_PASSWORD=replace-me
DJANGO_SUPERUSER_EMAIL=admin@example.com

# --- API keys ----------------------------------------------------------
# Accepted in the `X-API-Key` header by the token-protected API routes and by
# the agent. Comma-separated; supply several to allow rotation.
# Generate one with:  python -c 'import secrets; print(secrets.token_urlsafe(32))'
DJANGO_API_KEYS=

# --- Agent -------------------------------------------------------------
# The agent authenticates with this key; it MUST be one of DJANGO_API_KEYS.
REEF_AGENT_API_KEY=
# Optional; defaults to the monitored host's /etc/hostname (stable across restarts).
REEF_AGENT_NAME=
# Seconds between job polls / heartbeats.
REEF_POLL_INTERVAL=15

# --- Reef behaviour --------------------------------------------------
# Findings at or above this severity raise a notification (info|low|medium|high|critical).
REEF_NOTIFY_MIN_SEVERITY=high
# An agent with no check-in for this long is marked offline.
REEF_AGENT_OFFLINE_AFTER_SECONDS=300
# When "true", the auditd scan loads Reef's audit.rules into the host kernel
# (auditctl -R). Leave "false" to only *report* which rules are missing. To also
# persist the ruleset across reboots, add a read-write mount of /etc/audit to the
# agent service in docker-compose.yml.
REEF_AUDIT_MANAGE=false
# Size cap for the agent's scan scratch tmpfs (syft/grype temp files + the
# intermediate SBOM the grype scan builds). RAM-backed: size it to what the host
# can spare. A scan that needs more space fails cleanly with ENOSPC rather than
# filling the host disk. Grype's vuln DB lives on the grype-db volume, not here.
REEF_SCAN_TMP_SIZE=2g

# --- Celery ---------------------------------------------------------
# docker-compose sets these; override here if pointing at an external Redis.
# CELERY_BROKER_URL=redis://redis:6379/0
# CELERY_RESULT_BACKEND=redis://redis:6379/1

# --- SQLite3 ------------------------------------------------------
# Path inside the container (mounted volume so the DB survives rebuilds).
SQLITE_PATH=/data/db.sqlite3

# --- AI interpretation (optional, opt-in) -----------------------------
# Adds an "Interpret" button to the scan detail page and finding modals that
# sends scan/finding data to a local Ollama model and shows back a plain-
# language summary. Off unless OLLAMA_ENABLED=true. Needs real memory/CPU to
# run acceptably -- leave this off on a small box.
#
# 1. Uncomment the four lines below.
# 2. Start the bundled Ollama service too:
#      COMPOSE_PROFILES=ollama docker compose up -d
#    (or add `COMPOSE_PROFILES=ollama` as its own line in this file). The
#    `ollama-init` one-shot service pulls OLLAMA_MODEL on first start.
# OLLAMA_ENABLED=true
# OLLAMA_URL=http://ollama:11434
# OLLAMA_MODEL=llama3.2:3b
# OLLAMA_MEM_LIMIT=8g

2. Start everything

You will need to login to the PortIgniter registry using the email used when you purchased the license, and the password is the license itself. Attempting to pull images without doing the docker login will result in an error.

The Base Compose File

Below is the base compose file to get up and running on single host. You will need to modify this file for any agent containers running on other hosts. The ollama service is optional. I recommend leaving it off unless you have a pretty decent server. You also have the option of using another OLLAMA based endpoint.

name: portigniter-reef

x-app: &app
  # Check the product page for the latest release
  image: registry.portigniter.com/reef:2026.09.15
  env_file: .env
  environment:
    SQLITE_PATH: ${SQLITE_PATH:-/data/db.sqlite3}
    CELERY_BROKER_URL: redis://redis:6379/0
    CELERY_RESULT_BACKEND: redis://redis:6379/1
  volumes:
    - "app-data:/data"
  depends_on:
    redis:
      condition: service_healthy
  restart: unless-stopped

services:
  web:
    <<: *app
    command: ["web"]
    ports:
      - "8000:8000"
    healthcheck:
      # green once gunicorn is actually serving -- lets the agent wait for a
      # listening port instead of logging a startup "connection refused".
      test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/api/health/', timeout=3).status == 200 else 1)"]
      interval: 10s
      timeout: 5s
      retries: 6
      start_period: 40s

  worker:
    <<: *app
    command: ["worker"]
    depends_on:
      redis:
        condition: service_healthy
      web:
        condition: service_started

  beat:
    <<: *app
    command: ["beat"]
    depends_on:
      redis:
        condition: service_healthy
      web:
        condition: service_started

  redis:
    image: redis:7.4.10-alpine
    command: redis-server --appendonly yes  # Enable AOF
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    volumes:
      - redis-data:/data

  agent:
    image: registry.portigniter.com/reef-agent:2026.09.15
    env_file: .env
    environment:
      REEF_API_URL: http://web:8000
      REEF_SCAN_ROOT: /host
      # Persist grype's vulnerability DB (grype-db volume) so it isn't
      # re-downloaded and rebuilt (~1-2 GB) on every scan.
      GRYPE_DB_CACHE_DIR: /var/cache/grype
      # syft/grype scratch + our intermediate SBOM land here -- a size-capped
      # tmpfs (see below), so a runaway catalogue can't fill the host disk.
      TMPDIR: /scan-tmp
      # REEF_AUDIT_MANAGE (from .env): when true the auditd scan loads Reef's
      # ruleset into the host kernel. To also persist it across reboots, add a
      # read-write mount:  - "/etc/audit:/host/etc/audit:rw"
    # Privileged + host PID + host root are what let lynis / clamav / FIM see the
    # real system. The host filesystem is mounted read-only.
    privileged: true
    pid: "host"
    volumes:
      - "/:/host:ro"
      - "/var/log/audit:/var/log/audit:ro"
      - "clamav-db:/var/lib/clamav"
      - "grype-db:/var/cache/grype"
      - "agent-state:/var/lib/reef"
    # Hard cap on scan scratch space. It's RAM-backed: size it to what the host
    # can spare (REEF_SCAN_TMP_SIZE in .env). A scan that needs more fails
    # cleanly with ENOSPC instead of eating the disk. Swap to a named volume
    # here if you'd rather trade the disk risk back for not spending RAM.
    tmpfs:
      - "/scan-tmp:size=${REEF_SCAN_TMP_SIZE:-2g},mode=1777"
    depends_on:
      web:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      # green once the poll loop has written its heartbeat in the last 2 min
      test: ["CMD", "python3", "-c", "import os,time,sys; f='/tmp/reef-agent.heartbeat'; sys.exit(0 if os.path.exists(f) and time.time()-os.path.getmtime(f) < 180 else 1)"]
      interval: 20s
      timeout: 5s
      retries: 3
      start_period: 60s

  # Local LLM for the opt-in "Interpret" feature (scan/finding AI summaries).
  # Off by default -- only started with `docker compose --profile ollama up`
  # (or COMPOSE_PROFILES=ollama in .env). Also set OLLAMA_ENABLED=true plus
  # OLLAMA_URL/OLLAMA_MODEL in .env so the web/worker containers pick it up;
  # see .env.example.
  ollama:
    image: ollama/ollama:latest
    profiles: ["ollama"]
    environment:
      OLLAMA_HOST: 0.0.0.0
    volumes:
      - ollama-data:/root/.ollama
    # Small model (~2GB) -- still give the container a memory ceiling so a
    # customer on a modest box can't have it pushed into OOM-killing something
    # else. Override via OLLAMA_MEM_LIMIT in .env.
    mem_limit: ${OLLAMA_MEM_LIMIT:-8g}
    healthcheck:
      test: ["CMD", "ollama", "list"]
      interval: 10s
      timeout: 5s
      retries: 6
      start_period: 20s
    restart: unless-stopped

  # One-shot: pulls the model into the `ollama-data` volume, then exits.
  # Re-running compose is a no-op once the model is already pulled.
  ollama-init:
    image: ollama/ollama:latest
    profiles: ["ollama"]
    environment:
      OLLAMA_HOST: http://ollama:11434
    entrypoint: ["ollama", "pull", "${OLLAMA_MODEL:-llama3.2:3b}"]
    depends_on:
      ollama:
        condition: service_healthy
    restart: "no"

volumes:
  app-data: {}
  redis-data: {}
  clamav-db: {}
  grype-db: {}
  agent-state: {}
  ollama-data: {}

The Agent only Compose File

Use the below for any stand-alone hosts you only want to install the agent on.

name: portigniter-reef

services:
  agent:
    image: registry.portigniter.com/reef-agent:2026.09.15
    env_file: .env
    environment:
      REEF_API_URL: https://reef.yourdomain.com
      REEF_SCAN_ROOT: /host
      # Persist grype's vulnerability DB (grype-db volume) so it isn't
      # re-downloaded and rebuilt (~1-2 GB) on every scan.
      GRYPE_DB_CACHE_DIR: /var/cache/grype
      # syft/grype scratch + our intermediate SBOM land here -- a size-capped
      # tmpfs (see below), so a runaway catalogue can't fill the host disk.
      TMPDIR: /scan-tmp
      # REEF_AUDIT_MANAGE (from .env): when true the auditd scan loads Reef's
      # ruleset into the host kernel. To also persist it across reboots, add a
      # read-write mount:  - "/etc/audit:/host/etc/audit:rw"
    # Privileged + host PID + host root are what let lynis / clamav / FIM see the
    # real system. The host filesystem is mounted read-only.
    privileged: true
    pid: "host"
    volumes:
      - "/:/host:ro"
      - "/var/log/audit:/var/log/audit:ro"
      - "clamav-db:/var/lib/clamav"
      - "grype-db:/var/cache/grype"
      - "agent-state:/var/lib/reef"
    # Hard cap on scan scratch space. It's RAM-backed: size it to what the host
    # can spare (REEF_SCAN_TMP_SIZE in .env). A scan that needs more fails
    # cleanly with ENOSPC instead of eating the disk. Swap to a named volume
    # here if you'd rather trade the disk risk back for not spending RAM.
    tmpfs:
      - "/scan-tmp:size=${REEF_SCAN_TMP_SIZE:-2g},mode=1777"
    restart: unless-stopped
    healthcheck:
      # green once the poll loop has written its heartbeat in the last 2 min
      test: ["CMD", "python3", "-c", "import os,time,sys; f='/tmp/reef-agent.heartbeat'; sys.exit(0 if os.path.exists(f) and time.time()-os.path.getmtime(f) < 180 else 1)"]
      interval: 20s
      timeout: 5s
      retries: 3
      start_period: 60s

volumes:
  clamav-db: {}
  grype-db: {}
  agent-state: {}

The agent only .env looks as follows:

# --- Agent -------------------------------------------------------------
# The agent authenticates with this key; it MUST be one of DJANGO_API_KEYS.
REEF_AGENT_API_KEY=somelongkeyhere
# Optional; defaults to the monitored host's /etc/hostname.
REEF_AGENT_NAME=
# Seconds between job polls / heartbeats.
REEF_POLL_INTERVAL=15

# --- Reef behaviour --------------------------------------------------
REEF_NOTIFY_MIN_SEVERITY=high
REEF_AGENT_OFFLINE_AFTER_SECONDS=300
# "true" => auditd scan loads Reef's audit.rules into the host kernel.
REEF_AUDIT_MANAGE=false
# Size cap for the agent's scan scratch tmpfs (syft/grype temp + intermediate
# SBOM). RAM-backed; a scan needing more fails cleanly instead of filling disk.
REEF_SCAN_TMP_SIZE=2g

Ensure your docker-compose.yml file is in the same directory you put your .env file for both the all in one and any agent only installs.

# Starting the stack without Ollama
docker compose up --build
# Starting the stack with Ollama
docker compose up -d --profile ollama

On first start, the web service runs migrations, creates the superuser from DJANGO_SUPERUSER_*, and seeds the default scan schedules (manage.py reef_seed — nightly file integrity monitoring, hourly auditd, weekly Lynis, daily ClamAV/YARA/Grype, weekly OpenSCAP). You can edit or add to these later at /admin/scanning/scanschedule/.

3. Log in and confirm the agent checked in

Open http://localhost:8000/ or the domain name, if using a forward-facing proxy, and you should be greeted with the below login page.

Reef Login Page

Here, you can log in as the superuser, as set in your ENV. On first login, you will land on the Reef Dashboard.

Reef Dashboard

Click on Agents in the Navigation menu to see the host agent. Within 15 seconds or so, your configured agent(s) should start showing up as online — that’s the check-in loop working. This is the result of a background POST /api/agent/checkin/ happening every REEF_POLL_INTERVAL seconds. If you’re deploying for the first time, you will only see the agent deployed on the same box as the web interface is. In the example below, is a setup with three separate agents.

Reef Agents

4. Run your first scan

Runing a quick Lynis scan is the easiest smoke test. The job detail page auto-refreshes until it flips to done; findings land on /findings/ and the job summary shows the package count and detected distro. A high/critical finding raises a notification — bell badge, /notifications/, and an email copy in the web container logs (console email backend in dev).

5. See File Integrity Monitoring detect a change

Next let’s do: Run scan → File Integrity twice:

  1. The first run establishes the baseline and reports no findings.
  2. touch a file under /etc on the host, then run it again — you should see a finding for the changed file.

6. Let the schedules run

Leave the stack running for a minute or two and beat will enqueue the seeded schedules automatically — no action needed.

Next

  • Architecture for how the services relate to each other
  • Scans for what each of the seven scan types actually checks
  • Management Commands for reef_seed, reef_reset, and the other manage.py commands
Top