Skip to content

Set up apps

An app template renders one instance of an application as its own repository. The repository lives inside the core's repos/ directory, but it has its own git history and remote.

Before you render

Requirement Detail
Core is live ./lk check status reports live
Forgejo admin token FJ_TOKEN, used once by collect
Infisical access INFISICAL_TOKEN, or INFISICAL_EMAIL and INFISICAL_PASSWORD
Public exposure only A domain, Bunny account and the host's public origin IP
App-specific See the answer table below

The app host is usually the core host. A dedicated app host is supported by changing server_tailnet_ip; a shared host needs LK_SHARED_HOST=1 when decommissioning.

1. Render inside the core

cd lairkit-core-<slug>
cookiecutter /path/to/cookiecutter-lairkit-iac-<app> -o repos \
  instance_slug=amber-otter \
  exposure=tailnet-only \
  server_tailnet_ip=100.64.0.10

The render lands in repos/lairkit-iac-<app>-<slug>/. If you omit instance_slug, the template generates a two-word identity such as antidote-duplicity.

The template walks up from repos/ to find the core checkout, so the core coordinates, backup target, notification channel and owner email come from the core's config/instance.yml instead of being asked again.

For a render outside a core checkout, pass _entrypoint=/path/to/lairkit-core-<slug>.

One directory per instance

Do not render two instances into the same directory, and do not run app commands from the core root. Each render is a separate repository.

Answers that change the shape

These prompts are common to every app template:

Prompt Values Effect
instance_slug two words Identity for containers, Services, paths and tags
exposure tailnet-only, public Tailnet Services only, or a Bunny pull zone in front of the host
server_tailnet_ip 100.x address The host that runs the app
origin_public_ip public IP Required for public; the host's origin address
base_domain domain Required for public; the Bunny DNS zone
backup_* bucket, endpoint, region Defaults come from the core
owner_email, support_email addresses Recorded in the instance facts and reports
edge_waf_mode, edge_waf_plan enforce/off, basic/advanced WAF on a public pull zone

Each app adds its own:

App Prompts
CryptPad cryptpad_version, onlyoffice, allow_anonymous_docs, cryptpad_hostname, cryptpad_sandbox_hostname
ERPNext erpnext_version, mariadb_version, redis_version, gunicorn_workers, gunicorn_threads, admin_email, erp_hostname
Jitsi Meet jitsi_release, auth_mode, enable_guests, matrix_admin_username, lldap_base_dn, excalidraw_version, lldap_version, continuwuity_version, sable_version, jitsi_hostname, chat_hostname, matrix_hostname
Zipline zipline_version, postgres_version, pgbackrest_version, storage, s3_*, admin_username, zipline_hostname

public needs every hostname the app serves. CryptPad needs a main and a sandbox hostname; Jitsi needs main, chat and matrix hostnames. The template refuses a hostname that already resolves, so a new render cannot take over someone else's DNS name. Re-rendering a live instance needs LAIRKIT_SKIP_DNS_CHECK=1.

2. Collect the repository and secrets

cd repos/lairkit-iac-<app>-<slug>
./lk collect

collect is attended and runs on your laptop. It:

  • creates the repository in the core's Forgejo;
  • creates the Infisical project and the read-only CI identity;
  • writes the Actions secrets and every app secret;
  • records the core coordinates in .lairkit/repo.json;
  • prints N/N names resolve when every name is readable.

It writes .infisical.json and .lairkit/repo.json in the render. It never writes into the core checkout. A second run is a no-op and exits 10.

Before the first mutation, escrow the credentials the app cannot recreate — the store credentials, the S3 keys and the app's admin passwords — then record the acknowledgement:

./lk collect --escrow-ack
git add -A && git commit -m "record escrow acknowledgement"

./lk check preflight stays red until this value is committed.

3. Push to Forgejo

git remote add origin https://<core-slug>-forgejo.<tailnet_dns>/<org>/<project>.git
git push -u origin main

The first push runs check preflight only. It does not install anything.

4. Dispatch the install

Mutations run on the core's Forgejo runner, not on your laptop. To dispatch one:

  1. Open the app repository in Forgejo.
  2. Open Actions.
  3. Select install.
  4. Choose Run workflow.
  5. Set action and run.

Use the same path for the other workflows: change, recover, transfer, retire, and check.

Step Action Result
1 setup Boots accessories and the app, then deploys the proxy
2 admin Completes the first-run admin step
3 ops Uploads backup bodies, installs cron, advertises Services, takes the first backup and drill, opens the Operations issue

ERPNext also has a site action for creating an additional site.

A day-0 install is green when:

./lk check status    # live
./lk check verify    # all probes green
./lk check backup    # newest set inside the RPO

What each install creates

App Runtime Data Backup
CryptPad CryptPad + OnlyOffice, Dozzle, Kamal proxy Volume tree (blob, block, customize, data, datastore, OnlyOffice) One tar of the volume tree + JSON manifest
ERPNext ERPNext roles + Caddy, MariaDB, two Redis, Dozzle MariaDB + shared sites/ mariadb-dump + sites archive
Jitsi Meet Jitsi web/prosody/jicofo/jvb, Excalidraw, Continuwuity, Sable, lldap, Dozzle, Caddy router Seven state directories One tar + JSON manifest
Zipline Zipline, PostgreSQL + pgBackRest, Dozzle PostgreSQL; uploads on disk or S3 pgBackRest + uploads archive (local storage)

Public exposure

exposure=public adds a Bunny pull zone and a WAF in front of the app. DNS for the zone is delegated to Bunny. Bunny terminates TLS and forwards HTTP to the origin; the template does not manage the origin firewall, so restrict the origin IP yourself.

Two exceptions matter:

  • Jitsi media uses UDP 10000 directly to origin_public_ip. Bunny does not carry the media path.
  • Dozzle, lldap and other admin surfaces stay on the tailnet in both exposures.

After install