Skip to content

Deploying with Portainer

BJD1997 edited this page Oct 2, 2026 · 3 revisions

Deploying with Portainer Stacks

If you manage Docker through Portainer, use the Portainer-tailored compose file — docker-compose.portainer.yml — with the Web editor method described below, instead of the plain Docker Compose CLI path. It's fully self-contained: it pulls the published image (no build) and embeds the two files the CLI compose bind-mounts (the Postgres init script for the RLS role, and the resolver config) directly in the YAML, so there's nothing to clone.

⚠️ Use the Web editor, not the Repository method

With a Repository stack, Portainer only reads a stack.env file committed in the repo — the variables you type into Portainer's UI are silently ignored, so ${...} placeholders fall back to their defaults. This is the single most common cause of "I set API_HOST_PORT but it's still trying to bind :8000" reports. The Web editor builds the stack's environment from the variables you actually set in Portainer, so they apply correctly.

1. Create the stack

Stacks → Add stack → Web editor, and paste the contents of docker-compose.portainer.yml.

2. Set environment variables

Under Environment variables, add the settings below — they fill the ${...} placeholders in the compose file, the same options documented inline in .env.example and in the Configuration Reference:

Variable Required Notes
FERNET_KEY Yes Encrypts TOTP secrets & stored credentials at rest. Generate: openssl rand -base64 32 | tr '+/' '-_'
PUBLIC_BASE_URL Yes Public HTTPS URL your reverse proxy serves this at, e.g. https://dmarc.yourdomain.com
POSTGRES_PASSWORD Yes Any strong password
DMARC_APP_DB_PASSWORD Yes A second, different strong password
PLATFORM_ADMIN_BOOTSTRAP_EMAIL / PLATFORM_ADMIN_BOOTSTRAP_PASSWORD Recommended Your first admin login (a no-op once an admin exists — change the password after first sign-in)
FORWARDED_ALLOW_IPS Recommended Your reverse proxy's IP, so real client IPs (not the proxy's) get logged
IMAGE_TAG Optional Image tag to run (default latest); pin to a release tag like v0.1.4 if you prefer
API_HOST_PORT Optional Host port to publish (default 8000); change if 8000 is taken
ENTRA_MAIL_CLIENT_ID / ENTRA_MAIL_CLIENT_SECRET Optional Report ingestion via Microsoft Graph — leave unset for DNS-checks-only
ENTRA_SSO_CLIENT_ID / ENTRA_SSO_CLIENT_SECRET, CLOUDFLARE_*, HOSTED_REPORTS_*, SECURITY_CONTACT_EMAIL, MTA_STS_POLICY_* Optional Feature-gated — leave unset to keep the feature off

3. Deploy the stack

migrate runs first (schema migrations + bootstrapping the admin account), then api and worker start. If :8000 is already in use on the host (e.g. another instance), set API_HOST_PORT to a free port. Point your reverse proxy's upstream at this host's :8000 (or your API_HOST_PORT).

Updating

By hand: open the stack, change IMAGE_TAG under Environment variables to the new version, and click Update the stack with Re-pull image on. That runs the database migrations and restarts the app.

With Update now (from v0.2.0): the admin console's Update now button can do the same through Portainer's API, so Portainer stays in charge of the stack. The stack's updater container does the asking; the app itself never holds the Portainer token. To set it up, add these variables to the stack and redeploy once:

Variable Value
UPDATER_SHARED_SECRET A random secret shared by the app and the updater: openssl rand -hex 32
PORTAINER_URL Portainer's address as the updater can reach it, e.g. http://<docker host IP>:9000, or https://portainer.example.com
PORTAINER_API_KEY A Portainer access token: in Portainer, your user menu → My account → Access tokens → Add access token
PORTAINER_STACK_ID The stack's number: the id= in its address when you open it in Portainer
PORTAINER_TLS_VERIFY Optional, default true. Portainer's own HTTPS certificate is self-signed out of the box: use its http:// port, a trusted certificate, or set this to false

Keep the token tight. An access token can do whatever its Portainer user can, so don't create it for your main admin. In Portainer (Community Edition is enough):

  1. Settings → Users: add a standard user, e.g. yadt-updater.
  2. Settings → Teams: add a team with that user in it.
  3. Environments: give the team access to this environment.
  4. The YetAnotherDmarcTool stack → Access control: restrict it to that team (otherwise only administrators can update it, and the token is refused).
  5. Sign in as that user and create the access token there.

That token can then redeploy this stack only: other stacks that aren't shared with the team stay out of its reach.

What Update now does: it checks the version is a real, newer release, then asks Portainer to redeploy this stack with that IMAGE_TAG and fresh images. Portainer then runs the migrations, restarts the app and replaces the updater too. Problems show in the updater container's log, and in the api's log as SETUP TIP / SETUP PROBLEM lines. Stacks deployed from a Git repository aren't supported this way: update those in Portainer.

Next step

Onboarding Your First Organization


← Quick Start: Docker Compose · Deploying on Azure →

Clone this wiki locally