Configuration - ThinkHumanDotDev/marmot GitHub Wiki
Marmot is configured through environment variables. Every variable is declared and validated in
src/env.ts (zod) when a process starts, so a typo or a missing required value fails fast with a readable
message instead of a half-working install. .env.example at the repository root lists all of them for local
development; docker/.env.example is the shorter compose variant.
Three processes read the environment: web (Next.js + Payload: UI, API, status pages), worker
(checks, heartbeats, notifications, retention) and realtime (socket.io). With MARMOT_ROLE=all one
container runs all three. The Read by column below tells you which process must see a change; in a
compose stack the .env file is passed to all three, so you can simply restart the stack.
Boolean variables accept 1, true, yes, on (case-insensitive) as true and anything else as false.
The marmot command-line tool is a client, not one of these processes: its variables (MARMOT_URL,
MARMOT_API_KEY, MARMOT_ORG, β¦) are described in CLI and monitors as code.
| Variable | Default | Read by | Description |
|---|---|---|---|
PAYLOAD_SECRET |
β (required) | all | Signs session tokens and encrypts secrets at rest. At least 16 characters; use openssl rand -hex 32. Changing it signs everyone out. |
NEXT_PUBLIC_SERVER_URL |
http://localhost:3000 |
all | The URL users open Marmot at. Used for CORS/CSRF, invitation and password-reset emails, the OIDC redirect URI, status-page links, custom-domain detection and the realtime CORS origin. Must match the public https:// URL behind a proxy. |
ADDITIONAL_ORIGINS |
(empty) | web | Extra CORS/CSRF origins, comma-separated (e.g. a second hostname behind the same proxy). |
MARMOT_ROLE |
all |
all | Which process this container runs: web, worker, realtime, all, or probe (a probe agent). The compose file sets it per service. |
NODE_ENV |
development |
all |
development, test or production. The Docker image sets production, which enables HSTS, refuses plain-http:// OIDC issuers and makes the database adapters require migrations instead of pushing the schema. |
LOG_LEVEL |
info |
all | pino level: fatal, error, warn, info, debug, trace. Logs are JSON lines on stdout. |
| Variable | Default | Read by | Description |
|---|---|---|---|
DATABASE_ADAPTER |
postgres |
all |
postgres, mongodb or sqlite. SQLite is for local development only (no migrations, pushes its schema). |
DATABASE_URL |
β (required) | all | Connection string for the adapter: postgres://user:pass@host:5432/marmot, mongodb://host:27017/marmot, file:./data/marmot.db. Inside compose the host is the service name (postgres, mongo). |
Postgres migrations live in src/migrations/postgres and run when the web role starts (or with the image's
migrate command). MongoDB needs no schema migrations; indexes are created on boot.
| Variable | Default | Read by | Description |
|---|---|---|---|
REDIS_URL |
redis://localhost:6379 |
all | BullMQ queues and job schedulers (worker, and the web process when monitors change) plus socket.io pub/sub (realtime, and the emitters in web and worker). Run Redis with maxmemory-policy noeviction. |
On networks where the Redis hostname resolves only to IPv6 (Railway's *.railway.internal), append
?family=0 to REDIS_URL so the client looks up IPv6 addresses too; it defaults to IPv4.
Redis holds only queue state and live socket rooms. It does not need a backup: the worker re-creates every job scheduler from the database on start.
| Variable | Default | Read by | Description |
|---|---|---|---|
REALTIME_PORT |
3001 |
realtime | Port the socket.io server listens on. GET /healthz on the same port is the health probe. |
NEXT_PUBLIC_REALTIME_URL |
(empty) | web (build time) | Origin of the socket server when it is not reachable at /socket.io on the web origin. Leave empty with the bundled Caddy or any proxy that routes /socket.io/* to the realtime process. Inlined into the browser bundle at pnpm build; the published image has it empty. |
See Realtime origin below.
Uploads (organization logos, status-page logos) go to local disk unless S3_BUCKET is set, in which case
@payloadcms/storage-s3 is registered and the other S3_* variables configure the client.
| Variable | Default | Read by | Description |
|---|---|---|---|
UPLOADS_DIR |
uploads |
web | Local upload directory, relative to the working directory. The image uses /app/uploads (a volume). |
S3_BUCKET |
β | web | Bucket name. Setting it switches storage to S3. |
S3_REGION |
β | web | Region (auto for Cloudflare R2 and most S3-compatible stores). |
S3_ENDPOINT |
β | web | Custom endpoint for S3-compatible services (R2, MinIO, β¦). Leave unset for AWS. |
S3_ACCESS_KEY_ID |
β | web | Access key. |
S3_SECRET_ACCESS_KEY |
β | web | Secret key. |
S3_FORCE_PATH_STYLE |
false |
web | Use path-style URLs (https://endpoint/bucket/key), needed by MinIO and some proxies. |
Marmot sends invitations, password-reset mail, email verification and sign-in links and status page subscriber
emails (confirmations and announcements, sent by the worker) through the Payload email adapter, over a
pooled SMTP connection (at most five at a time per process). Without SMTP_HOST the messages are written
to the process log instead of being sent, which is enough to copy an invitation link during evaluation.
Subscriber SMS use a Twilio notification channel chosen per page, so they need no variable either. The smtp notification provider can reuse these settings (Use the server SMTP
settings); the last two variables decide who may do that and how much mail it may send.
| Variable | Default | Read by | Description |
|---|---|---|---|
SMTP_HOST |
β | web, worker | SMTP server. Unset = log mail to the console. |
SMTP_PORT |
587 |
web, worker | SMTP port. |
SMTP_USER |
β | web, worker | Username (optional for unauthenticated relays). |
SMTP_PASSWORD |
β | web, worker | Password. |
SMTP_SECURE |
false |
web, worker |
true for implicit TLS (usually port 465); false uses STARTTLS when offered. |
EMAIL_FROM |
Marmot <marmot@localhost> |
web, worker | Sender address, Name <address> form allowed. |
NOTIFICATIONS_SERVER_SMTP |
superadmin |
web, worker | Who may set up an smtp notification channel that sends through the settings above: all (anyone who manages channels), superadmin (instance superadmins only) or off (nobody; such channels fail to deliver). |
NOTIFICATIONS_SERVER_SMTP_RATE |
60 |
web, worker | Messages per organization per hour sent through the server SMTP settings, test messages included. 0 = unlimited. |
Notification mail sent through the server settings is limited to 10 recipients per message (to, cc and bcc
combined). In superadmin mode, channels set up before the upgrade keep sending; see
Deployment for how to list them. Details: Notifications.
| Variable | Default | Read by | Description |
|---|---|---|---|
DISABLE_SIGNUP |
false |
web | Default for the allowSignup instance setting. When sign-up is off, accounts are created only through the setup wizard, invitations, or SSO logins that match a pending invitation. |
REQUIRE_EMAIL_VERIFICATION |
false |
web | Default for the requireEmailVerification instance setting: new self-service accounts confirm their address by email before they can create organizations, invitations or notification channels. See Organizations and members β Email verification. |
MAGIC_LINK_ENABLED |
false |
web | Default for the magicLinkEnabled instance setting: "Email me a sign-in link" on the login page (single-use, 15 minutes; needs SMTP). Off in SSO-only mode. See Organizations and members β Sign-in links. |
OIDC_ISSUER_URL |
β | web | Issuer of your OpenID Connect provider (its iss value). Discovery is read from <issuer>/.well-known/openid-configuration. |
OIDC_CLIENT_ID |
β | web | Client id registered at the provider. |
OIDC_CLIENT_SECRET |
β | web | Client secret. All three OIDC_ISSUER_URL, OIDC_CLIENT_ID and OIDC_CLIENT_SECRET must be set to enable the SSO button. |
OIDC_DISPLAY_NAME |
Single sign-on |
web | Label of the login button ("Continue with β¦"). |
OIDC_AUTO_PROVISION |
true |
web | Create a Marmot account on first SSO login. false only lets existing users (matched by subject or verified email) in. |
OIDC_SCOPES |
openid email profile |
web | Scopes requested from the provider. |
OIDC_GROUP_CLAIM |
groups |
web | Claim listing the user's groups; dotted paths read nested claims (realm_access.roles). See Single sign-on β Groups. |
OIDC_ALLOWED_GROUPS |
β | web | Comma-separated, case-insensitive allow-list. When set, users in none of these groups cannot sign in or be provisioned through the OIDC client. |
OIDC_ROLE_MAPPING |
β | web | JSON object from group to {"org": "<slug>", "role": "owner|admin|member|viewer"} (or a list of them, or {"role": "superadmin"}), applied on every login. Invalid JSON stops startup. |
OIDC_ROLE_MAPPING_REMOVE |
false |
web | Also remove memberships of mapped organizations (and superadmin) the groups no longer grant. The last owner / superadmin is never removed. |
OIDC_DISABLE_LOCAL_LOGIN |
false |
web | SSO-only mode: no password logins, sign-ups or password resets. See Single sign-on β SSO-only mode. |
OIDC_BREAK_GLASS |
false |
web | With OIDC_DISABLE_LOCAL_LOGIN, superadmins can still sign in with their password from /login?local=1 (audited as auth.break_glass). |
GITHUB_CLIENT_ID |
β | web | GitHub OAuth app client id. With GITHUB_CLIENT_SECRET, enables "Continue with GitHub"; callback URL <NEXT_PUBLIC_SERVER_URL>/api/auth/sso/github/callback. |
GITHUB_CLIENT_SECRET |
β | web | GitHub OAuth app client secret. |
GOOGLE_CLIENT_ID |
β | web | Google OAuth client id. With GOOGLE_CLIENT_SECRET, enables "Continue with Google"; redirect URI <NEXT_PUBLIC_SERVER_URL>/api/auth/sso/google/callback. |
GOOGLE_CLIENT_SECRET |
β | web | Google OAuth client secret. |
API_KEY_RATE_LIMIT |
600 |
web | Management API requests per minute per API key; 0 turns the limit off. |
API_KEY_WRITE_RATE_LIMIT |
60 |
web | Of those, writes (POST/PUT/PATCH/DELETE) per minute per key; 0 turns the limit off. |
METRIC_PUSH_RATE_LIMIT |
60 |
web |
Custom metric pushes per minute per API key (or user) and metric, on top of API_KEY_WRITE_RATE_LIMIT; 0 turns the limit off. |
STATUS_PAGE_SESSION_DAYS |
30 |
web | Lifetime (days, 1β365) of the cookie a visitor gets after signing in to a protected status page (password or email link). |
Full setup guide with provider walkthroughs: Single sign-on.
| Variable | Default | Read by | Description |
|---|---|---|---|
KEEP_DATA_PERIOD_DAYS |
365 |
web, worker | Default for the keepDataPeriodDays instance setting: how long daily aggregates and important heartbeats are kept. Raw heartbeats live 24 h, minutely buckets 24 h, hourly 30 d. |
AUDIT_LOG_RETENTION_DAYS |
365 |
worker | Days the audit log keeps its rows; the hourly retention job deletes older ones. 0 keeps them forever. |
WEBHOOK_DELIVERY_RETENTION_DAYS |
14 |
worker | Days the webhook delivery log keeps a delivery (1β90); the hourly retention job deletes older ones. |
WEBHOOK_DISABLE_AFTER_FAILURES |
5 |
worker | An outbound webhook endpoint is disabled, and the organization's admins are emailed, after this many deliveries in a row failed for good (each after its retries). 0 never disables. |
OTLP_EXPORT_ENABLED |
true |
worker, web |
OpenTelemetry export of check metrics to the organizations' collectors. false turns it off for the whole instance. |
OTLP_EXPORT_INTERVAL_MS |
5000 |
worker, web | How long data points wait to be batched before they are sent (100β300000 ms). |
OTLP_EXPORT_MAX_BATCH |
1000 |
worker, web | Data points per export request; a full batch is sent at once. |
OTLP_EXPORT_MAX_QUEUE |
20000 |
worker, web | Data points buffered per collector while it is slow or down; beyond that the oldest are dropped. |
OTLP_EXPORT_TIMEOUT_MS |
10000 |
worker, web | Timeout of one export request (1000β120000 ms). |
WORKER_CONCURRENCY |
10 |
worker | Parallel checks (and notification deliveries) per worker process. One worker uses at most one core (about 25 checks/s): keep it at 10β20 and add worker replicas instead (Scaling). |
ON_DEMAND_CHECKS_PER_MINUTE |
30 |
web | On-demand checks per organization and minute: Check now, the monitor form's Test and their API (POST /api/orgs/:orgId/monitors/:id/check, POST /api/orgs/:orgId/checks). Further requests get 429. |
CONNECTIVITY_CHECK_ENABLED |
false |
worker | Self connectivity check: hold checks of external targets while the worker itself is offline instead of reporting them DOWN. |
CONNECTIVITY_CHECK_TARGETS |
1.1.1.1:53, 8.8.8.8:53, https://www.google.com/generate_204 |
worker | Probe targets, comma-separated: host:port (TCP connect, [v6]:port for IPv6) or an http(s):// URL (any HTTP response counts). Use your own when these are blocked. |
CONNECTIVITY_CHECK_MODE |
any |
worker |
any: online when at least one target answers; all: every target must answer. |
CONNECTIVITY_CHECK_INTERVAL |
30 |
worker | Seconds between probes (5β3600); the verdict is cached for this long. |
CONNECTIVITY_CHECK_TIMEOUT |
5 |
worker | Seconds each target gets to answer (1β60). |
CONNECTIVITY_CHECK_NOTIFY_EMAIL |
true |
worker | Email the "checker offline" / "back online" notices to every instance superadmin (needs SMTP_HOST). |
CONNECTIVITY_CHECK_NOTIFICATION_ID |
β | worker | Id of a notification channel that also receives the notices (any provider, e.g. a Telegram bot on a mobile network). |
DOCKER_SOCKET_ENABLED |
true |
web, worker | Allow socket Docker hosts, which talk to the Docker daemon of the worker's own host. Set false on shared installs where users must not reach the local daemon. |
MARMOT_DISABLE_ENGINE_HOOKS |
false |
web | Skip the BullMQ scheduler sync in the monitors collection hooks. Only for tests that run without Redis; the Vitest setup sets it. |
MONITOR_DENY_PRIVATE_ADDRESSES |
false |
web, worker | Refuse monitor checks and notification deliveries to private, loopback, link-local, CGNAT, multicast and container-network addresses (see below). Turn on when people you do not trust can create monitors, e.g. with open sign-up. |
MONITOR_DENY_CIDRS |
β | web, worker | Comma-separated CIDRs that are always refused, guard on or off (e.g. 203.0.113.0/24, 2001:db8::/32). Wins over MONITOR_ALLOW_CIDRS. |
MONITOR_ALLOW_CIDRS |
β | web, worker | Comma-separated CIDRs exempt from the private-address deny list, for internal subnets you do want to monitor (e.g. 10.20.0.0/16). |
Monitors with Certificate transparency alerts turned on (ctAlerts, see
Notifications) are looked up in the certificate
transparency logs by the worker's ct-alerts job.
| Variable | Default | Read by | Description |
|---|---|---|---|
CT_SOURCE_URL |
https://crt.sh/?q={query}&output=json&exclude=expired&deduplicate=Y |
worker | CT source, a URL template. {query} is the crt.sh identity (example.com, or %.example.com for the subdomains, looked up separately), {domain} the hostname, {subdomains} true or false. The answer may be crt.sh JSON or Cert Spotter issuances (expand=dns_names&expand=issuer). |
CT_CHECK_INTERVAL_MINUTES |
180 |
worker | Minutes between two lookups of a monitor (60β360). After a failed lookup the monitor and the source back off exponentially, up to 24 h. |
CT_REQUESTS_PER_MINUTE |
6 |
worker | Requests per minute sent to the source (1β120). A 429 or 503 answer pauses every lookup for its Retry-After (at least the backoff). |
Push monitors can also be pinged by email. Off unless INBOUND_EMAIL_DOMAIN and at least one of the two transports below are set.
| Variable | Default | Read by | Description |
|---|---|---|---|
INBOUND_EMAIL_DOMAIN |
β | web, worker | Domain of the monitor addresses, <push token>@<domain> (e.g. ping.status.example.com). Point its MX record at the worker, or route it through a mail provider to the webhook. |
INBOUND_SMTP_PORT |
0 |
worker | Port of the worker's inbound SMTP listener (plain SMTP, no AUTH, no relaying; only addresses of active push monitors are accepted). 0 turns the listener off. Use 25 when the worker is the domain's MX, or a high port behind a relay or port mapping. |
INBOUND_SMTP_HOST |
0.0.0.0 |
worker | Address the listener binds to. |
INBOUND_EMAIL_WEBHOOK_SECRET |
β | web | Turns on the inbound-parse webhook POST /api/inbound-email (Postmark, Mailgun, SendGrid, Cloudflare Email Workers) and is the secret it requires (at least 16 characters): ?secret=, the Basic auth password or an X-Marmot-Secret header. |
INBOUND_EMAIL_MAX_BYTES |
1048576 |
web, worker | Largest accepted message in bytes (1 KiBβ50 MiB). Larger mail is refused (552 over SMTP, 413 on the webhook). |
Integrations receive alerts from Alertmanager, Grafana or any JSON sender at POST /api/inbound/<token> (and by email at <token>@INBOUND_EMAIL_DOMAIN when ping-by-email is on).
| Variable | Default | Read by | Description |
|---|---|---|---|
INBOUND_ALERT_RATE_LIMIT |
120 |
web | Requests per minute per integration token, and per client address when trustProxy is on. Over the limit the endpoint answers 429. 0 turns the limit off. |
INBOUND_ALERT_MAX_BYTES |
262144 |
web | Largest accepted request body in bytes (1 KiBβ10 MiB); larger bodies get 413. |
When the worker's own uplink or DNS fails, every external monitor would go DOWN at once and every channel
would be flooded, although only Marmot's connectivity is down. With CONNECTIVITY_CHECK_ENABLED=true the
worker probes CONNECTIVITY_CHECK_TARGETS every CONNECTIVITY_CHECK_INTERVAL seconds (and again whenever a
check fails and the last probe is older than 10 s). While the probes fail (per CONNECTIVITY_CHECK_MODE):
- checks of external targets are skipped and recorded as PENDING heartbeats with the message
checker offline. They send no notifications, are left out of the uptime statistics, and leave the monitor's status as it was, so the first check after the outage is judged against the status from before; - monitors whose targets are all private or local keep running: IP addresses in the private ranges listed
under Private-address guard,
localhost, single-label names,.local,.lan,.internal,.home.arpaand.localdomainnames, names that resolve only to private addresses, database sockets, Docker socket hosts and groups. Push monitors are held (pushes cannot arrive either); - one "checker offline" notice goes to the instance superadmins by email and/or to the channel
CONNECTIVITY_CHECK_NOTIFICATION_ID, and one "back online" notice when the probes succeed again (notices that cannot be delivered while offline are retried after every probe). With several worker replicas only one of them sends the notices of an outage; - the Marmot UI shows a "Checker offline" banner,
GET /api/healthreportschecker.status = offlineand/api/metricsexposesmarmot_checker_online 0.
When connectivity returns, the held monitors are checked again at once. The check is off by default.
With MONITOR_DENY_PRIVATE_ADDRESSES=true the worker checks every address a monitor or notification
channel would connect to, after DNS resolution and at connect time (every redirect hop included), and
connects only to the address it checked. A refused check is a DOWN heartbeat such as
Blocked: db.internal resolves to a private address (MONITOR_DENY_PRIVATE_ADDRESSES).
-
Denied ranges:
0.0.0.0/8,10.0.0.0/8,100.64.0.0/10,127.0.0.0/8,169.254.0.0/16,172.16.0.0/12,192.0.0.0/24,192.168.0.0/16,198.18.0.0/15,224.0.0.0/4and above,::,::1,fc00::/7,fe80::/10,fec0::/10,ff00::/8,64:ff9b:1::/48, plus IPv6 forms that embed one of the IPv4 ranges (::ffff:127.0.0.1,::127.0.0.1, NAT6464:ff9b::/96, 6to42002::/16). -
Precedence:
MONITOR_DENY_CIDRSβΊMONITOR_ALLOW_CIDRSβΊ the ranges above. -
Refused types:
tailscale-ping,real-browserand Docker hosts of typesocketcannot be saved or run, and Apprise channels cannot send: they reach the network without passing the guard. -
Saving a monitor, proxy or Docker host whose target is a denied literal (
127.0.0.1,2130706433,[::1],localhost) is refused right away; names are judged when the check runs. - Instance-wide settings (
SMTP_HOST,DATABASE_URL,REDIS_URL) are not affected.
Malformed CIDRs stop the process at startup with a readable error.
Probe locations run checks from other networks. The first two variables configure the
server; the last two are all a probe agent (MARMOT_ROLE=probe) needs. An agent reads no database, Redis
or PAYLOAD_SECRET; besides these it honours WORKER_CONCURRENCY, LOG_LEVEL, DOCKER_SOCKET_ENABLED,
the MONITOR_* address guard and the CONNECTIVITY_CHECK_* variables (its own uplink check).
| Variable | Default | Read by | Description |
|---|---|---|---|
PROBE_OFFLINE_AFTER |
180 |
web, worker | Seconds without contact after which a location is marked offline and its admins are emailed (30β86400). Agents refresh their configuration every third of it, at most every 60 s. |
PROBE_RATE_LIMIT |
600 |
web | Requests per minute per location token to /api/probe/v1/*; 0 turns the limit off. |
MARMOT_URL |
β | probe | Agent only, required: the Marmot server, e.g. https://marmot.example.com. The agent only makes outbound HTTPS requests to it. |
MARMOT_PROBE_TOKEN |
β | probe | Agent only, required: the location's token (mp_β¦), shown once when the location is created or its token is rotated. |
Switches for running Marmot as a hosted service. Self-hosted installs leave them off.
| Variable | Default | Read by | Description |
|---|---|---|---|
LANDING_PAGE_ENABLED |
false |
web | Show a marketing landing page at / to signed-out visitors instead of redirecting to /login. Signed-in users and fresh installs (setup wizard) are unaffected. |
TERMS_URL |
β | web | Your terms of service. When set, linked from the landing and pricing footers, the sign-up form ("By signing up you agree toβ¦") and the Billing tab. Marmot does not host the text. |
PRIVACY_URL |
β | web | Your privacy policy; linked in the same places as TERMS_URL when set. |
SOURCE_URL |
β | web | Target of the "Source" link in the app, sign-in and landing footers (AGPL-3.0 Β§13). Set it when you run a modified build; unset, it links to this repository at the running version's tag. |
With LANDING_PAGE_ENABLED and BILLING_ENABLED both on, /pricing lists the purchasable plans with
their limits and their monthly and yearly Stripe prices (limits only without STRIPE_SECRET_KEY); it
returns 404 otherwise. See Billing.
A self-resetting public demo; see Demo mode. Every reset deletes all data.
| Variable | Default | Read by | Description |
|---|---|---|---|
DEMO_MODE |
false |
all | Seed the demo dataset, reset it on schedule, simulate checks, send nothing and switch off the features listed in the guide. |
DEMO_RESET_INTERVAL_MINUTES |
60 |
web, worker | Minutes between two resets (15β10080). |
DEMO_MODE_FORCE |
false |
all | Start although the database holds non-demo accounts, and wipe it on the next reset. |
A scaffold for hosted offerings. Self-hosted installs leave it off; every limit is then unlimited and no Stripe code is loaded. Details in Billing.
| Variable | Default | Read by | Description |
|---|---|---|---|
BILLING_ENABLED |
false |
web | Enforce plan limits and show the Billing settings tab. |
STRIPE_SECRET_KEY |
β | web | Registers the Stripe plugin and enables Checkout / Billing Portal. |
STRIPE_WEBHOOK_SECRET |
β | web | Signing secret of the Stripe webhook endpoint. |
STRIPE_PUBLISHABLE_KEY |
β | web | Reserved for a future Stripe Elements checkout; not needed today. |
Marmot sends nothing by default: no analytics SDK is loaded, no cookie banner appears and nothing calls home. Setting a PostHog key enables opt-in product analytics behind a consent banner; what is collected is documented in Telemetry.
| Variable | Default | Read by | Description |
|---|---|---|---|
NEXT_PUBLIC_POSTHOG_KEY |
β | web (build time), worker | PostHog project key. Unset = telemetry off. |
NEXT_PUBLIC_POSTHOG_HOST |
https://us.i.posthog.com |
web (build time), worker | PostHog ingestion host (https://eu.i.posthog.com for the EU cloud). Also the target of the /ph rewrite. |
These are read by docker/entrypoint.sh, docker/docker-compose.yml or the Caddyfile rather than by
src/env.ts, so they only matter for Docker deployments.
| Variable | Default | Used by | Description |
|---|---|---|---|
DOMAIN |
β | compose β Caddy | Hostname for automatic HTTPS. Unset = Caddy serves plain HTTP on port 80 (for use behind your own proxy). |
ACME_EMAIL |
β | compose β Caddy | Contact address passed to the certificate authority. |
SITE_ADDRESS |
:80 |
Caddyfile | Derived from DOMAIN by compose; set it directly when running Caddy by hand. |
WEB_UPSTREAM |
web:3000 |
Caddyfile |
host:port of the web process. Override when the proxy cannot reach it as web (the Railway template uses web.railway.internal:3000). |
REALTIME_UPSTREAM |
realtime:3001 |
Caddyfile |
host:port of the realtime process, for /socket.io/*. |
POSTGRES_PASSWORD |
marmot |
compose | Password of the bundled Postgres; the default DATABASE_URL picks it up. |
MARMOT_VERSION |
release | compose | Image tag of ghcr.io/thinkhumandotdev/marmot to run; a release's compose file defaults to that release. |
PORT |
3000 |
entrypoint (web) | Port of the Next.js server inside the container. |
BIND_HOST |
0.0.0.0 |
entrypoint (web) | Address the Next.js server listens on. :: listens on IPv6 and IPv4, for IPv6-only private networks such as Railway's. |
SKIP_MIGRATIONS |
false |
entrypoint (web) |
true skips migrate on start, for when you run migrations yourself (init container, CI/CD step). |
WORKER_SCHEMA_WAIT_MS |
120000 |
worker | How long the worker waits for the database schema (migrations running in web) before giving up on boot. |
Some options can be changed at runtime by a superadmin without redeploying: in the Marmot UI under
Settings β Instance (/{orgSlug}/settings/instance, visible to superadmins only, saved through
POST /api/globals/instance-settings) or in the Payload admin panel (System β Instance settings, the
instance-settings global). Environment variables provide the defaults; once a value has been saved in the
global it takes precedence over the variable. The Instance tab also shows whether SMTP is configured and
offers Send test email (POST /api/instance/smtp-test { to? }, superadmin-only; answers 400 while
SMTP_HOST is unset).
| Setting | Default | Description |
|---|---|---|
primaryBaseUrl |
NEXT_PUBLIC_SERVER_URL |
Public URL used in notifications and status page links. |
allowSignup |
!DISABLE_SIGNUP |
Whether anyone may create an account (POST /api/users, /signup). |
requireEmailVerification |
REQUIRE_EMAIL_VERIFICATION |
New self-service sign-ups confirm their address before creating organizations, invitations or notification channels. Existing accounts stay verified. |
magicLinkEnabled |
MAGIC_LINK_ENABLED |
Passwordless sign-in with a single-use link sent by email (two-factor authentication still applies). Ignored while OIDC_DISABLE_LOCAL_LOGIN is on. |
entryPage |
dashboard |
dashboard or status-page: what the root URL shows. |
tlsExpiryNotifyDays |
7, 14, 21 |
Days before a TLS certificate expires at which to notify. |
domainExpiryNotifyDays |
7, 14, 21 |
Days before a domain registration expires at which to notify. |
keepDataPeriodDays |
KEEP_DATA_PERIOD_DAYS |
Retention of daily aggregates and important heartbeats (0 disables). Honoured by the hourly retention job. |
trustProxy |
false |
Trust X-Forwarded-* headers from the reverse proxy for client IPs (rate limits, audit log, status page IP allow-lists). |
steamApiKey, globalpingApiToken
|
β | Third-party API keys for the corresponding monitor types (superadmin-only read). |
Server code reads the resolved values through getInstanceSettings(payload) (src/server/settings.ts),
which caches them for 60 seconds per process; saving the global clears the cache of the web process
immediately, the worker and realtime processes pick changes up within a minute.
A fresh install has no users. While that is the case GET /api/setup/status returns { "needsSetup": true },
/ and the auth pages redirect to /setup, and the wizard creates the first superadmin plus their
organization in one transaction (POST /api/setup) and signs them in. Once any user exists the wizard is
closed for good (/setup redirects to /login, the API answers 409); further users join through
/signup (if allowSignup) or invitations.
The browser opens one socket.io connection with credentials, so the realtime server must be reachable on a
URL that receives the payload-token cookie. By default the client connects to the page's own origin at
/socket.io, and Caddy (or your reverse proxy, see Deployment) forwards that path to the
realtime process on REALTIME_PORT. Set NEXT_PUBLIC_REALTIME_URL (e.g. https://realtime.example.com)
only when the realtime server is exposed on another origin; it must share the cookie's site (a subdomain of
the web app is fine) and allows NEXT_PUBLIC_SERVER_URL in CORS. Being a NEXT_PUBLIC_* variable it is
inlined at pnpm build time, so it requires building your own image; the published image expects the
same-origin layout.