mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
The compose comment still called DEPLOY_WEBHOOK_REQUIRE_TIMESTAMP a requirement for a fresh timestamp header, which the route stopped accepting in strict mode once requests could be signed, and the env manifest named only the secret header and lacked the strict switch. Both now say what the route checks: the secret header, which has no replay protection, or X-Deploy-Webhook-Signature over the timestamp and body, and that the strict setting takes signed requests only. The development example says the same and points at the deploy runbook instead of a page that no longer exists.
397 lines
21 KiB
Bash
397 lines
21 KiB
Bash
# =============================================================================
|
|
# HealthLog — Environment Configuration
|
|
# =============================================================================
|
|
# Copy this file to .env and fill in the values.
|
|
# $ cp .env.example .env
|
|
#
|
|
# This is a short starter. Every variable the app reads, with what it does,
|
|
# is documented in .env.production.example.
|
|
#
|
|
# Required variables: DATABASE_URL or POSTGRES_PASSWORD,
|
|
# ENCRYPTION_KEY, API_TOKEN_HMAC_KEY
|
|
#
|
|
# Generate the two secrets with:
|
|
# $ openssl rand -hex 32 # any UNIX shell
|
|
# $ node -e 'console.log(require("crypto").randomBytes(32).toString("hex"))'
|
|
#
|
|
# All secrets must be 64-character hex strings (= 32 bytes).
|
|
# =============================================================================
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Database
|
|
# -----------------------------------------------------------------------------
|
|
# Bundled docker-compose.yml path (the common case): you do NOT set DATABASE_URL
|
|
# here. Compose CONSTRUCTS it inside the `app` service from POSTGRES_PASSWORD
|
|
# (see docker-compose.yml — the `environment:` block wins over any value in this
|
|
# file), so a DATABASE_URL line here is ignored for that stack. Set only
|
|
# POSTGRES_PASSWORD below and compose wires the connection string for you.
|
|
#
|
|
# Bare-Node / custom-compose path (`pnpm dev` against a host Postgres, or your
|
|
# own compose that does NOT derive DATABASE_URL): uncomment DATABASE_URL and set
|
|
# it explicitly. Hostname is "db" on the bundled compose network, or
|
|
# "localhost" for a host-installed Postgres.
|
|
# DATABASE_URL="postgresql://healthlog:YOUR_PASSWORD@localhost:5432/healthlog?schema=public"
|
|
|
|
# Postgres password for the bundled `db` service — and the value compose splices
|
|
# into DATABASE_URL. The quick-start appends a generated one:
|
|
# echo "POSTGRES_PASSWORD=$(openssl rand -hex 32)" >> .env
|
|
# Left commented here so that append does not create a duplicate key. Uncomment
|
|
# and set it directly instead if you prefer to edit the file by hand.
|
|
# POSTGRES_PASSWORD="generate-with-openssl-rand-hex-32"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# App URL
|
|
# -----------------------------------------------------------------------------
|
|
# The public URL where users reach the app. Behind a reverse proxy with TLS,
|
|
# this looks like https://healthlog.example.com. For local dev, leave default.
|
|
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
|
APP_URL="http://localhost:3000"
|
|
|
|
# Node environment: "development" for `pnpm dev`, "production" for Docker.
|
|
NODE_ENV="development"
|
|
|
|
# Session cookie `Secure` flag (v1.5.1).
|
|
# Defaults to NODE_ENV === "production" — the safe choice for any
|
|
# deployment terminated by HTTPS (Caddy / Traefik / Nginx in front).
|
|
# Set to `false` if you intentionally serve plain HTTP on a LAN, VPN,
|
|
# or Tailscale-only surface — the browser otherwise drops the session
|
|
# cookie on HTTP origins and login round-trips fail silently. NEVER
|
|
# set to `false` for a deployment that serves plain HTTP to the open
|
|
# internet; the session cookie crosses the wire in cleartext.
|
|
# SESSION_COOKIE_SECURE="false"
|
|
|
|
# Where this server's environment is configured: compose, truenas, coolify,
|
|
# unraid or portainer. Only picks which instructions the admin's "Back up
|
|
# your encryption key" step shows first; unset means compose.
|
|
# HEALTHLOG_PLATFORM="compose"
|
|
|
|
# Start-up encryption key check: enforce (default) refuses to serve when the
|
|
# key does not open the stored data; warn only logs it and serves anyway. A
|
|
# last resort for a refusal you are certain is false (a first start, with no
|
|
# recorded key to reset). It disables a safety check: with a wrong key every
|
|
# encrypted value fails and new rows are written under that key.
|
|
# ENCRYPTION_KEY_CHECK="enforce"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Required Secrets — generate with `openssl rand -hex 32`
|
|
# -----------------------------------------------------------------------------
|
|
# AES-256-GCM key for encrypting Withings tokens, GitHub PATs, AI provider
|
|
# keys, and VAPID private keys at rest. Rotate carefully — old ciphertexts
|
|
# need re-encryption.
|
|
ENCRYPTION_KEY="REPLACE-WITH-OUTPUT-OF-openssl-rand-hex-32"
|
|
|
|
# HMAC-SHA256 key for hashing external-API Bearer tokens before storing them
|
|
# in the database. Rotating it invalidates all issued tokens.
|
|
API_TOKEN_HMAC_KEY="REPLACE-WITH-OUTPUT-OF-openssl-rand-hex-32"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Withings OAuth (optional — for automatic scale/BP/activity sync)
|
|
# -----------------------------------------------------------------------------
|
|
# Register an app at https://developer.withings.com/ and enter its client id
|
|
# and secret in Settings -> Integrations -> Withings. There is no environment
|
|
# variable for them; only the redirect override and webhook secret live here.
|
|
# WITHINGS_REDIRECT_URI="https://healthlog.example.com/api/withings/callback"
|
|
# WITHINGS_WEBHOOK_SECRET="" # any random string — used to harden the webhook URL
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Telegram (optional — webhook hardening)
|
|
# -----------------------------------------------------------------------------
|
|
# Set to verify incoming Telegram updates. The bot token itself is configured
|
|
# per-user in Settings (encrypted at rest).
|
|
# TELEGRAM_WEBHOOK_SECRET=""
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Notification targets on a private network (optional)
|
|
# -----------------------------------------------------------------------------
|
|
# Webhook (Gotify, Home Assistant, n8n) and ntfy targets on your own network,
|
|
# comma-separated exact origins, e.g. "https://gotify.example.com,http://ntfy.lan:8080".
|
|
# A target resolving to a private address is otherwise refused at send time.
|
|
# Link-local, metadata and unspecified addresses stay refused even when listed.
|
|
# See docs/self-hosting/notifications.md.
|
|
# NOTIFICATION_PRIVATE_ORIGINS=""
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Document picker: Paperless-ngx and Papra -- optional
|
|
# -----------------------------------------------------------------------------
|
|
# The Paperless-ngx and Papra instances people may connect to pick documents
|
|
# from, comma-separated exact origins (scheme://host[:port], no path), e.g.
|
|
# "https://paperless.example.com,http://papra.lan:1221". Unset, the document
|
|
# picker is switched off. Only a listed origin is ever contacted, public or
|
|
# private; requests are pinned to it, redirects are not followed, and
|
|
# link-local, metadata and unspecified addresses stay refused even when
|
|
# listed. A malformed entry is logged once and grants nothing.
|
|
# See docs/self-hosting/document-sources.md.
|
|
# DOCUMENT_SOURCE_ORIGINS=""
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# AI Provider (optional)
|
|
# -----------------------------------------------------------------------------
|
|
# Users bring their own API keys via Settings. No server-side key needed by
|
|
# default. The admin can configure a fallback key in the Admin Panel.
|
|
|
|
# AI endpoints on your own network (optional). An AI base URL on a private
|
|
# address is refused unless its exact origin is listed here, comma-separated
|
|
# (e.g. "http://ollama.lan:11434,http://10.0.0.5:4000") — for a self-hosted
|
|
# Ollama, LM Studio or LiteLLM gateway. Link-local, metadata and unspecified
|
|
# addresses stay refused even when listed. See docs/integrations/ai-providers.md.
|
|
# AI_PRIVATE_ORIGINS=""
|
|
# Older form, kept for existing installs: an exact host list still works
|
|
# (e.g. "ollama.lan,10.0.0.5"). "true" is deprecated: it now covers only the
|
|
# admin AI key and admin accounts' own AI settings, and will be removed.
|
|
# ALLOW_LOCAL_AI_PRIVATE_HOSTS=""
|
|
|
|
# Daily-briefing output-token ceiling (optional). Default 2500, clamped to
|
|
# 500-8000. Raise it when a verbose model's briefing JSON gets cut off
|
|
# ("AI response was cut off"). See docs/integrations/ai-providers.md.
|
|
# INSIGHTS_MAX_TOKENS=""
|
|
|
|
# Maximum number of comprehensive-insight generations a single user can
|
|
# trigger per hour (POST /api/insights/generate). Default 10. The 24h
|
|
# DB cache short-circuits this for read traffic; only force=true and
|
|
# expired-cache regenerations consume tokens. Lower this when running
|
|
# on a tight LLM budget.
|
|
# INSIGHTS_RATE_LIMIT_PER_HOUR="10"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Structured Logging — Wide Events (optional, sensible defaults)
|
|
# -----------------------------------------------------------------------------
|
|
# Levels: debug | info | warn | error
|
|
# LOG_LEVEL="info"
|
|
|
|
# Sample rate for successful requests (0.0 = sample nothing, 1.0 = log all).
|
|
# Errors and slow requests are ALWAYS logged regardless of this setting.
|
|
# LOG_SAMPLE_RATE="1.0"
|
|
|
|
# Slow-request threshold in milliseconds — anything slower is always kept.
|
|
# LOG_SLOW_THRESHOLD_MS="3000"
|
|
|
|
# Include stack traces on error events. Disable in environments where logs
|
|
# leave the host unencrypted.
|
|
# LOG_INCLUDE_STACK="true"
|
|
|
|
# Heap diagnostics, off by default. HEALTHLOG_HEAP_SNAPSHOT=1 writes ONE heap
|
|
# snapshot the first time the heap reaches 85 % of its limit, into
|
|
# HEALTHLOG_DIAG_DIR (default /tmp). A snapshot contains decrypted health data
|
|
# and keys; enable it only to chase a crash and delete the file afterwards.
|
|
# See docs/ops/heap-diagnostics.md.
|
|
# HEALTHLOG_HEAP_SNAPSHOT="1"
|
|
# HEALTHLOG_DIAG_DIR="/tmp"
|
|
|
|
# Optional: ship Wide Events to Loki. Leave blank for stdout-only logging.
|
|
# LOKI_ENDPOINT="http://loki:3100"
|
|
# LOKI_USERNAME=""
|
|
# LOKI_PASSWORD=""
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Reverse-proxy trust (optional, defaults match a single-proxy stack)
|
|
# -----------------------------------------------------------------------------
|
|
# When HealthLog sits behind Cloudflare (Tunnel, Workers, classic proxy),
|
|
# the visitor IP arrives in the `CF-Connecting-IP` header — the upstream
|
|
# `X-Forwarded-For` chain stops at the Coolify / Caddy loopback and the
|
|
# admin sign-in geo lookup has no signal to resolve from.
|
|
#
|
|
# Set this to "1" ONLY when every public request to HealthLog terminates
|
|
# at Cloudflare (Tunnel, Workers, classic proxy) before it reaches your
|
|
# origin, and the origin accepts connections from Cloudflare alone (a
|
|
# Tunnel, or a firewall that admits only Cloudflare's address ranges): the
|
|
# header is believed from every connection, before any other check.
|
|
# Deployments behind nginx, Caddy, Traefik, an AWS ALB, or any
|
|
# direct-to-Next.js exposure MUST leave this off.
|
|
#
|
|
# Security implications when the flag is mis-set:
|
|
# - `cf-connecting-ip` becomes the IP-rate-limit bucket key for
|
|
# login / chat / coach. An attacker who can set the header rotates
|
|
# it per request and defeats every IP-keyed throttle.
|
|
# - The same value lands in the `audit_logs.ipAddress` column for
|
|
# every `measurement.create`, `medication.intake`, sign-in, and
|
|
# admin action. A misconfigured flag lets any caller forge the
|
|
# IP recorded against their actions across the audit trail.
|
|
# - The admin sign-in overview's "Standort" column reads the same
|
|
# value via geo-backfill; a malicious caller can poison the column
|
|
# with a chosen IP per request.
|
|
#
|
|
# Code-level validation is strict (only "=== '1'" enables, value is
|
|
# regex- and length-bounded), but the gating decision is up to the
|
|
# operator — there is no way for the server to detect whether the flag
|
|
# matches the real deployment topology. When in doubt, leave it off.
|
|
# Number of trusted reverse-proxy hops in front of the app. The client IP is
|
|
# read that many entries from the right of the X-Forwarded-For chain. Set 0
|
|
# for a directly-exposed host; 1 (the default when unset) behind a single
|
|
# reverse proxy. X-Real-IP is read only at 1. Forwarding headers are read only
|
|
# from a connection whose own address is not public (loopback, private, CGNAT,
|
|
# unique-local, link-local, reserved or multicast; the full list is in
|
|
# docs/self-hosting/reverse-proxy.md). A wrong value makes the app
|
|
# refuse the headers, log one warning, and share one rate-limit bucket across
|
|
# all anonymous callers.
|
|
# TRUST_PROXY_HOPS="1"
|
|
# TRUST_CF_CONNECTING_IP=""
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Process split (optional, single-container deploys ignore this)
|
|
# -----------------------------------------------------------------------------
|
|
# `all` — single container runs HTTP + reminder worker (default, legacy).
|
|
# `web` — HTTP only; pair with a separate `app-worker` service.
|
|
# `worker`— pg-boss only; rejects HTTP traffic with a 503.
|
|
# See docs/self-hosting/scaling.md for the split-stack guide.
|
|
# HEALTHLOG_PROCESS_TYPE="all"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Encryption-key versioning (optional — for at-rest key rotation)
|
|
# -----------------------------------------------------------------------------
|
|
# JSON map of key id → 64-hex-char or base64 32-byte key. When set, it
|
|
# supersedes ENCRYPTION_KEY for new writes; old rows continue to decrypt under
|
|
# their original id. ENCRYPTION_ACTIVE_KEY_ID picks which entry encrypt() uses.
|
|
# Run `pnpm tsx scripts/rotate-encryption-key.ts <new-id>` to re-encrypt every
|
|
# existing row under the new key. See docs/ops/encryption-key-rotation.md.
|
|
# ENCRYPTION_KEYS='{"v1":"<old-key>","v2":"<new-key>"}'
|
|
# ENCRYPTION_ACTIVE_KEY_ID="v2"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Off-host backup (optional — daily encrypted dumps to an S3-compatible bucket)
|
|
# -----------------------------------------------------------------------------
|
|
# Required ALL-OR-NONE: setting any of these without setting the rest disables
|
|
# the backup silently. The backup runs at 02:30 Europe/Berlin via pg-boss cron.
|
|
# Restore with `pnpm tsx scripts/restore-backup.ts <userId> <YYYY-MM-DD>`.
|
|
# See docs/ops/backup-restore.md for the format spec + step-by-step.
|
|
#
|
|
# The worker never deletes a BACKUP object -- its grant is PutObject, GetObject
|
|
# and AbortMultipartUpload -- so BACKUP_RETENTION_DAYS is the number you set
|
|
# the bucket's own lifecycle rule to, not something the worker enforces. The
|
|
# one key it does delete is its own connection test's `_healthcheck/<ts>.bin`,
|
|
# and that delete is best-effort: without DeleteObject on that prefix every
|
|
# Test-connection press leaves one 1-byte orphan behind, silently.
|
|
#
|
|
# BACKUP_S3_ENDPOINT="https://<account>.r2.cloudflarestorage.com"
|
|
# BACKUP_S3_BUCKET="healthlog-backups"
|
|
# BACKUP_S3_ACCESS_KEY=""
|
|
# BACKUP_S3_SECRET_KEY=""
|
|
# BACKUP_S3_REGION="auto" # "auto" for Cloudflare R2; real region for AWS S3
|
|
# BACKUP_RETENTION_DAYS="30" # the number for your bucket lifecycle rule (see above)
|
|
# BACKUP_ENCRYPTION_KEY="" # 64-hex-char or base64 32-byte; SEPARATE from ENCRYPTION_KEY
|
|
# BACKUP_ENCRYPTION_PREVIOUS_KEYS="" # retired off-host keys, comma-separated, read-only during a rotation
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Database dumps (optional, on by default)
|
|
# -----------------------------------------------------------------------------
|
|
# The whole-database dump that rebuilds a lost server: nightly, and before
|
|
# every update that brings migrations. Written to DB_BACKUP_DIR with a
|
|
# manifest beside it (row counts, key fingerprints, versions); with the
|
|
# off-host bucket above set, each nightly dump is also uploaded under
|
|
# `database/`, sealed with BACKUP_ENCRYPTION_KEY. Local dumps are not
|
|
# encrypted: they sit beside the database volume, in the same trust zone.
|
|
# Restore: `docker compose run --rm db-restore latest`. See
|
|
# docs/ops/disaster-recovery.md.
|
|
#
|
|
# Set "false" to stop the nightly dump.
|
|
# DB_BACKUP_ENABLED="true"
|
|
# Where dumps are written. The bundled compose file mounts the `backups`
|
|
# volume here; change it only together with that mount. In a container a
|
|
# directory that is not on a mount of its own sits in the container's layer
|
|
# and is lost at the next update, so no dump is kept there.
|
|
# DB_BACKUP_DIR="/var/lib/healthlog/backups"
|
|
# Five-field cron, in Europe/Berlin time like every scheduled job.
|
|
# DB_BACKUP_SCHEDULE="30 1 * * *"
|
|
# Nightly dumps kept (at least 1). The last two pre-migration dumps are kept
|
|
# besides.
|
|
# DB_BACKUP_KEEP="7"
|
|
# Set "false" to start an update without the dump before its migrations.
|
|
# DB_BACKUP_BEFORE_MIGRATE="true"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Stored backups in the database (optional)
|
|
# -----------------------------------------------------------------------------
|
|
# The largest weekly or uploaded backup of one account the app keeps, in
|
|
# megabytes of encrypted copy. Default 2048. It does not depend on the app's
|
|
# memory: a copy is written and read in pieces of about a megabyte. The limit
|
|
# only stops a runaway copy from filling the database volume, which holds the
|
|
# old and the new copy side by side while a backup runs. An account with 2.6
|
|
# million readings stores a copy of about 100 MB.
|
|
# BACKUP_MAX_STORED_MB="2048"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Audit-trail retention (optional)
|
|
# -----------------------------------------------------------------------------
|
|
# How long rows in `audit_logs` are kept. Default 365 days; the floor is 7 and
|
|
# anything below it is ignored, so a window set in seconds by mistake cannot
|
|
# empty a fresh table. A daily pg-boss job does the pruning.
|
|
#
|
|
# The number is not internal. Settings -> Shared access states it under the
|
|
# activity list, and the dialog that ends somebody's access says how long
|
|
# "who entered what" stays answerable. Both read the value the server
|
|
# resolved, so an instance running 90 says 90 everywhere.
|
|
# AUDIT_LOG_RETENTION_DAYS="365"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# APNs — Apple Push Notification service (v1.4.23+, optional)
|
|
# -----------------------------------------------------------------------------
|
|
# All four core APNS_* vars must be set together for APNs to fan out to iOS
|
|
# devices. Leaving them unset disables the channel silently — the dispatcher
|
|
# falls back to Telegram → ntfy → Web Push as before.
|
|
#
|
|
# Provision an APNs auth key (.p8) at https://developer.apple.com/account/
|
|
# under Certificates, Identifiers & Profiles → Keys → APNs.
|
|
#
|
|
# APNS_KEY (preferred for 12-factor / Coolify env-block) and APNS_KEY_FILE
|
|
# (filesystem path) are mutually exclusive — pick one. APNS_KEY accepts the
|
|
# multi-line PEM body with literal newlines OR `\n` escapes.
|
|
# APNS_KEY_ID="" # 10-char Key ID from Apple Developer (e.g. ABCDE12345)
|
|
# APNS_TEAM_ID="" # 10-char Team ID from Apple Developer
|
|
# APNS_BUNDLE_ID="" # iOS app bundle id (e.g. dev.healthlog.ios)
|
|
# APNS_KEY="" # APNs auth key as PEM body (multi-line OK with \n escapes); preferred over APNS_KEY_FILE
|
|
# APNS_KEY_FILE="" # Alternate: filesystem path to .p8 file
|
|
# APNS_PRODUCTION="" # "true" to force the production gateway; default = mirror Device.apnsEnvironment
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Auto-deploy webhook (v1.4.15 — phase C2)
|
|
# -----------------------------------------------------------------------------
|
|
# Coolify (or any CI) pings POST /api/internal/deploy-webhook after a deploy
|
|
# attempt with success/failure. A request authenticates with this shared
|
|
# secret in the `X-Deploy-Webhook-Secret` header (timing-safe compare; what
|
|
# Coolify's stock sender does, with no replay protection), or by signing:
|
|
# `X-Deploy-Webhook-Signature: sha256=<hex>`, the HMAC-SHA256 under this
|
|
# secret of the `X-Deploy-Webhook-Timestamp` value, a dot and the raw body,
|
|
# the timestamp within 5 minutes. Generate with `openssl rand -hex 32`.
|
|
# Configure the SAME value in Coolify's Notifications → Webhook
|
|
# custom-headers panel. See docs/ops/deploy.md.
|
|
# DEPLOY_WEBHOOK_SECRET=""
|
|
# "true" accepts signed requests only; leave unset for Coolify's stock sender.
|
|
# DEPLOY_WEBHOOK_REQUIRE_TIMESTAMP=""
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Outbound requests HealthLog makes on its own (optional switches)
|
|
# -----------------------------------------------------------------------------
|
|
# Each accepts "1", "true" or "yes". See .env.production.example for detail.
|
|
# PASSWORD_BREACH_CHECK_DISABLED="1" # no HaveIBeenPwned range query when a password is set
|
|
# UPDATE_CHECK_DISABLED="1" # no daily GitHub releases request
|
|
# IP_GEO_LOOKUP_DISABLED="1" # no online IP geolocation for login events
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# IP geolocation (optional)
|
|
# -----------------------------------------------------------------------------
|
|
# Opt-in for a plain-HTTP IP_GEO_LOOKUP_URL provider (e.g. free ip-api.com); off by default — looked-up IPs then travel unencrypted over your server's egress.
|
|
# IP_GEO_ALLOW_INSECURE="true"
|
|
#
|
|
# Directory the offline GeoLite2 databases are read from; default /opt/geolite2 (the path the image bakes its own copy into). Point it at a directory you mount yourself to bring your own MMDBs — see docs/self-hosting/geolite2.md.
|
|
# GEOLITE2_DIR="/var/lib/geolite2"
|
|
#
|
|
# MaxMind licence key for the runtime GeoLite2 fetch (issue #659): set it and the worker downloads both databases into GEOLITE2_DIR itself, no rebuild or manual mount. Free key at https://www.maxmind.com/en/geolite2/signup — see docs/self-hosting/geolite2.md.
|
|
# MAXMIND_LICENSE_KEY="your-maxmind-licence-key"
|