Skip to content

Installation & deployment

moth is a single static binary with a single data directory. A deployment is: the binary, a config file (optional), and something that gives it TLS.

Coming in v1.0: signed release binaries for darwin/linux/windows (amd64/arm64), a Homebrew tap, and a ~15 MB scratch-based Docker image.

Today, build from source with Go 1.25+:

Terminal window
git clone https://github.com/aloisdeniel/moth.git
cd moth
make build # → bin/moth (dev build)
make build VERSION=… # release-style build

The binary is fully self-contained — the admin SPA, migrations, email templates, and the Flutter SDK tarball are embedded. Copy bin/moth to the target machine and you’re done installing.

Resolution order, highest first: command-line flagsMOTH_* environment variablesconfig file (moth.toml, loaded from the working directory when present, or --config <path> / MOTH_CONFIG) → built-in defaults.

Setting Flag Env moth.toml Default
Listen address --addr MOTH_ADDR addr :8080
Data directory --data-dir MOTH_DATA_DIR data_dir ./data
Public base URL --base-url MOTH_BASE_URL base_url http://localhost:8080
SMTP host MOTH_SMTP_HOST [smtp] host (empty → console transport)
SMTP port MOTH_SMTP_PORT [smtp] port 587
SMTP username MOTH_SMTP_USERNAME [smtp] username
SMTP password MOTH_SMTP_PASSWORD [smtp] password
SMTP from MOTH_SMTP_FROM [smtp] from
Log format (text/json) --log-format MOTH_LOG_FORMAT log_format text
gRPC reflection (release builds) --reflection MOTH_REFLECTION reflection false (dev builds: on)
Scheduled backup dir --backup-dir MOTH_BACKUP_DIR backup_dir (empty → disabled)
Scheduled backup interval --backup-interval MOTH_BACKUP_INTERVAL backup_interval 24h
Trusted proxy CIDRs/IPs --trusted-proxies MOTH_TRUSTED_PROXIES trusted_proxies (none)
ACME hostname(s) --acme-domain MOTH_ACME_DOMAINS acme_domains (empty → disabled)
Rate limit — per IP / account / project per minute MOTH_RATELIMIT_IP_PER_MINUTE, …_ACCOUNT_…, …_PROJECT_… [ratelimit] 60 / 10 / 600

base_url matters: it is baked into email links, the token iss claim, JWKS URLs, and OAuth redirect URIs, and it decides whether admin session cookies are marked Secure. Set it to the exact public URL (https://auth.example.com) before creating projects.

A production moth.toml:

/etc/moth/moth.toml
addr = ":8080"
data_dir = "/var/lib/moth/data"
base_url = "https://auth.example.com"
[smtp]
host = "smtp.eu.example.com"
port = 587
username = "moth"
password = ""
from = "auth@example.com"

SMTP can also be configured at runtime — stored in the database, taking precedence over the file — via the admin console or moth instance smtp set, and tested with moth instance smtp test --to you@example.com. With no SMTP configured anywhere, emails are printed to the server log (fine for dev, a visible warning in the admin for production).

Everything stateful lives under one directory:

data/
moth.db SQLite database (users, projects, tokens, events)
keys/ master.key + nothing else you should touch
uploads/ project logo assets
  • keys/master.key encrypts project signing keys and provider secrets at rest. Alternatively supply it as the MOTH_MASTER_KEY environment variable and keep no key file on disk. Losing the master key means losing every project’s signing keys — see Backups.
  • Back up the whole directory, not just the database.
/etc/systemd/system/moth.service
[Unit]
Description=moth authentication server
After=network-online.target
Wants=network-online.target
[Service]
User=moth
Group=moth
ExecStart=/usr/local/bin/moth serve --config /etc/moth/moth.toml
Restart=on-failure
RestartSec=2
# Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/moth
[Install]
WantedBy=multi-user.target
Terminal window
sudo useradd --system --home /var/lib/moth --shell /usr/sbin/nologin moth
sudo mkdir -p /var/lib/moth && sudo chown moth:moth /var/lib/moth
sudo systemctl enable --now moth

Create the first admin on the host (it talks to the same database):

Terminal window
sudo -u moth moth admin create --config /etc/moth/moth.toml --email you@example.com

— or just open https://auth.example.com/admin and use the first-run setup screen.

Coming in v1.0: an official scratch-based image. Until then a build-your-own image is a few lines:

Dockerfile
FROM golang:1.25 AS build
WORKDIR /src
RUN git clone --depth 1 https://github.com/aloisdeniel/moth.git . \
&& CGO_ENABLED=0 make build
FROM gcr.io/distroless/static
COPY --from=build /src/bin/moth /moth
VOLUME /data
EXPOSE 8080
ENTRYPOINT ["/moth", "serve", "--data-dir", "/data"]
Terminal window
docker build -t moth .
docker run -d -p 8080:8080 -v moth-data:/data \
-e MOTH_BASE_URL=https://auth.example.com \
-e MOTH_MASTER_KEY=… \
moth

moth is CGO-free (pure-Go SQLite driver), so the static/distroless base works without glibc. Mount /data — it is the entire state of the instance.

A minimal stack with a named volume for the data directory and Caddy in front for automatic TLS:

docker-compose.yml
services:
moth:
image: ghcr.io/aloisdeniel/moth:latest # or `build: .`
restart: unless-stopped
environment:
MOTH_BASE_URL: https://auth.example.com
MOTH_MASTER_KEY: ${MOTH_MASTER_KEY} # from a .env file / secret
volumes:
- moth-data:/data
expose:
- "8080"
caddy:
image: caddy:2
restart: unless-stopped
ports: ["80:80", "443:443"]
command: caddy reverse-proxy --from auth.example.com --to h2c://moth:8080
volumes:
- caddy-data:/data
volumes:
moth-data:
caddy-data:

caddy:2’s reverse-proxy --to h2c://… keeps the upstream on HTTP/2 so native gRPC from the mobile SDK works end to end. Generate a master key once (openssl rand -base64 32) and keep it in the .env — losing it means losing every project’s signing keys.

moth listens on plain HTTP and speaks HTTP/2 without TLS (h2c) on the same port, so a proxy can sit in front of everything. The one requirement that trips people up:

Caddy terminates TLS with automatic Let’s Encrypt certificates and can proxy h2c upstream — the entire config:

Caddyfile
auth.example.com {
reverse_proxy h2c://127.0.0.1:8080
}

The h2c:// scheme is the important part: it keeps proxy → moth on HTTP/2 so native gRPC round-trips.

# dynamic configuration
http:
routers:
moth:
rule: Host(`auth.example.com`)
service: moth
tls:
certResolver: letsencrypt
services:
moth:
loadBalancer:
servers:
- url: h2c://127.0.0.1:8080

nginx cannot proxy generic HTTP/2 upstream: proxy_pass is HTTP/1.1-only and grpc_pass handles only gRPC framing — but every protocol moth serves shares the same /moth.* paths, so a clean path-based split is not possible. proxy_pass http://127.0.0.1:8080 keeps everything working except native gRPC from the mobile SDK. A hardened, tested nginx recipe (routing on Content-Type: application/grpc) ships with the v1.0 deployment guide; until then, prefer Caddy or Traefik in front of moth.

moth serve --acme-domain auth.example.com obtains a Let’s Encrypt certificate and serves HTTPS directly from the binary on a bare VPS, no proxy at all — it listens on :443 with the http-01 challenge on :80 (pass a comma-separated list for multiple hostnames). Behind a proxy instead, set --trusted-proxies to the proxy’s CIDRs/IPs so per-IP rate limits read the real client address from X-Forwarded-For.

  • GET /healthz — plain-HTTP liveness (also: the standard gRPC health service).
  • moth doctor — the support checklist for “login stopped working”: base-URL/TLS sanity, health and pub endpoints, SMTP (with a real test send), and per-project JWKS + provider verification against Google’s and Apple’s live endpoints.
  • GET /metrics — a Prometheus endpoint (requires the admin credential).
  • Structured logs — --log-format json switches the slog handler from human-readable text to JSON for log shippers.
  • An append-only audit log records every admin action; browse it in the console or via moth.admin.v1.
  • gRPC server reflection is enabled only in dev builds — release builds don’t advertise their surface unless you pass --reflection.