There are three independent choices, and the defaults are right for most people. This page exists so you can tell when they stop being right.

ChoiceDefaultChange it when
Databasebundled PostgreSQLyou already run PostgreSQL, or you need backups and failover you trust
Topologyone container (APP_MODE=all)ingestion is heavy enough to slow the console
PlatformDocker Composeyou already run Kubernetes

They combine freely: bundled or external, combined or split, on either platform. Every combination reads the same environment variables, so moving between them is a deployment change, not a reconfiguration.

Database: bundled or your own

The quick start bundles PostgreSQL because it makes the first run one command. It is a single container with a named volume — no replication, no backups, no connection pooling.

That is genuinely fine for a small internal deployment, as long as you are taking backups. Point at your own PostgreSQL when you want someone else’s backup and failover story, or when your organisation already has one.

curl -fsSL -O https://raw.githubusercontent.com/dmarc-analyzer-net/DmarcAnalyzerApp/main/deploy/compose.external-db.yml
# .env
DMARC_DB_HOST=db.internal
DMARC_DB_PORT=5432
DMARC_DB_NAME=dmarc_analyzer
DMARC_DB_USER=dmarc
DMARC_DB_PASSWORD=
COMPOSE_FILE=compose.yml:compose.external-db.yml

The database must exist and the user must own it — the app creates its tables on first boot, but not the database itself.

Managed PostgreSQL

RDS, Cloud SQL, Neon and similar all work — the app is an ordinary Npgsql client and uses no exotic SQL. Releases are tested against PostgreSQL 17, which is what the bundled container runs; older majors are very likely fine but are not something we test, so treat 17 as the known-good version.

Two things to get right:

  • TLS. Most managed providers require it. Set the connection string directly rather than assembling it from the DMARC_DB_* pieces:

    ConnectionStrings__Default=Host=db.example.com;Port=5432;Database=dmarc_analyzer;Username=dmarc;Password=;SSL Mode=Require

    Npgsql spells it SSL Mode, not sslmode.

  • Connection pooling — use session pooling, not transaction pooling. The worker holds a PostgreSQL advisory lock for the life of the process to guarantee only one worker runs. Advisory locks belong to a session, so a pooler that hands the connection to someone else between statements makes that guarantee meaningless. With PgBouncer, pool_mode = session.

Nothing else changes: migrations, retention and the worker behave identically.

Topology: one container or two

By default one container serves the console and runs the mailbox polling loop. One container, one log stream, nothing to order at startup.

Split them when a sync pass is heavy enough to compete with the console for CPU, when you want to restart one without the other, or when they need different resource limits.

curl -fsSL -O https://raw.githubusercontent.com/dmarc-analyzer-net/DmarcAnalyzerApp/main/deploy/compose.split.yml
echo "COMPOSE_FILE=compose.yml:compose.split.yml" >> .env
docker compose up -d

The overlay flips the app to APP_MODE=api in the same file that adds the worker, which is deliberate. Running a worker beside a container that is still in all mode would mean two schedulers polling the same mailboxes.

Only one worker, whichever shape you pick. The application takes a PostgreSQL advisory lock at startup and a second worker exits rather than starting, so --scale worker=2 cannot quietly double-poll your mailboxes. Scale the console instead — APP_MODE=api containers take no lock.

Platform: Compose or Kubernetes

Compose is the recommended path for a single host, and most self-hosted DMARC Analyzer installs are a single host.

The Helm chart exposes the same two choices as values — postgres.enabled and mode: combined | split — so a deployment can move between Compose and Kubernetes without relearning its configuration. Use it if you already run Kubernetes; it is not a reason to start.

Overlay reference

The overlays need Docker Compose v2.24 or newer (docker compose version).

WantFiles
One container, bundled database (default)compose.yml
One container, your databasecompose.yml compose.external-db.yml
Console and worker split, bundled databasecompose.yml compose.split.yml
Console and worker split, your databaseall three

Set COMPOSE_FILE in .env once and day-to-day use stays docker compose up -d.

Sizing

Modest, and it does not grow the way people expect — aggregate reports are statistical summaries, not copies of your mail.

  • ~1 GB RAM for the whole stack. A combined container idles well under 200 MB.
  • Disk is dominated by report records. A busy domain produces a few MB a month; a hundred domains for two years still measures in gigabytes, and retention trims the tail.
  • CPU matters only during a sync pass, which is why splitting helps once you have many mailboxes.