Skip to content

Docker

Published Images

Each version is published for linux/amd64 and linux/arm64 on the GitHub Container Registry and Docker Hub. The same image works with SQLite and PostgreSQL: it includes the PostgreSQL driver.

docker pull ghcr.io/cyr-ius/security-review:latest   # or docker.io/cyrius44/security-review
Tag Content
X.Y.Z, X.Y, X, latest Version X.Y.Z (the latest stable version for latest)
X.Y.Z-pg, X.Y-pg, X-pg, latest-pg Deprecated: aliases of the same image, kept until the next major version (former PostgreSQL variant)
X.Y.ZrcN (a, b, rc) Pre-release: applies only to this specific tag; never used for latest

The images include an SBOM and a provenance attestation (accessible via docker buildx imagetools inspect --format '{{ json .Provenance }}' IMAGE).

Docker Compose with PostgreSQL

One file, examples/docker/docker-compose.yml, starts everything with the published image: PostgreSQL, the migration of the schema, the controller and the MCP server. The commands that download it and start it are in Deploying the server.

The command printf writes the .env file, readable by you only: the password of PostgreSQL and the secret of the web sessions, generated at random. docker compose up -d --wait returns once every service is ready. The web interface answers on http://127.0.0.1:8080/ui/, the MCP endpoint on http://127.0.0.1:8000/mcp.

Content of the files of version v5.0.0:

# security-review with PostgreSQL, ready to use: download this file in an empty folder,
# create the .env file next to it, then start everything.
#
#   (umask 077 && printf 'POSTGRES_PASSWORD=%s\nSECREVIEW_SECRET_KEY=%s\n' \
#       "$(openssl rand -hex 24)" "$(openssl rand -hex 32)" > .env)
#   docker compose up -d --wait
#   docker compose exec controller security-review-controller user reset-password admin
#
# Web interface: http://127.0.0.1:8080/ui/   MCP endpoint: http://127.0.0.1:8000/mcp
#
# Three services:
#   db          PostgreSQL (official image), reachable by the other services only
#   controller  web interface and REST API (for people); creates or updates the tables at start
#   mcp         MCP server (for the AI agents and the CI), started once the controller is ready
#
# The two ports are published on every interface of the server, for the team: enable TLS (the
# commented TLS lines of controller and mcp) or put a reverse proxy in front (see the
# documentation, page Docker), plain HTTP exposes the tokens.
# To keep a port on this machine only, prefix it with 127.0.0.1 ("127.0.0.1:8000:8000").
# Inside their containers, the servers always listen on every interface (set by the image):
# only the ports choose the exposure.

name: security-review

services:
  db:
    image: postgres:18-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: secreview
      POSTGRES_USER: secreview
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      # Since PostgreSQL 18, the data is kept under /var/lib/postgresql/18/docker
      - secreview-pg:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U secreview -d secreview"]
      interval: 10s
      timeout: 5s
      retries: 5
    security_opt:
      - no-new-privileges:true

  controller:
    image: ghcr.io/cyr-ius/security-review:latest
    restart: unless-stopped
    entrypoint: ["security-review-controller"]
    command: ["serve"]
    environment:
      SECREVIEW_DATABASE_URL: postgresql+psycopg2://secreview:${POSTGRES_PASSWORD}@db:5432/secreview
      # Signs the web sessions: keep the same value, or every session ends at restart
      SECREVIEW_SECRET_KEY: ${SECREVIEW_SECRET_KEY}
      # Daily download of the OSV vulnerability advisories (needs Internet access)
      # SECREVIEW_OSV_AUTO_UPDATE: "true"
      # SECREVIEW_OSV_ECOSYSTEMS: PyPI,npm,Maven
      # SECREVIEW_TLS_CERTFILE: /etc/secreview/tls/cert.pem
      # SECREVIEW_TLS_KEYFILE: /etc/secreview/tls/key.pem
    # volumes:
    #   - ./tls:/etc/secreview/tls:ro
    ports:
      - "8080:8000"
    depends_on:
      db:
        condition: service_healthy
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true

  mcp:
    image: ghcr.io/cyr-ius/security-review:latest
    restart: unless-stopped
    # The image runs `security-review-mcp serve`: streamable HTTP on /mcp
    environment:
      SECREVIEW_DATABASE_URL: postgresql+psycopg2://secreview:${POSTGRES_PASSWORD}@db:5432/secreview
      # SECREVIEW_TLS_CERTFILE: /etc/secreview/tls/cert.pem
      # SECREVIEW_TLS_KEYFILE: /etc/secreview/tls/key.pem
    # volumes:
    #   - ./tls:/etc/secreview/tls:ro
    ports:
      - "8000:8000"
    depends_on:
      # The MCP server only checks the tables: it starts once the controller has migrated them
      controller:
        condition: service_healthy
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true

volumes:
  secreview-pg:
POSTGRES_PASSWORD=<48 hexadecimal characters>
SECREVIEW_SECRET_KEY=<64 hexadecimal characters>
Service Role Listens on
db PostgreSQL, reachable by the other services only —
controller Web interface (/ui/) and REST API, for people; creates or updates the tables at start 8080
mcp MCP server (/mcp) and public enrollment, for the agents and the CI 8000

Good to know:

  • Order of start-up. controller waits until PostgreSQL is ready, then creates or updates the tables; mcp, which only checks them, waits until controller is healthy. After an image update, docker compose up -d is enough: the controller applies the new migrations.
  • Keep .env. It holds the password of the database and the secret of the sessions; never commit it. The password is inserted as is in the connection URL: keep hexadecimal characters, as generated above.
  • Open to the team. The ports are published on every interface of the server, in plain HTTP: enable the built-in TLS or put a reverse proxy in front before the team connects. To keep a port on the server only (behind a reverse proxy on the same machine, for example), prefix it with 127.0.0.1 ("127.0.0.1:8000:8000"). Only the ports choose the exposure: inside its container, each process listens on every interface (SECREVIEW_HOST=0.0.0.0, set by the image, not to be changed). For the built-in TLS, uncomment in controller and mcp the TLS variables (SECREVIEW_TLS_CERTFILE, SECREVIEW_TLS_KEYFILE) and the volumes that mounts the certificate files read-only from ./tls (see Certificate Authority). The controller exposes no write route without a token: it can stay internal.
  • Vulnerability advisories. The OSV database is empty at the first start: the dependency analysis answers that no database is loaded. To let the controller download it at start-up, then every 24 hours, uncomment SECREVIEW_OSV_AUTO_UPDATE (and SECREVIEW_OSV_ECOSYSTEMS) in the controller service, then run docker compose up -d. Follow the import with docker compose logs -f controller. Without Internet access, import an archive with advisories import (see Dependencies).
  • Hardening. The containers of security-review-mcp run with a read-only file system and without any Linux capability.
  • Data. The secreview-pg volume holds the reports, the accounts and the tokens: back it up (see Backing up PostgreSQL). It is mounted on /var/lib/postgresql, where PostgreSQL 18 keeps its data; a volume created by PostgreSQL 17 does not start with this image (see Upgrading PostgreSQL).

The administration commands run in the controller container:

docker compose exec controller security-review-controller token list

Without Docker Compose

The same image runs each process in its own container, on an existing PostgreSQL database (see Architecture):

  • the MCP server is the default entry point of the image (security-review-mcp, default command serve, in streamable HTTP; add --transport stdio for the local mode);
  • the controller (REST API, web interface, administration) is started with --entrypoint security-review-controller, with the built-in tokens and TLS or behind a reverse proxy that handles them.
DB=postgresql+psycopg2://user:pass@db:5432/secreview

# 1. Controller: web interface and API, preferably on an internal network; creates or
#    updates the schema (Alembic) at start, so start it first
docker run -d --name secreview-controller -p 127.0.0.1:8080:8000 \
    -e SECREVIEW_DATABASE_URL=$DB \
    --entrypoint security-review-controller \
    ghcr.io/cyr-ius/security-review serve

# 2. MCP server: exposed to the agents and the CI
docker run -d --name secreview-mcp -p 8443:8443 \
    -e SECREVIEW_DATABASE_URL=$DB \
    -v ./tls:/etc/secreview/tls:ro \
    -e SECREVIEW_TLS_CERTFILE=/etc/secreview/tls/cert.pem \
    -e SECREVIEW_TLS_KEYFILE=/etc/secreview/tls/key.pem \
    ghcr.io/cyr-ius/security-review serve --port 8443

# 3. Password of the admin account, created at the first start of the controller
docker exec secreview-controller security-review-controller user reset-password admin

In the image, both processes listen on every interface of the container (SECREVIEW_HOST=0.0.0.0), port 8000 unless --port is given: the -p option chooses the address and the port of the host (-p 127.0.0.1:8080:8000: this machine only). Mounted files must be readable by the user of the container (UID 10001). The MCP server refuses to start while the schema is not migrated (step 1).

Backing up PostgreSQL

The secreview-pg volume holds the reports, the accounts and the token digests. Export it regularly (cron, systemd timer) and keep the files away from the server, readable by you only:

(umask 077 && docker compose exec -T db pg_dump -U secreview -d secreview -Fc > ~/secreview-$(date +%F).dump)
# Restore an export (the services are stopped during the restore)
docker compose stop mcp controller
docker compose exec -T db pg_restore -U secreview -d secreview --clean --if-exists --no-owner < ~/secreview-2026-10-25.dump
docker compose start controller mcp

Upgrading PostgreSQL

A major version of PostgreSQL cannot read data files created by a previous version. To upgrade a secreview-pg volume from PostgreSQL 17 to 18, export the database using the old image, recreate the volume, and then restore the export:

# 1. Using the previous Compose file (postgres:17-alpine, mounted at /var/lib/postgresql/data)
docker compose stop mcp controller
(umask 077 && docker compose exec -T db pg_dump -U secreview -d secreview -Fc > ~/secreview.dump)
docker compose down

# 2. Verify the export (it should list the tables), then recreate the volume: the export
#    is now the only copy of your data
docker compose run --rm -T --no-deps --entrypoint pg_restore db --list < ~/secreview.dump | grep 'TABLE DATA'
docker volume rm security-review-mcp_secreview-pg

# 3. Using the current Compose file (postgres:18-alpine, mounted at /var/lib/postgresql)
docker compose up -d db
docker compose exec -T db pg_restore -U secreview -d secreview --clean --if-exists --no-owner < ~/secreview.dump
docker compose up -d

The controller then applies any pending Alembic revisions at start. The export contains reports, password hashes, and tokens; keep it outside the repository (ensuring it is readable only by you, using umask 077) and delete it once the restoration has been verified.

Internal Rules and CLI

Rules are stored in the database; you can import a directory of TOML packs using the controller CLI (running servers will apply them within seconds):

docker run --rm -v secreview-data:/var/lib/secreview -v ./rules.d:/rules:ro \
--entrypoint security-review-controller ghcr.io/cyr-ius/security-review rules import /rules

Other commands can be executed in the same way:

docker run --rm -v secreview-data:/var/lib/secreview \
--entrypoint security-review-controller ghcr.io/cyr-ius/security-review token list