WarmblyDocs

Deploying without Docker

Step-by-step instructions for running Warmbly as native systemd services on one Linux host, built from source, with no containers anywhere.

The self-hosting guide assumes Docker Compose. Nothing in Warmbly needs a container, though: every service is a single binary or a static build that reads its configuration from environment variables. This page walks through building those artifacts from source and running them under systemd on one Linux host, with nginx in front.

It is longer than the compose route because it does by hand what the compose file does for you: installing the backing services, generating secrets, fanning one set of values out to five processes, and serving two static frontends. Read it once end to end before starting; the quick reference at the bottom is enough the second time.

Before you start

You needWhy
A Linux host with systemdThe units below; Debian 12, Ubuntu 22.04+ and their relatives are known to work
Root or sudoTo install packages, create the service user, and write under /etc and /opt
PostgreSQL 16, Redis 7, NATS 2.10+The backing services. Any packaging works: distro packages, upstream repositories, or a managed instance elsewhere
Go 1.25Builds the backend, consumer, worker, migrate and warmblyctl
Rust (stable, 1.93 or newer)Builds the tracking service
Elixir 1.18 on OTP 26Builds the realtime service
Node 22 and pnpmBuilds the dashboard and admin panel
nginxServes the static frontends and terminates TLS for the API, websocket and tracking hosts
Five DNS namesapp, admin, api, ws and t under your domain, all pointing at the host

The toolchains are only needed on the machine that builds. If you would rather not install compilers on the production host, build on another machine of the same architecture and copy /opt/warmbly over; nothing below is path-dependent beyond the values you put in the env file.

Not a development setup

For working on Warmbly itself use make dev, which runs the Go services natively already and only puts Postgres, Redis and NATS in containers. This page is for running the product for other people, which means real secrets, TLS and a mail relay.

Install the backing services

PostgreSQL

Install PostgreSQL 16, then create the role and database the backend will own:

sudo -u postgres psql <<'SQL'
CREATE ROLE warmbly WITH LOGIN PASSWORD 'choose-a-password';
CREATE DATABASE warmbly OWNER warmbly;
SQL

The migrations create every table and index, so nothing else is required in the database. Migrations apply automatically on every backend boot.

Redis

Install Redis 7 and leave the default configuration: localhost only, no password. Redis holds the rate limiter, the decrypted-key cache and the realtime bridge; none of it is durable state, and a restart loses nothing that matters.

If you enable requirepass, put the password in every REDIS and REDIS_URL value below as redis://:[email protected]:6379.

NATS with JetStream

NATS is one static binary. Install it from your distribution or nats.io, then run it with JetStream on, a storage directory, and both listeners bound to loopback:

sudo useradd --system --home /var/lib/nats --create-home nats
sudo tee /etc/nats.conf >/dev/null <<'EOF'
listen: 127.0.0.1:4222
http: 127.0.0.1:8222
jetstream { store_dir: /var/lib/nats }
EOF
sudo tee /etc/systemd/system/nats.service >/dev/null <<'EOF'
[Unit]
Description=NATS server
After=network-online.target
Wants=network-online.target

[Service]
User=nats
ExecStart=/usr/local/bin/nats-server -c /etc/nats.conf
Restart=always

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload && sudo systemctl enable --now nats
curl -s http://127.0.0.1:8222/healthz

Two lines matter. jetstream is required: without it the backend starts, then fails on its first publish with a message about the stream not existing. listen: 127.0.0.1 is what keeps the bus private. Everything that goes over NATS is unauthenticated by default, and it carries worker commands (send this mail, sync this mailbox) and tracking events, so a client that can reach port 4222 can issue both. On a single host nothing but the services on that host needs it, and loopback is the whole access control.

Only open it when a worker on another machine has to reach it, and then add authentication and TLS in the same change. See workers on other machines for the configuration.

Build from source

Get the code and create the layout

sudo useradd --system --home /var/lib/warmbly --create-home --shell /usr/sbin/nologin warmbly
sudo mkdir -p /opt/warmbly/bin /etc/warmbly /var/lib/warmbly/blobs
sudo chown -R warmbly:warmbly /var/lib/warmbly
sudo chmod 0700 /etc/warmbly
sudo install -d -o "$USER" -g "$USER" /opt/warmbly/src
git clone https://github.com/warmbly/warmbly /opt/warmbly/src

The checkout belongs to you, not root: every build below runs as your normal user and writes into /opt/warmbly/src, and only the final sudo install or sudo cp of each artifact touches the root-owned /opt/warmbly/bin, /opt/warmbly/web and friends. Cloning with sudo leaves a root-owned tree that the builds cannot write to.

Checkout a release tag rather than main when you want a known version: git -C /opt/warmbly/src checkout vX.Y.Z. Everything below runs from /opt/warmbly/src.

Go services

Five binaries come out of one module. Static builds, so the production host needs no Go runtime:

cd /opt/warmbly/src
export CGO_ENABLED=0
for cmd in backend consumer worker migrate warmblyctl; do
  go build -ldflags="-s -w" -o "out/$cmd" "./cmd/$cmd"
done
sudo install -m 0755 out/backend out/consumer out/worker out/migrate out/warmblyctl /opt/warmbly/bin/
sudo ln -sf /opt/warmbly/bin/warmblyctl /usr/local/bin/warmblyctl

The default build has no Kafka support, which is what you want: NATS is the event bus. Add -tags kafka (and CGO_ENABLED=1 with librdkafka installed) only if you are pointing at an existing Kafka cluster.

Tracking service (Rust)

cd /opt/warmbly/src/tracking
cargo build --release
sudo install -m 0755 target/release/tracking /opt/warmbly/bin/tracking

The tracking snippet in static/ is compiled into the binary, so nothing else needs copying.

Realtime service (Elixir)

A mix release bundles the Erlang runtime, so the host does not need Elixir installed once the build is done:

cd /opt/warmbly/src/realtime
mix local.hex --force && mix local.rebar --force
MIX_ENV=prod mix deps.get --only prod
MIX_ENV=prod mix compile
MIX_ENV=prod mix release --overwrite
sudo rm -rf /opt/warmbly/realtime
sudo cp -r _build/prod/rel/realtime /opt/warmbly/realtime
sudo chown -R warmbly:warmbly /opt/warmbly/realtime

Build on a machine with the same libc as the host: a release built on Alpine (musl) does not start on Debian (glibc), and the other way round.

Dashboard and admin panel

Both are static Vite builds. No URL is baked in at build time; the frontends read config.js at runtime, which you write in the next step.

cd /opt/warmbly/src/web   && pnpm install --frozen-lockfile && pnpm build
cd /opt/warmbly/src/admin && pnpm install --frozen-lockfile && pnpm build
sudo rm -rf /opt/warmbly/web /opt/warmbly/admin
sudo cp -r /opt/warmbly/src/web/dist   /opt/warmbly/web
sudo cp -r /opt/warmbly/src/admin/dist /opt/warmbly/admin

Runtime config for the frontends

In the container images an entrypoint renders this file from environment variables. Without containers you write it yourself, once per frontend:

sudo tee /opt/warmbly/web/config.js >/dev/null <<'EOF'
window.__WARMBLY_ENV__ = {
  API_URL: "https://api.example.com",
  APP_URL: "https://app.example.com",
  TURNSTILE_KEY: ""
};
EOF

sudo tee /opt/warmbly/admin/config.js >/dev/null <<'EOF'
window.__WARMBLY_ENV__ = {
  API_URL: "https://api.example.com",
  DASHBOARD_URL: "https://app.example.com",
  ENV_LABEL: "production",
  TURNSTILE_KEY: ""
};
EOF
sudo chmod -R a+rX /opt/warmbly/web /opt/warmbly/admin

TURNSTILE_KEY is the Cloudflare Turnstile site key and stays empty unless you also set CAPTCHA_PROVIDER=turnstile on the backend. The file is read on every page load (nginx serves it with no-store below), so changing a URL later never needs a rebuild.

Configure

Generate the secrets

AUTH_SECRET=$(openssl rand -base64 32)
INTERNAL_API_TOKEN=$(openssl rand -hex 24)
SECRET_KEY_BASE=$(openssl rand -base64 64 | tr -d '\n')
KMS_LOCAL_MASTER_KEY=$(openssl rand -base64 32)
CREDENTIALS_ENCRYPTION_KEY=$(openssl rand -hex 32)

Back up the last two before you connect a mailbox

KMS_LOCAL_MASTER_KEY and CREDENTIALS_ENCRYPTION_KEY seal every stored credential. Losing them is unrecoverable, and a database backup without them cannot be decrypted.

Write /etc/warmbly/warmbly.env

One file feeds the backend, consumer, tracking and realtime services. Compose maps a few names between services for you (AUTH_SECRET to JWT_SECRET, PRIMARY_DB to DATABASE_URL, REDIS to REDIS_URL); here both spellings sit in the same file with the same value.

sudo tee /etc/warmbly/warmbly.env >/dev/null <<EOF
# ── Mode ─────────────────────────────────────────────────────────
APP_ENV=prod
DEPLOYMENT_MODE=self_hosted
GIN_MODE=release
AWS_CONFIG_ENABLED=false

# ── Secrets ──────────────────────────────────────────────────────
AUTH_SECRET=${AUTH_SECRET}
JWT_SECRET=${AUTH_SECRET}
INTERNAL_API_TOKEN=${INTERNAL_API_TOKEN}
SECRET_KEY_BASE=${SECRET_KEY_BASE}
KMS_PROVIDER=local
KMS_LOCAL_MASTER_KEY=${KMS_LOCAL_MASTER_KEY}
CREDENTIALS_ENCRYPTION_KEY=${CREDENTIALS_ENCRYPTION_KEY}

# ── Backing services ─────────────────────────────────────────────
PRIMARY_DB=postgres://warmbly:[email protected]:5432/warmbly?sslmode=disable
DATABASE_URL=postgres://warmbly:[email protected]:5432/warmbly?sslmode=disable
REDIS=redis://127.0.0.1:6379
REDIS_URL=redis://127.0.0.1:6379
EVENTBUS_PROVIDER=nats
NATS_URL=nats://127.0.0.1:4222
CODEC_PROVIDER=json
PUBSUB_ENABLED=false
ENCRYPTED_KEYS_PROVIDER=postgres
TASKS_PROVIDER=local
BILLING_PROVIDER=none
CAPTCHA_PROVIDER=none

# ── Storage ──────────────────────────────────────────────────────
BLOB_PROVIDER=filesystem
BLOB_FS_ROOT=/var/lib/warmbly/blobs
BLOB_PUBLIC_BASE_URL=https://api.example.com/public

# ── Where it lives ───────────────────────────────────────────────
API_HOST=127.0.0.1:8080
API_PUBLIC_URL=https://api.example.com
APP_URL=https://app.example.com
CORS_ALLOW_ORIGINS=https://app.example.com,https://admin.example.com
WEBSOCKET_URL=wss://ws.example.com/socket/websocket
TRACKING_DOMAIN=t.example.com
TRUSTED_PROXIES=127.0.0.1/32

# ── Tracking service ─────────────────────────────────────────────
TRACKING_HOST=127.0.0.1
TRACKING_PORT=3000
BACKEND_INTERNAL_URL=http://127.0.0.1:8080
TRACKING_TRUSTED_PROXIES=127.0.0.1/32

# ── Realtime service ─────────────────────────────────────────────
PHX_HOST=ws.example.com
PORT=4000
CHECK_ORIGIN=true

# ── Platform email (resets, invitations, digests) ────────────────
MAIL_TRANSPORT=smtp
SMTP_HOST=smtp.example.com
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_SECURITY=starttls
EMAIL_NAME=Warmbly
[email protected]

# ── Connecting mailboxes (optional) ──────────────────────────────
BOX_GOOGLE_CLIENT_ID=
BOX_GOOGLE_CLIENT_SECRET=
BOX_OUTLOOK_CLIENT_ID=
BOX_OUTLOOK_CLIENT_SECRET=
EOF
sudo chmod 0600 /etc/warmbly/warmbly.env
sudo chown warmbly:warmbly /etc/warmbly/warmbly.env

A few lines differ from the compose defaults on purpose:

  • API_HOST, TRACKING_HOST and the realtime PORT sit on 127.0.0.1, so only nginx reaches them. The realtime service binds all interfaces regardless, so keep 4000 closed at the firewall
  • TRUSTED_PROXIES and TRACKING_TRUSTED_PROXIES name the proxy, so rate limits and audit records see the visitor's address instead of 127.0.0.1
  • CHECK_ORIGIN=true makes the websocket refuse browsers that are not on PHX_HOST's origin list
  • GEODB_PATH is left unset. It only adds a city to sessions and audit rows; set it to a MaxMind GeoLite2-City.mmdb if you have one

Every other variable, with its default, is in the configuration reference, and deploy/config/env.example is the annotated template this file was cut down from. Mail relay, OAuth clients, single sign-on and the AI provider are configured exactly as in the self-hosting guide; only the way values reach the process differs.

Write /etc/warmbly/worker.env

The worker is deliberately blind to the database: it reaches encrypted keys over the backend's internal API and holds only what it needs to send. Give it its own file:

sudo tee /etc/warmbly/worker.env >/dev/null <<EOF
APP_ENV=prod
AWS_CONFIG_ENABLED=false
WORKER_ID=$(uuidgen)
WORKER_TIER=shared_premium

EVENTBUS_PROVIDER=nats
NATS_URL=nats://127.0.0.1:4222
CODEC_PROVIDER=json
REDIS=redis://127.0.0.1:6379

ENCRYPTED_KEYS_PROVIDER=http
ENCRYPTED_KEYS_BACKEND_URL=http://127.0.0.1:8080
ENCRYPTED_KEYS_WORKER_TOKEN=${INTERNAL_API_TOKEN}
KMS_PROVIDER=local
KMS_LOCAL_MASTER_KEY=${KMS_LOCAL_MASTER_KEY}
CREDENTIALS_ENCRYPTION_KEY=${CREDENTIALS_ENCRYPTION_KEY}

BLOB_PROVIDER=filesystem
BLOB_FS_ROOT=/var/lib/warmbly/blobs

BOX_GOOGLE_CLIENT_ID=
BOX_GOOGLE_CLIENT_SECRET=
BOX_OUTLOOK_CLIENT_ID=
BOX_OUTLOOK_CLIENT_SECRET=
EOF
sudo chmod 0600 /etc/warmbly/worker.env
sudo chown warmbly:warmbly /etc/warmbly/worker.env

WORKER_ID is generated once and never changed: it is the worker's identity, and the mailboxes assigned to it follow that UUID. A worker that boots with a fresh id every time leaves the old one behind still holding its mailboxes, and sending stalls until the reconciler notices. The BOX_* values must match the backend's, because the worker is what refreshes an expiring OAuth token.

Run it

Install the units

The repository ships one unit per service in deploy/systemd/, already pointed at the paths above and locked down to the warmbly user:

sudo cp /opt/warmbly/src/deploy/systemd/warmbly-*.service /etc/systemd/system/
sudo systemctl daemon-reload

Start the backend first

The backend applies migrations on boot, and everything else expects the schema to exist:

sudo systemctl enable --now warmbly-backend
sudo journalctl -u warmbly-backend -f

Watch for the migrations to finish and curl http://127.0.0.1:8080/health to answer 200. A prod boot refuses any of the published development secrets, so a message naming a variable means the env file still has a placeholder in it.

To apply migrations without starting the API, for instance in a deploy script, sudo -u warmbly env $(grep PRIMARY_DB /etc/warmbly/warmbly.env) /opt/warmbly/bin/migrate runs the same embedded migrations and exits.

Start the rest

sudo systemctl enable --now warmbly-consumer warmbly-tracking warmbly-realtime warmbly-worker
systemctl status 'warmbly-*' --no-pager

Each answers on its own port:

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/health   # backend
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/health   # tracking
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4000/health   # realtime

The consumer and worker have no port; journalctl -u warmbly-worker shows the worker registering and heartbeating, and it appears in the admin panel under Workers once you can sign in.

Put nginx in front

deploy/nginx/warmbly.conf serves the two static frontends and proxies the API, websocket and tracking hosts. It references Let's Encrypt certificate files by path, so the certificates have to exist before nginx will accept the site. Issue them first, in standalone mode with nothing on port 80, then enable the site:

sudo systemctl stop nginx
sudo certbot certonly --standalone --agree-tos -m [email protected] \
  -d app.yourdomain.com -d admin.yourdomain.com -d api.yourdomain.com \
  -d ws.yourdomain.com -d t.yourdomain.com \
  --cert-name app.yourdomain.com

sudo cp /opt/warmbly/src/deploy/nginx/warmbly.conf /etc/nginx/sites-available/warmbly.conf
sudo sed -i 's/example\.com/yourdomain.com/g' /etc/nginx/sites-available/warmbly.conf
sudo ln -s /etc/nginx/sites-available/warmbly.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl start nginx

--cert-name app.yourdomain.com puts all five names on one certificate under /etc/letsencrypt/live/app.yourdomain.com/, which is the path every server block in the shipped file uses. Renewals run from the timer certbot installs; because the certificate was issued standalone, tell it how to reload nginx once:

sudo certbot renew --dry-run --pre-hook 'systemctl stop nginx' --post-hook 'systemctl start nginx'

Those hooks are saved in the renewal config and reused on every real renewal.

The frontends are plain files under /opt/warmbly/web and /opt/warmbly/admin with a history fallback to index.html, and index.html and config.js are served with Cache-Control: no-store so a redeploy is picked up on the next load. Any other web server can do the same; the config file has the exact headers.

Claim the instance

On its first boot with an empty users table the backend prints a single-use setup link to its log:

sudo journalctl -u warmbly-backend | grep -o 'http[^ ]*/setup?token=[a-f0-9]*' | tail -1

Open it at https://app.yourdomain.com, pick a password, and you are the owner and platform admin. If the line has scrolled away, warmblyctl setup-link prints a fresh one:

sudo -u warmbly env $(grep -E '^(PRIMARY_DB|REDIS|AUTH_SECRET|APP_URL)=' /etc/warmbly/warmbly.env | xargs) warmblyctl setup-link

warmblyctl is the same operator CLI the container image ships; it reads PRIMARY_DB and friends from the environment, so the env $(grep ...) prefix above is how every command on the warmblyctl reference is run on a bare host. A shell alias saves typing it:

alias warmblyctl='sudo -u warmbly env $(sudo grep -E "^(PRIMARY_DB|REDIS|AUTH_SECRET|APP_URL|API_PUBLIC_URL|MAIL_TRANSPORT|SMTP_HOST|EMAIL_ADDRESS|EMAIL_NAME)=" /etc/warmbly/warmbly.env | xargs) /opt/warmbly/bin/warmblyctl'
warmblyctl status

First run covers what to do when the database already has accounts, and unattended provisioning through WARMBLY_BOOTSTRAP_EMAIL.

Workers on other machines

The point of workers is to spread sending across machine identities, so most installs eventually add a worker on another host. Two routes, neither needing Docker on the control plane:

The enrollment installer, which is what the admin panel's Add Worker flow produces, runs the worker as a container on the remote host. It needs Docker only there. The backend serves it at GET /worker-install.sh from WORKER_INSTALLER_PATH, which the shipped unit already points at the checkout.

A native worker is the same binary and env file as above, with the addresses changed. The backend renders a complete env file for an enrollment token, so nothing has to be copied by hand:

# On the remote host, with /opt/warmbly/bin/worker built for it:
curl -fsS https://api.yourdomain.com/api/v1/workers/enroll \
  -H 'Content-Type: application/json' \
  -d '{"token":"wmenroll_..."}' | sudo tee /etc/warmbly/worker.env >/dev/null
echo "WORKER_ID=$(uuidgen)" | sudo tee -a /etc/warmbly/worker.env >/dev/null
sudo chmod 0600 /etc/warmbly/worker.env
sudo cp deploy/systemd/warmbly-worker.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now warmbly-worker

The token is consumed on first use and the response carries the decryption material, so do this over HTTPS only. Before enrolling anything, set API_PUBLIC_URL, NATS_URL and REDIS on the backend to addresses the remote host can reach, and open those ports to it: the rendered file inherits the backend's own values, and 127.0.0.1 does not resolve to your control plane from another machine.

Opening NATS and Redis to another machine means they stop being protected by the loopback bind, so give both a credential and TLS first. For NATS, extend /etc/nats.conf and restart it:

listen: 0.0.0.0:4222
http: 127.0.0.1:8222
jetstream { store_dir: /var/lib/nats }
authorization { token: "<openssl rand -hex 32>" }
tls {
  cert_file: "/etc/letsencrypt/live/app.yourdomain.com/fullchain.pem"
  key_file:  "/etc/letsencrypt/live/app.yourdomain.com/privkey.pem"
}

Then set NATS_URL=tls://<token>@nats.yourdomain.com:4222 in warmbly.env and worker.env; the token in the URL is what every service authenticates with. Redis gets requirepass and REDIS=redis://:<password>@redis.yourdomain.com:6379, ideally over a private network or a tunnel. Restrict both ports at the firewall to the worker's address as well. Use BLOB_PROVIDER=s3 with a bucket both sides can reach; a remote worker on the filesystem provider writes to its own disk. The self-hosting guide has the same caveats in more detail.

Because the worker is not in a container, the admin panel's SSH-driven day-two actions (pull image, restart container) do not apply to it. Manage it with systemctl and the update steps below.

Upgrading

Rebuild the artifacts that changed, then restart. Migrations are forward-only and apply on backend boot, so bring the backend up before the rest:

cd /opt/warmbly/src && sudo git pull                     # or checkout a newer tag
# repeat the build steps for the services that changed, then:
sudo systemctl restart warmbly-backend
sudo systemctl restart warmbly-consumer warmbly-tracking warmbly-realtime warmbly-worker

Frontend rebuilds overwrite config.js when you copy dist/ over, so rewrite it afterwards (or keep the two files somewhere and copy them back). A build script that does all of it in order is worth writing the second time you upgrade.

Backups

Three things, all three:

sudo -u postgres pg_dump warmbly > backup.sql       # 1. the database
sudo tar czf blobs.tgz -C /var/lib/warmbly blobs      # 2. uploads and stored message bodies
sudo cp /etc/warmbly/*.env somewhere-safe/            # 3. the env files, above all the two encryption keys

Restore into an empty database before the backend starts, then start it and it applies only what is newer than the dump. See restoring; replace the docker compose exec commands with plain psql.

Troubleshooting

SymptomCause
Backend exits at boot naming AUTH_SECRET or another variableAPP_ENV=prod refuses the published development defaults; the env file still holds a placeholder
First publish fails with a stream or JetStream error/etc/nats.conf has no jetstream block
go build or cargo build fails with permission deniedThe checkout was cloned with sudo and is root-owned; chown -R "$USER" it and build as yourself
nginx -t fails on a missing certificate fileThe site was enabled before certbot certonly ran, or --cert-name does not match the path in the file
mkdir /var/lib/warmbly/blobs/...: permission denied on the first sendBLOB_FS_ROOT is not writable by the warmbly user, or the unit's ReadWritePaths does not cover it
Realtime unit starts and exits immediatelyA release only listens when PHX_SERVER=true; the shipped unit sets it. Otherwise journalctl -u warmbly-realtime names the missing variable
Realtime raises JWT_SECRET ... requiredProd releases read JWT_SECRET, SECRET_KEY_BASE and DATABASE_URL, not the backend's names; keep both spellings in the file
Dashboard loads but every request is 403CORS_ALLOW_ORIGINS does not list the origin the browser is on; it must match the APP_URL you wrote into config.js
Dashboard never goes live, notifications stay silentWEBSOCKET_URL is wrong, or CHECK_ORIGIN=true with a PHX_HOST that is not the dashboard's host
Worker logs encryptedkeys.http: ... unexpected status 401ENCRYPTED_KEYS_WORKER_TOKEN does not equal the backend's INTERNAL_API_TOKEN
Sends fail with email account not found in workerThe worker booted with a new WORKER_ID; the mailboxes still belong to the old one until the reconciler moves them. Pin the id
Opens and clicks are not recordedTRACKING_DOMAIN does not match the host nginx proxies to :3000, or BACKEND_INTERNAL_URL is unreachable from the tracking service
Mailbox connects, then fails about an hour laterThe worker env is missing the BOX_* OAuth client; only the worker refreshes tokens

warmblyctl status runs the same instance checks the admin panel's System Status page shows and exits non-zero on anything at error severity. The troubleshooting page covers symptoms that are not specific to running without Docker.

Quick reference

/opt/warmbly/bin/{backend,consumer,worker,tracking,migrate,warmblyctl}
/opt/warmbly/realtime/bin/realtime        mix release
/opt/warmbly/web, /opt/warmbly/admin      static builds + config.js
/opt/warmbly/src                          the checkout (worker installer is served from here)
/etc/warmbly/warmbly.env                  backend, consumer, tracking, realtime
/etc/warmbly/worker.env                   worker
/var/lib/warmbly/blobs                    BLOB_FS_ROOT
/etc/systemd/system/warmbly-*.service     from deploy/systemd/
/etc/nginx/sites-available/warmbly.conf   from deploy/nginx/
systemctl status 'warmbly-*'
journalctl -u warmbly-backend -f
warmblyctl status                # with the alias above
warmblyctl setup-link

See also: self-hosting with Docker, configuration reference, warmblyctl, architecture.

On this page