Skip to content

Configuration

Ember reads configuration from environment variables at startup. A handful of settings can also be changed at runtime via the admin UI and persist in the app_settings table.

Required env vars

VarDescription
EMBER_SESSION_KEYsecurecookie key, 32+ random bytes. Generate via openssl rand -base64 48. Also seeds (domain-separated) the HMAC key that signs image-proxy URLs.
EMBER_ADMIN_PASSWORDFirst-run admin password. Used only when the users table is empty; change via Settings → Profile after first login. Must be at least 8 characters — a shorter or empty value makes the container exit on startup with bootstrap admin: …must be at least 8 characters rather than starting with no usable account.

Optional env vars

VarDefaultNotes
EMBER_ADDR:8080Bind address.
EMBER_DB_PATH/data/ember.dbSQLite file path.
EMBER_ADMIN_USERadminFirst-run admin username.
EMBER_OLLAMA_URLhttp://ollama:11434Ollama API endpoint.
EMBER_OLLAMA_MODELqwen2.5:0.5bInitial active model. The admin UI can swap to any pulled model live.
EMBER_DISABLE_SUMMARIES0Skip LLM summarization entirely. Articles still surface (poller stamps summary_model='disabled').
EMBER_DISABLE_IMAGES0Drop article hero images at ingest.
EMBER_ALLOW_PRIVATE_URLS0Bypass the SSRF private-IP block so feeds on RFC1918 / loopback addresses can be subscribed. The scheme allowlist and the non-web service-port block still apply. Only set this if you trust every user who can add feeds.
EMBER_PUBLIC_URLCanonical scheme://host users hit, e.g. https://reader.example.com. Required to enable passkey / WebAuthn sign-in.
EMBER_SECURE_COOKIES1Secure flag on the session + CSRF cookies. Ember serves plain HTTP and expects a TLS-terminating proxy in front, so leave this on. Set 0 only for a deliberate plain-HTTP deployment (e.g. private VPN) — otherwise browsers drop the cookies over HTTP and login silently fails.
EMBER_TRUSTED_PROXIESComma/space-separated CIDRs or IPs of the proxy in front of Ember. X-Real-IP (rate-limit keying) and X-Forwarded-Proto (HTTPS detection for HSTS) are honored only from these peers. Empty = trust nobody (Ember is the edge; reads the real client from the connection). The bundled compose sets this to the Caddy bridge range.
EMBER_SMTP_HOSTSMTP server hostname. Required to enable the daily-digest email feature. Can also be set per-server in Settings → Email / SMTP (admin), which takes precedence.
EMBER_SMTP_PORT587SMTP port. Overrideable in Settings → Email / SMTP.
EMBER_SMTP_USERSMTP auth username (optional; omit for relays without auth). Overrideable in Settings → Email / SMTP.
EMBER_SMTP_PASSWORDSMTP auth password. Overrideable in Settings → Email / SMTP (stored write-only — never echoed back to the UI).
EMBER_SMTP_FROMFrom: address used on digest emails. Overrideable in Settings → Email / SMTP.
EMBER_SMTP_STARTTLS1STARTTLS on submission ports (587). When on, the server must offer STARTTLS or the send fails (no silent plaintext downgrade). Set 0 only for a loopback relay (localhost / 127.0.0.1 / ::1) — plain SMTP to any remote host is refused so credentials never cross the network in the clear. Overrideable in Settings → Email / SMTP.
EMBER_FRESH_WINDOW6hHow recent an article must be to appear in the "Fresh" smart view. Fresh lists recent unread articles, matching its sidebar badge.
EMBER_POLL_CONCURRENCY8Number of feed-fetch worker goroutines.
EMBER_POLL_TICK60sHow often the poller scans for feeds due to fetch.
EMBER_POLL_MIN_INTERVAL30mFloor for the adaptive per-feed fetch interval ("check feeds every…"). Active feeds settle near this value; quiet feeds back off up to 6h. Range-validated 5m–24h. Admins can change it at runtime in Settings → (Email/Data) → Feed check interval (persisted in app_settings, overrides this env default, no restart needed).
EMBER_SESSION_TTL24hLifetime of a freshly-issued session cookie. Go duration (e.g. 30m, 12h, 168h). Range-validated (5m–90d). Admin UI override in Settings → Sessions takes precedence when set.
EMBER_LOG_LEVELinfodebug / info / warn / error.
EMBER_TEST_MODE0Seeds fake admin + articles; loosens cookie Secure flag; and (when EMBER_SESSION_KEY is unset) falls back to a hardcoded, publicly-known session signing key — session cookies become forgeable. Logs a loud warning at startup. Never enable in production.
EMBER_HSTS_PRELOAD0Appends ; preload to the Strict-Transport-Security header. Long-term, browser-level commitment — only set after the domain is (or will be) submitted to the HSTS preload list.
EMBER_EMAIL_DOMAINEnables the per-user newsletter inbox. Without it, the SMTP listener doesn't start and the inbox endpoints return enabled: false. See Email newsletter inbox for the operator setup.
EMBER_EMAIL_LISTEN_ADDR:2525Inbound SMTP bind address for the newsletter inbox. Front with Caddy layer4 / haproxy / nginx stream to publish on port 25.
EMBER_EMAIL_MAX_BYTES26214400 (25 MiB)Per-message size cap on inbound newsletter mail.

Runtime settings (persist across restarts)

Stored in the app_settings KV. Edit via the admin UI in Settings → ....

SettingWhere to change
Active LLM modelLanguage model
Temperature / Top P / Context windowLanguage model → Tuning
Session cookie TTL (overrides EMBER_SESSION_TTL)Sessions
App name, page title, favicon URLBranding
Backup schedule + retention (db_backup_keep, default 7)Database
Backup directory (db_backup_dir, default /data/backups)Database → Backups → Directory (setup)
Cleanup schedule + window (db_cleanup_older_days, default 90)Database
OPML export directory + schedule + retention (opml_export_dir default /data/exports, opml_keep default 12)Database → OPML export (setup)
SMTP host / port / username / password / from / STARTTLSEmail / SMTP
Initial feed-backlog window (default 24 hours; 0 = no gate)Email / SMTP → Initial backlog window
Reading window (default 24h; range 24–168h)Feeds → Reading window
Search window (default 48h; range 24–168h)Feeds → Search window

Custom backup directory

Database backups — the manual Back up now button and the scheduled job — are written to /data/backups by default, inside the container's /data volume. To put them on a host path you control (a specific disk, a NAS mount, a directory you already back up off-box), change the path and bind-mount it:

  1. Bind-mount a host directory into the ember service in deploy/docker-compose.yml. Alongside the existing ember-data:/data volume, add a bind mount — host path : container path:

    yaml
    services:
      ember:
        volumes:
          - ember-data:/data
          - /srv/ember/backups:/backups   # ← your host path : container path

    The host directory must be writable by the container's user. Ember's image runs as the distroless nonroot user (UID 65532), and a bind mount keeps the host's ownership — unlike the /data named volume, which is set up for you. Create the directory and hand it to that user before starting:

    sh
    sudo mkdir -p /srv/ember/backups
    sudo chown -R 65532:65532 /srv/ember/backups

    Use -R so any snapshots already in the directory (e.g. created by an earlier run under a different user) are re-owned too. This matters for deleting as well as creating: removing a file needs the directory writable by UID 65532. Without the right ownership, "Back up now" fails with … is not writable by the server (running as uid 65532), and the per-file Delete button reports "the backup directory isn't writable by the server …".

    Docker Desktop / WSL2

    A bind mount pointing at a Windows path (/mnt/c/…, C:\…) ignores chown — ownership is synthesized by the filesystem driver, so the container user can never be made the owner. Use a directory on the Linux filesystem, or keep backups on the /data named volume, if you want to manage them from the UI.

  2. Point Ember at it. In Settings → Database → Backups → Directory, enter the container-side path (/backups in the example), then Save schedule. It must be an absolute path the container can write to. Leave it empty to reset to /data/backups.

Bind-mount it first

The directory only persists on the host if you've bind-mounted it. Ember can't create the host mount for you — add it to the compose file and recreate the container before entering the container path in the UI. A path that isn't backed by a mount is lost when the container is recreated.

The same directory is used by both the scheduled job and the manual button; Keep prunes the oldest snapshots beyond the count you set.

The OPML export directory (Settings → Database → OPML export → Directory, default /data/exports) works exactly the same way — bind-mount the host path, chown -R it to UID 65532 (same ownership caveat for creating and deleting exports, and the same Windows-path limitation), then enter the container path. Its Keep prunes old exports the same way.

Time windows, retention, and counts

Ember keeps articles for a fixed rolling 1-week (168h) retention window, then prunes them automatically (a daily delete-only sweep). Starred, Read-Later, board-pinned, and shared articles are exempt and kept indefinitely. This retention window is not configurable — it's the hard ceiling for the two settings below, because you can't surface what's already been pruned.

  • Ingest — a brand-new feed pulls only the last 24h on its first fetch (the initial backlog window). Existing feeds only add genuinely new items (GUID + content-hash dedup), so reposts don't reappear.
  • Reading window (default 24h, extendable to 168h) — the floor for how far back the unread views and counts reach. Today always shows exactly this window, newest first. A feed, a category, and All Unread show at least this window but extend back to your previous login when you've been away longer (so time away surfaces everything new since), clamped at the retention window. Older articles stay in the database (searchable) but are hidden from these views. Because a feed/category list and its sidebar badge share this one cutoff, the badge counts exactly the set the column pages through — the list loads 50 at a time with a Load more button (search loads 25 at a time), so the badge is the running total and "Load more" reveals the rest.
  • Search window (default 48h, extendable to 168h) — full-text search matches articles published within this window. Results older than 24h are tagged with a date pill showing when they were polled. The window can't exceed retention — that's the safeguard.

Sidebar badges (All Unread, per-folder, per-feed) are computed server-side with the same window, summary gate, and cross-feed dedup as the article list, so a badge always matches the column it summarizes. When AI summarization is enabled, articles are hidden from every view and count until the summarizer has processed them; when it's disabled, nothing is gated.

Each user also has client-side preferences stored in browser localStorage:

PreferenceDefaultKey
ThemeAuto (OS)ember:theme
Article densityCardsember:density
Sidebar collapsedOpenember:sidebar
AI summary card visibleOnember:show-summary
Summary card collapsedOpenember:summary-collapsed
Custom theme paletten/aember:custom

Hardware-aware model recommendation

Run ember probe (or open the admin Language model page) to see a recommendation based on detected RAM, CPU count, and GPU.

RAMGPURecommended
< 2 GiBDisable summaries
2–4 GiBqwen2.5:0.5b
4–8 GiBqwen2.5:0.5b
8–16 GiBqwen2.5:1.5b
16 GiB+qwen2.5:3b
anyNVIDIA / Apple Siliconqwen2.5:7b

Stack-level env vars (docker-compose)

These configure the bundled deploy/docker-compose.yml stack rather than the Ember binary itself.

VarDefaultNotes
EMBER_HOSTNAMElocalhostHostname Caddy serves. Real DNS name → automatic Let's Encrypt.
CADDY_EMAILadmin@localhostEmail registered with Let's Encrypt for ACME notifications. Set this when using a real hostname.
EMBER_HTTP_PORT80Host port Caddy publishes for HTTP. Change when 80 is taken locally.
EMBER_HTTPS_PORT443Host port Caddy publishes for HTTPS. Change when 443 is taken locally.
EMBER_DISABLE_HTTPS_REDIRECT(unset)Set to 1 to turn off Caddy's default 80 → 443 redirect. Use when an upstream proxy already terminates TLS.

If you change the ports, reach the site at the mapped port — e.g. EMBER_HTTPS_PORT=8443 → visit https://localhost:8443. Inside the container Caddy still listens on 80/443; only the host-side mapping changes.

Let's Encrypt

Caddy fetches a free Let's Encrypt cert automatically when all three of these are true:

  1. EMBER_HOSTNAME is a real DNS name that resolves to this server (e.g. ember.example.com).
  2. CADDY_EMAIL is a valid email address.
  3. The public internet can reach port 80 (HTTP-01 challenge) or port 443 (TLS-ALPN-01 challenge) on this host.

If you remap EMBER_HTTP_PORT / EMBER_HTTPS_PORT and expect Let's Encrypt to issue certs, ensure your public ingress (router / upstream proxy / Cloudflare) still terminates on 80/443 and forwards to your mapped host ports. For homelab use with tls internal in the Caddyfile, any ports work fine.

HTTP → HTTPS redirect

Caddy redirects port 80 to 443 by default for any site with managed TLS. Set EMBER_DISABLE_HTTPS_REDIRECT=1 in .env to turn this off — needed when Ember sits behind another reverse proxy (Traefik, nginx, Cloudflare Tunnel, etc.) that already handles the redirect or terminates TLS upstream.

Reverse proxy

Ember expects TLS to be terminated upstream. The reference Caddyfile in deploy/Caddyfile covers:

  • Automatic Let's Encrypt for a real hostname (default).
  • tls internal for self-signed homelab certs.
  • EMBER_DISABLE_HTTPS_REDIRECT toggle for the 80 → 443 redirect.
  • Forwarding X-Real-IP + X-Forwarded-Proto (Ember honors these only from peers listed in EMBER_TRUSTED_PROXIES; the bundled compose sets it to the Caddy bridge range). Without it, Ember treats itself as the edge and ignores both headers.

If you front Ember with Cloudflare, set the authenticated origin pull so only Cloudflare can reach your origin.

Released under the MIT License.