Configuration¶
maintenant is configured entirely through environment variables. No configuration files required.
Environment Variables¶
| Variable | Default | Description |
|---|---|---|
MAINTENANT_ADDR |
127.0.0.1:8080 |
HTTP bind address. Use 0.0.0.0:8080 inside containers; on a host install, bind the interface you actually need. See Choosing a Bind Address. |
MAINTENANT_DB |
./maintenant.db |
SQLite database file path. |
MAINTENANT_BASE_URL |
http://localhost:8080 |
Public base URL. Used for heartbeat ping URLs and as the fallback target for the status page link. |
MAINTENANT_STATUS_URL |
— | Canonical public URL of the status page (e.g. https://status.example.com). When set, the admin UI's View public status page link points here. Optional — falls back to {MAINTENANT_BASE_URL}/status when unset. See Public Status Page → Status URL. |
MAINTENANT_CORS_ORIGINS |
same-origin | CORS allowed origins (comma-separated). Empty means same-origin only. Set to * for wildcard. |
MAINTENANT_RUNTIME |
auto-detect | Force container runtime: docker or kubernetes. Auto-detected by default. |
DOCKER_HOST |
local socket | Docker API endpoint (standard Docker SDK variable). Point it at a socket proxy (tcp://socketproxy:2375) to run without mounting /var/run/docker.sock — see Security → Docker Socket Proxy. |
MAINTENANT_MAX_BODY_SIZE |
1048576 |
Maximum request body size in bytes for POST/PUT requests (default: 1 MB). |
MAINTENANT_UPDATE_INTERVAL |
24h |
Update intelligence scan interval. Accepts Go duration format (e.g., 12h, 30m). |
MAINTENANT_K8S_NAMESPACES |
all | Kubernetes namespace allowlist (comma-separated). Empty monitors all namespaces. |
MAINTENANT_K8S_EXCLUDE_NAMESPACES |
none | Kubernetes namespace blocklist (comma-separated). |
MAINTENANT_LICENSE_KEY |
— | Pro license key. Enables Pro features when set to a valid key. |
MAINTENANT_MCP |
false |
Enable the MCP server on /mcp (Streamable HTTP transport). |
MAINTENANT_MCP_CLIENT_ID |
— | OAuth2 client ID for MCP authentication. |
MAINTENANT_MCP_CLIENT_SECRET |
— | OAuth2 client secret for MCP authentication. |
MAINTENANT_MCP_ALLOWED_REDIRECT_URIS |
— | Comma-separated allowlist of OAuth2 redirect_uri values accepted by /oauth/authorize. Required when MCP credentials are set. |
MAINTENANT_ORGANISATION_NAME |
Maintenant |
Organisation name displayed on the public status page. |
MAINTENANT_SMTP_HOST |
— | SMTP server hostname for email notifications. |
MAINTENANT_SMTP_PORT |
587 |
SMTP server port. |
MAINTENANT_SMTP_USERNAME |
— | SMTP authentication username. |
MAINTENANT_SMTP_PASSWORD |
— | SMTP authentication password. |
MAINTENANT_SMTP_FROM |
maintenant@localhost |
Sender address for email notifications. |
MAINTENANT_DISABLE_TELEMETRY |
unset (telemetry on) | Disable anonymous install telemetry. Truthy values: 1, t, true, y, yes, on (case-insensitive). |
MAINTENANT_ALLOW_PRIVATE_WEBHOOKS |
unset | Development only. Bypass the HTTPS requirement and private-IP SSRF protection for alert channels and webhook subscriptions. Allows http:// URLs and internal Docker hostnames. Never set in production. |
Example .env File¶
# Listen address — see "Choosing a Bind Address" below.
# In a container: keep 0.0.0.0:8080 and control exposure with the port mapping.
# On the host: 127.0.0.1:8080 (local only) or <lan-ip>:8080 for LAN access.
MAINTENANT_ADDR=127.0.0.1:8080
# SQLite database path
MAINTENANT_DB=./maintenant.db
# Public base URL (used for heartbeat ping URLs and as the status page fallback)
MAINTENANT_BASE_URL=https://maintenant.example.com
# Canonical public URL of the status page (optional).
# When set, the admin UI links to this URL instead of {BASE_URL}/status.
# Use this when serving the status page on its own subdomain.
# MAINTENANT_STATUS_URL=https://status.example.com
# CORS allowed origins (comma-separated, empty = same-origin only)
# MAINTENANT_CORS_ORIGINS=http://localhost:5173
# Container runtime override (auto-detected by default: docker or kubernetes)
# MAINTENANT_RUNTIME=docker
# Max request body size in bytes (default: 1MB)
# MAINTENANT_MAX_BODY_SIZE=1048576
# Update intelligence scan interval (Go duration, default: 24h)
# MAINTENANT_UPDATE_INTERVAL=24h
# Kubernetes namespaces to monitor (comma-separated, empty = all)
# MAINTENANT_K8S_NAMESPACES=default,production
# Kubernetes namespaces to exclude (comma-separated)
# MAINTENANT_K8S_EXCLUDE_NAMESPACES=kube-system
# Organisation name (displayed on the public status page)
# MAINTENANT_ORGANISATION_NAME=Acme Corp
# MCP Server (Model Context Protocol for AI assistants)
# MAINTENANT_MCP=true
# MAINTENANT_MCP_CLIENT_ID=maintenant-mcp
# MAINTENANT_MCP_CLIENT_SECRET=your-secret-here
# SMTP configuration (required for email notification channels)
# MAINTENANT_SMTP_HOST=smtp.example.com
# MAINTENANT_SMTP_PORT=587
# MAINTENANT_SMTP_USERNAME=alerts@example.com
# MAINTENANT_SMTP_PASSWORD=secret
# MAINTENANT_SMTP_FROM=maintenant@example.com
# Disable anonymous install telemetry (truthy: 1, true, yes, on)
# MAINTENANT_DISABLE_TELEMETRY=1
# [DEV ONLY] Allow http:// URLs and private IPs for webhook/channel targets.
# Never set in production — disables SSRF protection.
# MAINTENANT_ALLOW_PRIVATE_WEBHOOKS=true
Choosing a Bind Address¶
MAINTENANT_ADDR controls where the maintenant process itself listens. In Docker, the published port (the ports: mapping / -p flag) is a separate decision — and it is the one that determines what your network can reach.
Running in a container (recommended)¶
Keep MAINTENANT_ADDR=0.0.0.0:8080. Inside a container this is required: Docker's port mapping reaches the process through the container's bridge interface, so binding 127.0.0.1 there would make the UI unreachable from outside the container. Control exposure with the port mapping instead:
| Goal | Port mapping |
|---|---|
| Behind a reverse proxy on the same host | 127.0.0.1:8080:8080 — or no ports: at all when the proxy shares a Docker network with maintenant |
| Headless server, direct access from your LAN | 192.168.1.50:8080:8080 (the server's LAN IP) |
| Reachable from every interface | 8080:8080 — only behind a reverse proxy with authentication, or on a trusted network you accept exposing it to |
Running the binary directly on the host¶
The default 127.0.0.1:8080 keeps the UI local-only. To reach it from your LAN without a reverse proxy, bind the server's LAN IP: MAINTENANT_ADDR=192.168.1.50:8080. Binding 0.0.0.0:8080 also works but listens on every interface of the host, including public ones if the machine has any.
\"Port exposed on all interfaces\" on maintenant's own container
maintenant's security scanner analyzes every discovered container — including its own. If the UI port is published on all interfaces (8080:8080), it reports a critical port_exposed_all_interfaces finding against itself. That is expected behaviour, not a bug: the port really is reachable from any network interface.
Either restrict the published port to a specific interface as shown above, or — when the exposure is intentional (e.g. a trusted home LAN) — acknowledge the finding with an audit trail: see Network Security Insights → Acknowledging Findings.
Telemetry¶
maintenant sends an anonymous, opt-out usage snapshot once an hour to https://metrics.kolapsis.com. No hostnames, IPs, container names, endpoint URLs, certificates, webhook targets, status-page component names, license keys, or operator-supplied free-form strings of any kind are transmitted.
What is collected¶
Each snapshot contains only the following fields:
Application fields:
edition—communityorprocontainers_total,endpoints_total,heartbeats_total,certificates_total,webhooks_total,status_components_total— counts of configured/auto-discovered entities
Runtime context (collected automatically by the SHM SDK):
sys_os,sys_arch,sys_cpu_cores,sys_go_version,sys_mode(docker/kubernetes/standalone)app_mem_alloc_mb,app_goroutines,app_uptime_h- An opaque installation identifier generated on first run and persisted to
/data/shm/shm_identity.json
How to disable it¶
Set MAINTENANT_DISABLE_TELEMETRY to a truthy value before the process starts:
Truthy values (case-insensitive, whitespace-trimmed): 1, t, true, y, yes, on. Anything else — including empty or unset — leaves telemetry enabled.
When disabled, exactly one log line is emitted at startup and nothing else:
No background goroutine, no DNS lookup of metrics.kolapsis.com, no outbound packets.
Persistent install identity¶
The SDK persists its identity to /data/shm/shm_identity.json on first run. The reference Docker Compose mounts /data as a named volume, so /data/shm is covered by default. The container entrypoint chowns /data/shm at startup, so bind mounts work too without manual permission setup.
For Kubernetes, mount a PVC at /data/shm:
volumeMounts:
- name: shm-data
mountPath: /data/shm
volumeClaimTemplates:
- metadata:
name: shm-data
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 100Mi
Without persistence, every restart is counted as a fresh install. The privacy impact is zero (the identifier is opaque), but fleet stats are distorted.
If /data/shm is unwritable (read-only mount), telemetry disables itself silently with a single WARN line and the process continues normally:
Pro License¶
To enable Pro features (Slack/Teams/Email channels, CVE enrichment, incident management, maintenance windows, subscriber notifications, and more), set the MAINTENANT_LICENSE_KEY environment variable:
services:
maintenant:
image: ghcr.io/kolapsis/maintenant:latest
environment:
MAINTENANT_LICENSE_KEY: "your-license-key"
The license is verified periodically against the license server. If the server is temporarily unreachable, Pro features remain active from cache with a graceful degradation window.
You can check the current license status via the API:
Security¶
maintenant does not include built-in authentication — by design. It delegates auth to your reverse proxy and middleware (Authelia, Authentik, OAuth2 Proxy).
The /api/v1/* routes and the dashboard must be behind authentication. The /ping, /status (match the prefix, not the trailing slash) and /manifest.webmanifest routes must be publicly accessible — the browser fetches the PWA manifest without credentials. If MCP is enabled with OAuth2, the /mcp, /oauth/, and /.well-known/ routes should bypass proxy auth (MCP handles its own).
See the Security Guide for the complete route reference, reverse proxy examples (Traefik, Caddy, nginx), built-in protections, MCP authentication details, and deployment hardening checklist.
Database¶
maintenant uses SQLite in WAL (Write-Ahead Logging) mode with a single-writer pattern. This provides excellent read performance while maintaining data integrity.
- The database file is created automatically on first startup.
- Migrations run automatically — no manual steps required.
- Back up the database by copying the
.db,.db-wal, and.db-shmfiles while maintenant is stopped, or usesqlite3 backupwhile running.