Skip to content

Production stack

  • A Linux host with Docker Engine and the Docker Compose plugin.
  • Ports 80 and 443 reachable by your users (or any other port, set in .env).
  • For Let’s Encrypt certificates: a DNS name pointing to the host, and ports 80 and 443 reachable from the Internet.

Auditix is published as three images (frontend, backend, updater) on the Auditix registry, rg.fr-par.scw.cloud/auditix, always deployed with the same tag. The compose file adds the services they rely on:

Service Role
gateway The only published service: serves Auditix on HTTP and HTTPS.
frontend The web interface.
backend The application. Applies database changes itself when it starts.
updater Applies the updates started from Settings › Platform › Update. Optional.
orchestrator Starts and stops the worker containers that run the background work.
developer The reference of the external API, at /developer.
logs Forwards the logs to the syslog servers of Settings › Platform › Logs.
rabbitmq Queues of the background work.
mercure Real-time updates of the interface.
database PostgreSQL: every piece of data lives here.

The updater and orchestrator services have access to Docker. They only ever act on the containers of their own installation (the compose project deployed from the same folder), never on other containers of the host.

  1. Log in to the registry (skip this step if the registry is public), with a Scaleway API secret key allowed to read the registry:

    Terminal window
    docker login rg.fr-par.scw.cloud/auditix -u nologin
    # Password: your Scaleway API secret key
  2. Create the installation folder:

    Terminal window
    mkdir -p /opt/auditix && cd /opt/auditix
  3. Create compose.yaml with the content below.

  4. Create .env, readable by root only (chmod 600 .env), and fill each secret once with openssl rand -hex 32:

    Terminal window
    # "latest": updated from Settings › Platform › Update. Or a fixed release (vX.Y.Z).
    AUDITIX_VERSION=latest
    HTTP_PORT=80
    HTTPS_PORT=443
    # Generate each once with: openssl rand -hex 32
    APP_SECRET=
    POSTGRES_PASSWORD=
    MERCURE_JWT_SECRET=
    RABBITMQ_PASSWORD=
    GATEWAY_SECRET=
    UPDATER_SECRET=
  5. Start:

    Terminal window
    docker compose pull
    docker compose up -d
    docker compose ps # every service should be "healthy"
  6. Sign in at http://<host> with the default administrator, admin / password. Auditix asks for a new password before anything else: do it right away, before exposing the service.

name: auditix
# Every service hands its output to Docker's syslog driver, towards the log collector, which
# forwards it to the syslog servers of Settings › Platform › Logs.
x-logging: &logging
driver: syslog
options:
syslog-address: udp://127.0.0.1:15013
syslog-format: rfc5424
tag: "{{.Name}}"
services:
# The only published service. HTTPS and the certificate are set in Settings › Platform › Web
# access; the gateway reads them from the backend.
gateway:
logging: *logging
image: traefik:v3.7
command:
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.web.forwardedHeaders.trustedIPs=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1/32
- --entrypoints.websecure.forwardedHeaders.trustedIPs=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1/32
- --providers.http.endpoint=http://backend:8080/internal/gateway
- --providers.http.pollInterval=5s
- --providers.http.headers.X-Gateway-Secret=${GATEWAY_SECRET:?Set GATEWAY_SECRET in .env}
- --certificatesresolvers.letsencrypt.acme.storage=/data/acme.json
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
- --certificatesresolvers.letsencrypt-staging.acme.storage=/data/acme-staging.json
- --certificatesresolvers.letsencrypt-staging.acme.httpchallenge.entrypoint=web
- --certificatesresolvers.letsencrypt-staging.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
- --ping=true
- --global.checkNewVersion=false
- --global.sendAnonymousUsage=false
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
volumes:
- gateway:/data
healthcheck:
test: ["CMD", "traefik", "healthcheck", "--ping"]
interval: 10s
timeout: 3s
retries: 6
depends_on:
backend:
condition: service_healthy
restart: unless-stopped
frontend:
logging: *logging
image: rg.fr-par.scw.cloud/auditix/frontend:${AUDITIX_VERSION:-latest}
environment:
BACKEND_URL: http://backend:8080
depends_on:
backend:
condition: service_healthy
restart: unless-stopped
backend:
logging: *logging
image: rg.fr-par.scw.cloud/auditix/backend:${AUDITIX_VERSION:-latest}
environment: &backend-environment
APP_SECRET: ${APP_SECRET:?Set APP_SECRET in .env}
DATABASE_URL: postgresql://auditix:${POSTGRES_PASSWORD}@database:5432/auditix?serverVersion=18&charset=utf8
MERCURE_URL: http://mercure/.well-known/mercure
MERCURE_JWT_SECRET: ${MERCURE_JWT_SECRET:?Set MERCURE_JWT_SECRET in .env}
MESSENGER_TRANSPORT_DSN: amqp://auditix:${RABBITMQ_PASSWORD:?Set RABBITMQ_PASSWORD in .env}@rabbitmq:5672/%2f
GATEWAY_SECRET: ${GATEWAY_SECRET}
UPDATER_SECRET: ${UPDATER_SECRET:-}
LOGS_COLLECTOR_HOST: logs
# Never published: the internal API is only for the frontend; integrations use the
# external API (/api/v1) through the gateway.
volumes:
- logs-config:/app/var/logs-collector
depends_on:
database:
condition: service_healthy
rabbitmq:
condition: service_healthy
restart: unless-stopped
# Applies the updates started from Settings › Platform › Update. The only service with access to
# Docker: it only acts on the services of this file (its own compose project), never on other
# containers of the host. Optional: without it, update by hand (see Update below).
updater:
logging: *logging
image: rg.fr-par.scw.cloud/auditix/updater:${AUDITIX_VERSION:-latest}
environment:
BACKEND_URL: http://backend:8080
UPDATER_SECRET: ${UPDATER_SECRET:?Set UPDATER_SECRET in .env}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# The installation directory: the backups taken before updates in backups/, and its .env
# when AUDITIX_VERSION is a fixed release.
- .:/install
depends_on:
backend:
condition: service_healthy
restart: unless-stopped
# Runs the background work: starts and stops the worker containers (the backend image, started
# with the "worker" command) as the pools of Settings › Platform › Workers ask, a fixed number or
# following the backlog of their queues. Same image as the updater; it only acts on the worker
# containers it creates for this installation. Without it, no worker runs (see Scale the workers).
orchestrator:
logging: *logging
image: rg.fr-par.scw.cloud/auditix/updater:${AUDITIX_VERSION:-latest}
entrypoint: ["auditix-orchestrator"]
environment:
BACKEND_URL: http://backend:8080
UPDATER_SECRET: ${UPDATER_SECRET:?Set UPDATER_SECRET in .env}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
backend:
condition: service_healthy
restart: unless-stopped
# The developer portal: the reference of the external API, at /developer.
developer:
logging: *logging
image: scalarapi/api-reference:0.6.12
environment:
BASE_PATH: /developer/scalar
API_REFERENCE_CONFIG: '{"sources":[{"url":"/api/v1/openapi.json","title":"Auditix API"}],"theme":"none","withDefaultFonts":false,"customCss":"@import url(\"/theme/developer\");","favicon":"/icon.svg","metaData":{"title":"Auditix API"},"defaultHttpClient":{"targetKey":"shell","clientKey":"curl"},"showDeveloperTools":"never","agent":{"disabled":true},"mcp":{"disabled":true}}'
restart: unless-stopped
# The log collector: forwards the logs to the syslog servers of the settings.
logs:
image: timberio/vector:0.59.0-alpine
command: ["--config-dir", "/etc/vector/auditix", "--watch-config"]
ports:
- "127.0.0.1:15013:5514/udp"
volumes:
- logs-config:/etc/vector/auditix:ro
depends_on:
backend:
condition: service_healthy
restart: unless-stopped
# Queues of the background work.
rabbitmq:
logging: *logging
image: rabbitmq:4.3-management-alpine
hostname: rabbitmq
environment:
RABBITMQ_DEFAULT_USER: auditix
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
volumes:
- rabbitmq:/var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 10s
timeout: 10s
retries: 12
restart: unless-stopped
# Real-time updates. Not published: the browsers reach it through the gateway.
mercure:
logging: *logging
image: dunglas/mercure:v1.1
environment:
SERVER_NAME: ":80"
MERCURE_TRUSTED_ISSUERS: auditix
MERCURE_PUBLISHER_JWT_KEY: ${MERCURE_JWT_SECRET}
MERCURE_PUBLISHER_JWT_ALG: HS256
MERCURE_SUBSCRIBER_JWT_KEY: ${MERCURE_JWT_SECRET}
MERCURE_SUBSCRIBER_JWT_ALG: HS256
MERCURE_EXTRA_DIRECTIVES: |
resource_identifier http://mercure/.well-known/mercure
cookie_name auditix_live
transport bolt {
path /data/mercure.db
size 1000
cleanup_frequency 0.3
}
heartbeat 15s
volumes:
- mercure:/data
restart: unless-stopped
database:
logging: *logging
image: postgres:18-alpine
environment:
POSTGRES_DB: auditix
POSTGRES_USER: auditix
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
volumes:
- database:/var/lib/postgresql
healthcheck:
test: ["CMD", "pg_isready", "-U", "auditix", "-d", "auditix"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
volumes:
database:
gateway:
logs-config:
mercure:
rabbitmq:
  1. In Settings › Platform › General, set the Instance name, the Time zone, the Default language and, in Appearance, the Default theme and Main colour (see Settings).
  2. Activate the licence.
  3. Set up HTTPS.
  4. Create the tenants and the accounts.

Accounts can also be created from the command line (with Portainer, from the console of the backend container):

Terminal window
docker compose exec backend bin/console auditix:user:create jane@example.com \
--first-name=Jane --last-name=Doe --platform-admin --tenant="My company"
  1. Stacks › Add stack › Web editor: paste compose.yaml.
  2. Put the variables of .env in the environment variables of the stack, with AUDITIX_VERSION=latest.
  3. In the updater service, remove the .:/install volume: Portainer keeps the files of a stack in its own volume. The backups taken before updates then go to a Docker volume named <stack>_backups, created by the updater.

Nothing else is needed: updates never change the stack, and the workers are started by the orchestrator service, outside the stack.

In Settings › Platform › Web access, give the Host name of the instance (its DNS must point to the server), then choose the Certificate:

  • Let’s Encrypt: free, issued and renewed automatically. Ports 80 and 443 must be reachable from the Internet. Let’s Encrypt (test) checks the setup first, with certificates browsers do not trust, without hitting the limits of Let’s Encrypt.
  • My own certificate: paste the Certificate (PEM) (with its intermediate certificates) and the Private key (PEM). The key is stored encrypted and never shown again.
  • No HTTPS (HTTP only): for a test, or behind a reverse proxy that handles HTTPS.

Then switch on Redirect HTTP to HTTPS and, once HTTPS works, Strict HTTPS (HSTS). Changes apply within a few seconds, without restart. If a wrong setting makes Auditix unreachable over HTTPS, switch HTTPS off from the server:

Terminal window
docker compose exec backend bin/console auditix:web:reset

Behind a reverse proxy that already handles HTTPS, leave HTTPS off and forward to the HTTP port of the gateway with the X-Forwarded-Proto and X-Forwarded-For headers.

The orchestrator service starts the worker containers that do the background work (scheduled tasks, checks, imports…), as set in Settings › Platform › Workers › Pools: a fixed number of containers, or Automatic scaling with the number of messages waiting. The default pool runs 2 to 4 containers. Without the orchestrator, the backend still runs the scheduled tasks every five minutes, but nothing else is processed in the background.

The worker containers are not part of the compose project, so docker compose down does not remove them. To take the installation down completely (auditix being the project name):

Terminal window
docker compose stop orchestrator
docker rm -f $(docker ps -aq --filter label=io.auditix.orchestrator.project=auditix)
docker compose down

All data lives in PostgreSQL. Back it up regularly, and always before an update by hand:

Terminal window
docker compose exec -T database pg_dump -U auditix -Fc auditix > auditix-$(date +%F).dump

Restore into an empty database:

Terminal window
docker compose exec -T database pg_restore -U auditix -d auditix --clean --if-exists < auditix-YYYY-MM-DD.dump

When no administrator can sign in, set a new password from the server:

Terminal window
docker compose exec backend bin/console auditix:user:password admin@example.com