Available

Self-hosting over Tailscale

Run the Loams 0.1.1 stack with Docker or Podman Compose on your laptop or your own VM, reachable only over your Tailscale tailnet, and use it from Loams Desktop.

Updated 2026-10-23

Changed: the hosted console at console.loams.dev is retired. In 0.1.1 the console is Loams Desktop; the engine serves its own console at /ui/ from 0.1.2 (merged for that release, not shipped yet). Loams Cloud, a free, invite-only try-out, will host only sign-up and the control plane.

Loams 0.1.1 ships one self-hosting path: you run the stack, on your own Tailscale tailnet, and use it from Loams Desktop, the 0.1.1 console. The engine serves a console of its own at /ui/ from 0.1.2. The Farmhouse, the console hub, says where to find them. For other places to run it, see Deploy. The engine never has a public address. Your browser reaches it through the Tailscale client on your own device, and nothing in the stack binds a host port on 0.0.0.0.

PieceWho runs itWhere
The engine stack: loams on its embedded stores, the House, Git, the Postgres stack and agentd (TiDB, TiKV and PD with the tikv profile)YouDocker or Podman Compose on your laptop, or on your own VM with byoc/install.sh over SSH
The private network between your devices and the stackYouYour own Tailscale tailnet
The consoleYouLoams Desktop on your device. The compose console profile serves a local copy of the static console for trials; the engine's own /ui/ arrives in 0.1.2
An OIDC issuer, only for oidc modeYouYour own issuer. Zitadel at auth.loams.dev is planned and does not answer yet
The console runs on your device; every API call goes over your tailnet.

The stack's tailscale service is a sidecar in the engine's network namespace. It joins your tailnet as the node loams and serves https://loams.<tailnet>.ts.net with tailscale serve, proxying to the engine's dedicated Tailscale listener (127.0.0.1:7781). Funnel stays off. Loams House runs by default, linked to the engine. With the house-tailnet profile, a second sidecar joins as loams-house and serves its ClickHouse HTTP interface.

What you need

  • Docker with the Compose plugin, v2.24.0 or later (the compose file's optional env_file entries use required: false, added in 2.24.0; include needs 2.20), or Podman with a compose provider. On macOS and Windows, Docker Desktop or Podman Desktop.
  • About 10 GB of free disk and 2 GB of free RAM for the default stack. The tikv profile (Loams SQL) adds about 3 GB of images and 2 GB of RAM; its patched TiDB image is amd64 only and runs emulated on Apple silicon.
  • A Tailscale account. The tailnet is yours; Ostrium is not in it.
  • The Tailscale client on the device that runs Loams Desktop.
  • On a cloud VM: Linux 6.7 or later for the House's workers (Landlock; Ubuntu 24.04 works, Debian 12 and Amazon Linux 2023's default kernel are too old). For EC2, GCE and other clouds, see Deploy and the cloud requirements in Running Loams 0.1.1.

Podman

The whole stack runs under podman compose. Every docker compose … command on this page has a podman compose … equivalent with the same arguments. Point the compose provider at your user's Podman socket first, and turn the socket on if it is off:

systemctl --user enable --now podman.socket
export DOCKER_HOST=unix:///run/user/$UID/podman/podman.sock
podman compose up -d

1. Create a tailnet

Sign up at tailscale.com and install the client on your own device; your first login creates the tailnet. Your tailnet's DNS name (the <tailnet>.ts.net part) is on the DNS page of the admin console (Tailscale: tailnet name).

2. Turn on MagicDNS and HTTPS certificates

In the admin console's DNS page:

  1. Enable MagicDNS, so loams.<tailnet>.ts.net resolves on your devices (Tailscale: MagicDNS).
  2. Under HTTPS Certificates, select Enable HTTPS, so the sidecar can get a certificate for that name (Tailscale: enabling HTTPS).

A console served over HTTPS can only call an https:// engine, because the browser blocks mixed content. Without HTTPS certificates such a console cannot reach your stack.

Certificate Transparency

Enabling HTTPS certificates publishes your machine names, such as loams.<tailnet>.ts.net, in the public Certificate Transparency logs, so the hostname becomes discoverable. It stays unreachable outside your tailnet. If your tailnet name itself is sensitive, Tailscale lets you select a randomized tailnet DNS name (like yak-bebop.ts.net) instead (Tailscale: enabling HTTPS).

3. Add tags and an access policy

Tag the stack's nodes so your policy can name them, and allow only your own devices to reach them, on port 443 only. In the admin console's Access controls page, add the tags' owners and a rule. This example uses the classic ACL syntax (Tailscale: tags, ACLs, policy syntax):

{
  "tagOwners": {
    "tag:loams": ["autogroup:admin"],
    "tag:loams-house": ["autogroup:admin"]
  },
  "acls": [
    // Keep the rules your tailnet already needs here, each followed by a comma.
    // A new tailnet's default policy has an allow-all rule; remove it, or every
    // device reaches every port.

    // Your own devices reach the engine and the House on HTTPS, nothing else.
    { "action": "accept", "src": ["you@example.com"], "dst": ["tag:loams:443", "tag:loams-house:443"] }
  ]
}

To admit a team, use a group ("group:data": ["alice@example.com", "bob@example.com"] under groups, then "src": ["group:data"]) or autogroup:member. To let a CI runner on the tailnet call the engine, tag it (for example tag:ci) and add { "action": "accept", "src": ["tag:ci"], "dst": ["tag:loams:443"] }; it will need an API key, as step 8 explains. Entries in acls are separated by commas; the policy editor refuses a file with one missing.

4. Create an auth key

In the admin console, go to Settings → Keys and Generate auth key (Tailscale: auth keys):

SettingChooseWhy
Tagstag:loamsThe node is owned by the tag, not by your user, and your policy names it. Tagged nodes have key expiry turned off by default
EphemeralOffThe sidecar keeps its node state in the tailscale-state volume and should stay the same node across restarts; an ephemeral node is removed when it goes offline
ReusableOffThe key is used once, on first start (TS_AUTH_ONCE). Make it reusable only if the House's sidecar will share it
Pre-approvedOn, if your tailnet requires device approvalSo the node does not wait for an admin

For the House, create a second key tagged tag:loams-house for HOUSE_TS_AUTHKEY, or leave that empty and make the first key reusable.

5. Configure and start the stack

Download the 0.1.1 compose bundle from the GitHub release, with the release's checksums and their signature, verify both, and unpack it:

base=https://github.com/ostrium-labs/loams/releases/download/v0.1.1
curl -fLO $base/loams-compose-0.1.1.tar.gz -fLO $base/SHA256SUMS -fLO $base/SHA256SUMS.asc

# the release signing key; check that its fingerprint is
# 333A C945 99D5 A1EB 2A41 6D1E 83DC 134D 8CAF 895D
curl -fsSL https://raw.githubusercontent.com/ostrium-labs/loams/main/docs/release/loams-release-key.asc | gpg --import
gpg --verify SHA256SUMS.asc SHA256SUMS            # "Good signature from Loams Release Signing Key"
sha256sum --ignore-missing -c SHA256SUMS          # macOS: shasum -a 256 --ignore-missing -c SHA256SUMS

tar -xzf loams-compose-0.1.1.tar.gz
cd loams-compose-0.1.1/release
cp .env.example .env

The bundle is the release's deploy/ directory, with images pinned to LOAMS_VERSION=0.1.1, so use it rather than a plain git clone of the default branch. A git clone --branch v0.1.1 checkout also matches the images and has the same first-run fixes (the v0.1.0 tag predates them: there, LOAMS_SETUP_TOKEN works only from secrets/engine.env, step 7).

Edit .env:

TS_AUTHKEY=tskey-auth-…          # from step 4
TS_HOSTNAME=loams                # the node name; keep it unless you run two stacks
TS_LOGIN_SERVER=                 # empty: Tailscale
LOAMS_AUTH_MODE=tailscale        # the default
LOAMS_TAILSCALE_ALLOWED_USERS=you@example.com,alice@example.com
LOAMS_SETUP_TOKEN=               # the output of: openssl rand -hex 32 (step 7)

LOAMS_TAILSCALE_ALLOWED_USERS is a comma-separated list of Tailscale logins, exactly as the admin console's Users page shows them (for example you@example.com, or you@github for a GitHub sign-in), with no quotes. Matching ignores case and spaces around commas. Empty admits every user your access policy lets reach the node, and the engine logs a warning. If your .env.example has no such line (a v0.1.0 git checkout), add it; the compose file passes it to the engine either way.

Setting LOAMS_SETUP_TOKEN now saves a restart in step 7.

Then start it. docker compose up -d starts the light default: the engine on its embedded stores with a local-folder bucket, Loams House linked to the engine, Loams Git, Loams Postgres on local folders, agentd and the tailscale sidecar. Add COMPOSE_PROFILES=tikv,rustfs to .env for Loams SQL (PD, TiKV, TiDB) and S3 storage (RustFS), and choose it before the first start: the engine records the store its data started on and refuses the other.

docker compose up -d
docker compose ps                                   # the services, and their state
docker compose exec tailscale tailscale status      # the stack is on your tailnet

Every long-running service is listed as running. The one-shot setup jobs, migrate, house-init and pg-bootstrap (and create-bucket with rustfs), exit 0 by design once their work is done, so plain docker compose ps does not list them (docker compose ps -a shows them as exited). pg-bootstrap creates the Postgres stack's tenant and timeline, and compute1 starts after it. To attach compute1 to ids of your own, set both TENANT_ID and TIMELINE_ID in .env. They win over the generated ones, and setting only one of them is refused.

A git clone --branch v0.1.0 checkout predates pg-bootstrap: there, set TENANT_ID and TIMELINE_ID by hand as in deploy/loams-postgres-dev/README.md.

If TS_AUTHKEY is empty, the sidecar logs a login URL instead (docker compose logs tailscale). The engine is now at https://loams.<tailnet>.ts.net. To stop, docker compose down; your data stays in the compose volumes, and docker compose down -v deletes it.

6. Connect the console

On a device that is on the same tailnet, open Loams Desktop and enter https://loams.<tailnet>.ts.net as your engine. In tailscale mode you do not sign in with a password on every visit: Tailscale tells the engine who you are. A new engine has no organization yet, though, so the first time you also run first-run setup, once.

Your browser may ask for permission the first time a page on a public origin calls an address on your tailnet (Local Network Access). Chrome, Edge and Firefox all do this. Allow it for the console's origin. If the console still cannot reach the engine, the prompt may have been answered (or auto-allowed) while the request stayed blocked; Firefox then logs "CORS request did not succeed" in the console. Open the site settings for the console's address (the icon left of the address), set Local network access to Allow, and reload the page.

Browser requests are limited by CORS. The 0.1.1 bundle's default LOAMS_CORS_ORIGINS is https://console.loams.dev, which is retired: remove it from .env and list the origin of any console you serve yourself (for example the console profile). From 0.1.2 the default allows no origin, and the engine's own /ui/ is same-origin.

7. First-run setup

A new engine has no organization. First-run setup creates it, with you as its owner, and runs once. With LOAMS_AUTH_MODE=tailscale (or oidc or apikey) authentication is on, and setup needs a setup token: without LOAMS_SETUP_TOKEN the console does not offer setup, and a setup call fails with "first-run setup on an engine with authentication on needs LOAMS_SETUP_TOKEN; set it and restart".

  1. Generate a token: openssl rand -hex 32.

  2. Give it to the engine. Put it in .env as LOAMS_SETUP_TOKEN=<the token>, or in secrets/engine.env (one KEY=value per line; create the file with mode 0600 if it does not exist). When both set it, secrets/engine.env wins. In a v0.1.0 git checkout only secrets/engine.env works, because that compose file does not pass the variable from .env.

  3. Recreate the engine if it was already running, so it reads the token:

    docker compose up -d loams
  4. Create the organization in the console. Open the console (Loams Desktop) with your engine URL (step 6). While the engine has no organization and a token is set, the console opens on Set up this install (#/setup). Enter the setup token, an organization name, your name, your email and a password of 12 characters or more; the console signs you in as the owner. Use your Tailscale login as the email, so that when Tailscale identifies you the engine maps you to this owner; a different email leaves your tailnet identity without a role.

    Or from the command line. The loams CLI has no setup command; make the same call from inside the engine's container, over its loopback port, reading the token from the engine's environment so it never appears on your command line:

    # Is setup still to do? Prints "setupRequired":true until it has run.
    docker compose exec loams curl -sS http://127.0.0.1:8080/loams.instance.v1.InstanceService/GetInstance \
      -H 'content-type: application/json' -d '{}'
    
    docker compose exec -e ORG='Acme' -e NAME='Your Name' -e EMAIL='you@example.com' \
      -e PASSWORD='at-least-12-characters' loams sh -c '
      curl -sS http://127.0.0.1:8080/loams.auth.v1.AuthService/CompleteSetup \
        -H "content-type: application/json" \
        -d "{\"setupToken\":\"$LOAMS_SETUP_TOKEN\",\"orgName\":\"$ORG\",\"name\":\"$NAME\",\"email\":\"$EMAIL\",\"password\":\"$PASSWORD\"}"'

    A wrong token is refused with permission_denied.

  5. Remove the token afterwards (optional). Once setup has run, every further attempt is refused with "setup has already run", whatever the token. Empty LOAMS_SETUP_TOKEN where you set it and run docker compose up -d loams again.

For a VM installed with byoc/install.sh, the installer has already generated LOAMS_SETUP_TOKEN in ~/loams/release/.env on the VM (mode 0600, not printed); read it there for the console's setup page.

8. Allowed users and machines

People. tailscale serve adds the caller's identity in Tailscale-User-Login and related headers. The engine trusts them only on its dedicated Tailscale listener, which is the sidecar's serve target; a forged header on any other listener gets a 401. LOAMS_TAILSCALE_ALLOWED_USERS is a comma-separated list of Tailscale logins to admit. Empty admits every user your access policy lets reach the node, and the engine logs a warning.

Machines. Tailscale's identity headers are empty for requests from tagged devices (Tailscale Serve). A CI runner or a server cannot use tailscale mode. Give it an API key instead. With the loams CLI from the release, in the compose directory:

loams auth new-key --label ci --append secrets/api-keys     # prints the key once
# in .env:
LOAMS_AUTH_MODE=tailscale,apikey
docker compose up -d loams

The CLI and SDKs then use the same tailnet URL with Authorization: Bearer lk_…. A 0.1.x key has the form lk_<26-character ULID>_<32 base62 characters>; the file holds only its hash.

BYOC: your own VM over SSH

The stack is the same on a VM. The VM needs SSH and outbound internet only; no inbound port is opened, and SSH itself can move onto the tailnet once the stack is up. From the unpacked bundle on your own machine (or deploy/release/byoc/install.sh in a v0.1.1 checkout):

cd loams-compose-0.1.1
release/byoc/install.sh -i ~/.ssh/id_ed25519 you@your-vm

install.sh installs Docker if it is missing, copies the compose bundle (with deploy/loams-postgres-dev) to ~/loams, writes .env with mode 0600, asking for your Tailscale auth key (or take it from --authkey or $TS_AUTHKEY), and starts the stack.

OptionEffect
(default)The light default stack: the engine on its embedded stores, the House, Git, Loams Postgres, agentd and tailscale
--tikvAdds the tikv profile: PD, TiKV and TiDB (Loams SQL)
--rustfsAdds the rustfs profile: Loams Postgres's storage in an S3 bucket
--allBoth, --tikv --rustfs
--profiles "A,B"Adds other profiles, for example house-tailnet
--no-postgresLeaves the Loams Postgres stack out
--version VThe image tag (default 0.1.1)
--reconfigureRewrites an existing .env on the host
-p, --dir, -ySSH port, install directory, no prompts

byoc/uninstall.sh removes it, and byoc/upgrade.sh you@your-vm --to <version> runs loams upgrade on the VM. Your device reaches the engine at https://loams.<tailnet>.ts.net, exactly as with a local stack.

The House on the tailnet

The House runs by default, linked to the engine. Its own tailnet node is the house-tailnet profile:

# .env: COMPOSE_PROFILES=house-tailnet (with any others, comma-separated)
docker compose up -d
docker compose exec house cat /var/lib/loams-house/house.key     # user default's password
curl -u "default:$PASSWORD" https://loams-house.<tailnet>.ts.net/ --data-binary 'SELECT version()'

Loams House is Experimental. Any ClickHouse HTTP client works the same way (port 443, TLS, user default). Set HOUSE_PRINCIPALS to say who may use it, as in the deploy README.

What is not on the tailnet

Only the engine's HTTP API (and, with the profile, the House) is served over the tailnet. The Qdrant, Elasticsearch, Flight SQL, Postgres-wire and MySQL-wire listeners, Live's own port and Resonate stay on loopback inside the stack. TiDB starts with a passwordless root and the Postgres stack with default credentials; neither is forwarded. Do not publish these in 0.1.1.

Headscale: coming soon

Planned The compose file has a headscale profile and TS_LOGIN_SERVER, but Headscale is unsupported in 0.1.1: it issues no *.ts.net HTTPS certificates, so a console served over HTTPS (HTTPS) cannot call an engine on it; the browser blocks it as mixed content. Use Tailscale.

Upgrading

0.1.1 changes the default stack; reinstall. Start a 0.1.1 stack from its bundle with a fresh .env. An existing 0.1.0 stack can keep its data instead: add COMPOSE_PROFILES=tikv,rustfs to its .env first, then run loams upgrade --to 0.1.1, which keeps 0.1.0's stores.

For now loams upgrade refuses a stack on the default embedded stores. Upgrade one of those by hand: back up its volumes, docker compose stop, set LOAMS_VERSION in .env, then docker compose up -d.

Troubleshooting

loams.<tailnet>.ts.net does not resolve. MagicDNS is off, or your device is not on the tailnet. Enable MagicDNS (step 2), check that the Tailscale client is connected, and that tailscale status on your device lists loams. Off the tailnet the name resolves to nothing, by design.

The console says it cannot reach the engine, or the browser shows a certificate error. HTTPS certificates are not enabled for the tailnet (step 2), or the engine URL starts with http://. Enable HTTPS and use https://loams.<tailnet>.ts.net. docker compose logs tailscale shows certificate errors.

The request times out from one device but works from another. Your access policy does not let that device reach tag:loams:443. Check the rule in step 3; the admin console's access-controls preview shows what a user can reach.

401 or 403 from the engine. You are reaching it, but it does not admit you. A 403 saying "this tailnet user is not allowed on this engine" means your login is not in LOAMS_TAILSCALE_ALLOWED_USERS (step 5). A 401 means the request carried no identity: requests from tagged devices carry none, so use an API key there.

Setup fails with "needs LOAMS_SETUP_TOKEN", or the console never offers setup. The engine has no setup token. Set it in .env or secrets/engine.env (only secrets/engine.env in a v0.1.0 git checkout) and recreate the engine (step 7). "setup_token is not this engine's LOAMS_SETUP_TOKEN" means the token you entered differs from the file's.

The sidecar never joins, or joins under another name. Check docker compose logs tailscale. An expired or already used single-use key fails; create a new one. A node named loams that already exists makes Tailscale pick loams-1; remove the old node in the admin console, then recreate the sidecar with an empty state (docker compose down, remove the tailscale-state volume, docker compose up -d).

Podman: recreating the engine after an .env change fails with "has dependent containers". The sidecar shares the engine's network namespace. Run docker compose rm -sf tailscale && docker compose up -d loams tailscale.

More in Running Loams 0.1.1 and the deploy README.

On this page