DocsGetting StartedConfiguration

Configuration

Bulwark has two configuration surfaces that work together:

  1. The web setup wizard and admin dashboard, which is what new installs should use. The wizard runs on first launch when no JMAP_SERVER_URL is set in the environment and writes JSON config to ADMIN_CONFIG_DIR. The admin dashboard manages the same file afterwards.
  2. Environment variables in .env.local (or your container's env). Still fully supported, and the right choice for immutable / read-only / env-as-code deployments.

When an environment variable is set, it takes precedence over the corresponding admin-managed value, so setting JMAP_SERVER_URL will hide that field from the wizard and lock it in the admin UI.

Configuration model

Bulwark supports three configuration layers:

  • Admin-managed config - Written by the setup wizard or the admin dashboard to data/admin/config.json (or ADMIN_CONFIG_DIR/config.json). Changes take effect on the next user session without a restart.
  • Runtime environment variables - Read by the server at request time. Override admin-managed values. Work well for Docker and reverse-proxy deployments.
  • Legacy build-time variables - NEXT_PUBLIC_* fallbacks still supported for older deployments, but new setups should prefer the wizard or runtime variables.

The order of precedence is env var > admin config > build-time fallback > built-in default.

Minimal setup

A basic deployment needs one variable:

JMAP_SERVER_URL=https://mail.example.com

Most deployments also set a display name:

APP_NAME=Bulwark Webmail
JMAP_SERVER_URL=https://mail.example.com

Environment variables

VariableRequiredDefaultDescription
HOSTNAMENo0.0.0.0Address the server binds to - use :: for IPv6
PORTNo3000Port the server listens on
APP_NAMENoWebmailApplication name shown in the UI, browser tab, and PWA manifest
APP_SHORT_NAMENofalls back to APP_NAMEShort name for the PWA install prompt and home screen
APP_DESCRIPTIONNogeneric Bulwark descriptionDescription shown in the PWA manifest
JMAP_SERVER_URLYes¹-URL of your JMAP-compatible mail server
JMAP_SERVERSNoemptyJSON array of servers to offer on the login form (id, label, url, optional domains and oauth)
JMAP_SERVER_AUTO_PICK_BY_DOMAINNofalsePick a server from JMAP_SERVERS by the domain the user types
ALLOW_CUSTOM_JMAP_ENDPOINTNofalseShow a "JMAP Server" field on the login form so users can specify their own server
STALWART_FEATURESNotrueEnables Stalwart-specific features (password change, Sieve, vacation, admin, API keys)
OAUTH_ENABLEDNofalseEnables OAuth2 / OpenID Connect login
OAUTH_ONLYNofalseHides the username/password login form and requires OAuth
OAUTH_CLIENT_IDOAuth only-OAuth client ID
OAUTH_CLIENT_SECRETNoemptyOAuth client secret for confidential clients
OAUTH_CLIENT_SECRET_FILENoemptyPath to a file containing the OAuth client secret (Docker / Kubernetes secrets)
OAUTH_ISSUER_URLNofalls back to JMAP_SERVER_URL discoveryExplicit issuer URL for external IdPs
OAUTH_AUTHORIZE_URLNofrom discoveryOverride only the user-facing authorize endpoint (per-brand login hosts)
OAUTH_ALLOW_PRIVATE_ENDPOINTSNofalseLet OAuth discovery resolve to RFC 1918 / loopback addresses (split-DNS setups)
OAUTH_SCOPESNobuilt-in defaultOverride OAuth scope string
OAUTH_EXTRA_SCOPESNoemptyAdditional scopes appended to defaults
SESSION_SECRETFeature-gated-Enables encrypted Remember-me, settings sync, multi-account, and embedded SSO
SESSION_SECRET_FILENoemptyPath to a file containing the session secret
COOKIE_SAME_SITENolaxCookie SameSite attribute (lax, none, strict)
COOKIE_SECURENoderived from envForce Secure flag on cookies
SETTINGS_SYNC_ENABLEDNofalseEnables encrypted server-side settings sync across devices and accounts
SETTINGS_DATA_DIRNo./data/settingsDirectory for encrypted settings storage (resolves to /app/data/settings in Docker)
ADMIN_PASSWORDNoset via wizard or random on first startInitial admin dashboard password (overrides whatever the wizard wrote)
ADMIN_CONFIG_DIRNo./data/adminOperator-authored: config.json, policy.json, admin.json (passwordHash), plugins, themes, branding uploads. Safe to mount read-only after setup
ADMIN_STATE_DIRNo./data/admin-stateRuntime: admin-state.json (login timestamps), audit.log, setup token. Always read-write
ADMIN_CONFIG_READONLYNofalseEnforce read-only mode at the app layer (pair with :ro mount of the config volume)
ADMIN_DATA_DIRNo-Legacy single dir used by pre-1.6.4 installs. Honoured when neither split var is set
ADMIN_SESSION_TTLNosafe defaultAdmin session lifetime in seconds
BULWARK_TELEMETRYNooffSet to on to enable the anonymous daily heartbeat. Setting it either way locks the admin toggle
BULWARK_TELEMETRY_URLNohttps://telemetry.bulwarkmail.org/...Point at your own collector, or clear to disable
TELEMETRY_DATA_DIRNo./data/telemetryWhere the instance id and consent live; mount a volume to survive upgrades
BULWARK_UPDATE_CHECKNoonSet to off to stop the startup release check and the in-app update notice
BULWARK_UPDATE_CHECK_URLNoproject release feedPoint the check at your own feed; an empty value disables it
VERSION_CHECK_DATA_DIRNo./data/version-checkWhere the update check stores its state
TRUSTED_PROXY_DEPTHNo1Number of X-Forwarded-For hops to trust
SEARCH_ENGINE_INDEXINGNofalseAllow search engines to index the app; off emits noindex/nofollow
EXTENSION_DIRECTORY_URLNohttps://extensions.bulwarkmail.orgMarketplace URL for browsing and installing plugins/themes; clear it to hide the marketplace
DEMO_MODENofalseServe fixture data instead of talking to a mail server
PLUGIN_DEV_DIRNoemptyLoad plugins from a source folder with hot reload during development
LOG_FORMATNotextLog output format: text or json
LOG_LEVELNoinfoLog verbosity: error, warn, info, or debug
FAVICON_URLNoBulwark faviconCustom browser tab favicon (SVG, PNG, or ICO; 32-512px)
PWA_ICON_URLNofalls back to FAVICON_URLSource image used to generate PWA install icons
PWA_THEME_COLORNo#ffffffPWA browser UI chrome color
PWA_BACKGROUND_COLORNo#ffffffPWA splash screen background
APP_LOGO_LIGHT_URLNoemptySidebar logo for light mode (SVG, PNG, or WebP; 24-128px)
APP_LOGO_DARK_URLNoemptySidebar logo for dark mode (SVG, PNG, or WebP; 24-128px)
LOGIN_LOGO_LIGHT_URLNoBulwark light logoLogin page logo for light backgrounds (SVG, PNG, or WebP; 32-512px)
LOGIN_LOGO_DARK_URLNoBulwark dark logoLogin page logo for dark backgrounds (SVG, PNG, or WebP; 32-512px)
LOGIN_COMPANY_NAMENoemptyCompany name shown on the login page
LOGIN_IMPRINT_URLNoemptyLogin page imprint / legal notice link
LOGIN_PRIVACY_POLICY_URLNoemptyLogin page privacy policy link
LOGIN_WEBSITE_URLNoemptyLogin page website link
LOGIN_LOGO_MAX_HEIGHT / _WIDTHNonatural sizeCap the rendered login logo; any CSS length
LOGIN_SHOW_HEADING / _SUBTITLE / _TOTP / _VERSIONNotrueHide individual login page elements
DOMAIN_BRANDINGNoemptyJSON array of per-hostname branding overrides, matched on Host; *. wildcards allowed
NEXT_PUBLIC_DEFAULT_LOCALEBuild-timeenFallback UI locale when Accept-Language matches nothing supported
NEXT_PUBLIC_LOCALE_PREFIXBuild-timeneverLocale URL prefix mode: always, as-needed, or never
NEXT_PUBLIC_BASE_PATHBuild-timeemptyMount Bulwark under a subpath (e.g. /webmail) - read at build time by Next.js
NEXT_PUBLIC_PUSH_RELAY_URLBuild-timehttps://notifications.relay.bulwarkmail.orgPush relay used for web push; point at your own to avoid the hosted one
AUTO_SSO_ENABLEDNofalseAutomatically start OAuth flow on login page (for embedded SSO)
ALLOWED_FRAME_ANCESTORSNo'none'CSP frame-ancestors value for iframe embedding
NEXT_PUBLIC_PARENT_ORIGINNoemptyOrigin of parent frame for postMessage validation
BULWARK_JWT_AUTH_SECRETNoemptyHS256 key enabling master-user impersonation; the route 404s while unset
BULWARK_STALWART_MASTER_USER / _PASSWORDNoemptyStalwart Admin-role account used for impersonation
LIBRETRANSLATE_URL / _API_KEYNopublic MyMemory APIPoint /api/translate at a LibreTranslate instance you control
STALWART_VERSIONNoprobed from the Server headerReport a fixed Stalwart version when a proxy strips that header
NEXT_PUBLIC_APP_NAMELegacy fallback-Legacy build-time fallback for APP_NAME
NEXT_PUBLIC_JMAP_SERVER_URLLegacy fallback-Legacy build-time fallback for JMAP_SERVER_URL

¹ JMAP_SERVER_URL is required unless ALLOW_CUSTOM_JMAP_ENDPOINT=true, in which case users supply the URL on the login form.

Full reference

The table above is the short version. Every setting in .env.example is written up individually, with defaults, dependencies, and the reasoning behind each one, on the environment reference page.

STALWART_API_URL is the one variable you may still find in old guides. Stalwart 0.16 dropped the REST self-service API it pointed at, Bulwark stopped reading it in 1.5.0, and it has since been removed from .env.example entirely. Delete it from your config.

Stalwart server setup

Bulwark requires a Stalwart Mail Server with JMAP enabled. Make sure your Stalwart configuration includes:

[server.listener.jmap]
bind = ["0.0.0.0:8080"]
protocol = "http"

CORS configuration

If Bulwark and Stalwart are on different domains, configure CORS in Stalwart:

[server.http]
permissive-cors = true

Authentication

Bulwark uses JMAP's built-in authentication. Users log in with their email credentials configured in Stalwart. Supported authentication methods:

  • Basic Auth - Username and password
  • OAuth 2.0 - If configured in Stalwart
  • OAuth App Passwords - For environments using OAuth as primary auth

Custom JMAP server endpoints

Users can enter a JMAP server URL on the login page and in settings, so they can connect to a different server without an environment variable change. This is useful for multi-server deployments and for testing.

Theming

Light and dark themes both ship with Bulwark. The preference is stored in the browser's local storage and follows the system setting by default.

To set a default theme, you can customize the CSS variables in your deployment. See the Customization page for details.