This walkthrough takes you from a fresh laptop to a working home-lab stack with Proxmox containers, automatic DNS, HTTPS, and single sign-on — and shows you how to plug your own web app into it. Plan for about an hour the first time through.

Before you start, you'll need: a Proxmox host on your LAN with an API token; a Namecheap-registered domain with Dynamic DNS enabled; a small Linux box (Raspberry Pi, mini-PC, or VM) to act as your edge router; and SSH access to all of them.

1. Install Celilo

One command installs Bun (the runtime Celilo runs on), the Celilo CLI, and the e2e test harness:

curl -fsSL https://celilo.computer/install.sh | bash

The script is idempotent and walks you through the interactive bits at the end. It will:

  1. Install Bun if it's not already present.
  2. Install @celilo/cli, @celilo/event-bus, and @celilo/e2e as global Bun packages.
  3. Print the line to add to your shell rc (~/.zshrc for zsh, ~/.bashrc for bash) so celilo stays on your $PATH in new shells.
  4. Run celilo system init — interactive; asks for your network subnets (DMZ, app, secure, internal), DNS resolvers, and an SSH public key Celilo can use to reach machines.

The script does not install the event-bus daemon as a system service. Once you've decided you like Celilo and want deploy progress streamed somewhere persistent, you can add it with celilo events install-daemon — but first-time tire-kickers don't need a system service running.

Read the script before piping it to your shell if you'd like — view the source.

If you want the packages but not the interactive system init (e.g. for CI), pass --skip-init:

curl -fsSL https://celilo.computer/install.sh | bash -s -- --skip-init

Then verify:

celilo --version

You can re-run system init any time. To take all defaults, or override specific values inline:

celilo system init --accept-defaults
celilo system init network.dmz.subnet=10.0.10.0/24

2. Add a Proxmox service

A service is an infrastructure provider Celilo can provision new compute on — Proxmox creates LXC containers; a Digital Ocean account would create droplets; another adapter could provision VMs on bare-metal libvirt or anything else with an API. Modules declare what they need (CPU, memory, disk, zone) and the service provides something that meets those requirements. Proxmox is the path this walkthrough takes; the abstraction is open for extension.

Before running the next command, create an API token in the Proxmox UI (Datacenter → Permissions → API Tokens) and copy the secret somewhere safe — Proxmox only shows it once.

celilo service add proxmox

You'll be prompted for:

  • A human-readable name (e.g. Proxmox Home Lab)
  • Which zones this service can host containers in (multi-select: dmz, app, secure, internal)
  • Proxmox IP, API port (default 8006), token ID (default root@pam!celilo) and the token secret you just created
  • Default target node (default pve) and storage (default local-lvm)
  • An Ubuntu LTS version for the container template (24.04 / 22.04 / 20.04)

Celilo tests the API connection at the end and offers to download the chosen Ubuntu template into Proxmox so it's ready when modules need it.

3. Set up the Namecheap registrar

The namecheap module provides the dns_registrar capability — every other module that needs a public DNS record (Caddy, Authentik, your web app) will reach for it automatically.

First, in the Namecheap UI, enable Dynamic DNS for each domain you want Celilo to manage and copy the DDNS password it generates (Domain List → Advanced DNS → Dynamic DNS).

celilo module import namecheap
celilo module generate namecheap

module generate prompts for any required config or secrets it doesn't already have:

  • domains — the list of domains you'll manage. The first one is treated as primary.
  • ddns_passwords — a JSON object mapping each domain to its DDNS password.

You can also set values non-interactively:

celilo module config set namecheap domains '["example.com"]'
celilo module config set namecheap ddns_passwords '{"example.com":"<your-ddns-password>"}'

Then deploy:

celilo module deploy namecheap

4. Add an edge router and install iptables

The iptables module turns a small Linux box into your home-lab router and provides the firewall capability — port forwarding, NAT, and DNAT for every service you expose. Unlike Caddy or Authentik (which Celilo provisions as new Proxmox containers), iptables runs on an existing machine, because that machine is the network boundary.

Register the router as a machine. Replace the IP with whatever your router box uses:

celilo machine add 192.168.1.1 --ssh-user root --earmark iptables

Celilo SSHes in, auto-detects hostname, OS, CPU, memory, network interfaces, and figures out which zone each interface belongs to based on your subnet config. The --earmark iptables flag reserves this machine for the iptables module so no other module tries to land on it.

Then install and deploy:

celilo module import iptables
celilo module generate iptables
celilo module deploy iptables

You'll be asked which zones this firewall services (typically all of dmz, app, secure, internal). The NAT IP and per-zone IPs are derived automatically from the machine you just added.

Why a real machine? Caddy and Authentik are workloads — Celilo builds them fresh each time. The firewall is infrastructure — it has to exist before anything can route through it, so you bring your own.

5. Deploy Caddy and Authentik

Now the foundations are in place: Proxmox can spawn containers, Namecheap can publish DNS, and iptables can forward traffic. Caddy and Authentik just need a few app-specific values and they'll land on their own.

Caddy — HTTPS reverse proxy

celilo module import caddy
celilo module generate caddy
celilo module deploy caddy

You'll be asked for:

  • hostnames — FQDNs Caddy should serve (e.g. ["www.example.com"]). The first is canonical.
  • acme_email — contact email Let's Encrypt uses for cert notifications.

That's it. Celilo provisions an LXC container on Proxmox in the dmz zone, calls firewall.exposeService on the iptables module to open ports 80/443 and DNAT them inbound, calls dns_registrar.registerHost on namecheap to point each hostname at your public IP, and lets Caddy fetch real Let's Encrypt certs over the resulting public path.

Authentik — single sign-on

celilo module import authentik
celilo module generate authentik
celilo module deploy authentik

The only thing Authentik insists on is admin_email. Database password, bootstrap token, and signing key are auto-generated and stored encrypted; the hostname (auth.<your-primary-domain>), ACME cert, and DNS record come from Caddy and Namecheap automatically.

When the deploy finishes, log into https://auth.your-domain.com with the admin email and the password Celilo printed during generation.

6. Write a web app that uses these capabilities

There's no scaffolder yet — the simplest path is to copy an existing module directory and edit it. modules/celilo-website is a good reference: it's a static Astro site that uses public_web for hosting and dns_registrar for its hostname.

The minimum manifest for an app that wants HTTPS, DNS, and SSO looks like this:

# my-app/manifest.yml
celilo_contract: "1.0"
id: my-app
name: My App
version: 1.0.0
description: "A web app behind Caddy and Authentik"

requires:
  capabilities:
    - name: public_web         # Caddy gives me HTTPS + a route
      version: 3.0.0
    - name: dns_registrar      # Namecheap gives me a public hostname
      version: 4.0.0
    - name: idp                # Authentik gives me OIDC sign-in
      version: 1.0.0

variables:
  owns:
    - name: subdomain
      type: string
      required: true
      description: "Subdomain to serve on (e.g. 'app' for app.example.com)"
      source: user
  imports: []

secrets:
  declares: []

hooks:
  on_install:
    script: ./scripts/on-install.ts
    timeout: 120000
  health_check:
    script: ./scripts/health-check.ts
    timeout: 60000

The on_install hook is where you wire the capabilities together. Each capability arrives as a typed function on ctx.capabilities:

// my-app/scripts/on-install.ts
import type { HookContext } from '@celilo/capabilities';

export default async function onInstall(ctx: HookContext) {
  const fqdn = `${ctx.config.subdomain}.${ctx.capabilities.dns_registrar.primary_domain}`;

  // 1. Open a public DNS record pointing at our firewall's external IP
  const { ip } = await ctx.capabilities.firewall.exposeService({
    internal_host: ctx.machine.hostname,
    internal_port: 8080,
    public_port: 443,
  });
  await ctx.capabilities.dns_registrar.registerHost({ host: fqdn, ip });

  // 2. Register an OIDC client with Authentik
  const oidc = await ctx.capabilities.idp.createOidcClient({
    name: 'my-app',
    redirect_uris: [`https://${fqdn}/auth/callback`],
  });

  // 3. Tell Caddy to reverse-proxy https://<fqdn> to my container
  await ctx.capabilities.public_web.registerRoute({
    hostname: fqdn,
    upstream: `http://${ctx.machine.hostname}:8080`,
  });

  // oidc.client_id and oidc.client_secret are now available to your app
}

Build, import, and deploy your module:

celilo module pack ./my-app          # creates my-app-1.0.0+1.netapp
celilo module import ./my-app-1.0.0+1.netapp
celilo module generate my-app
celilo module deploy my-app

When the deploy finishes you'll have a public URL with HTTPS, a DNS record that survives IP changes, and an OIDC integration with Authentik — none of which you had to configure by hand.

What's next? Read the module catalogue for the full list of capabilities you can compose, or jump to Authoring modules below for hooks, secrets, and Terraform templates.

Core concepts

Modules

A module is a self-contained infrastructure unit — a DNS server, a reverse proxy, an identity provider. Each module declares its capabilities, requirements, and secrets. Celilo handles dependency resolution, provisioning, and wiring.

Capabilities

Capabilities are typed interfaces between modules. A module that provides dns_registrar can register DNS records; any module that requires it gets that function injected automatically. No hardcoded hostnames or IPs needed.

Machines and services

A machine is an existing host (Raspberry Pi, VPS, bare metal) that Celilo can SSH into. A service is a container host (Proxmox, Digital Ocean) that Celilo can provision new containers on. Modules run on whichever infrastructure is available in their zone.

Module commands

celilo module list              # List installed modules
celilo module import <name>   # Install from registry
celilo module import <path>    # Import local module
celilo module remove <name>    # Remove a module
celilo module config <name>    # Edit module configuration
celilo module health <name>    # Run health check
celilo module deploy <name>    # Re-run on_install hook

Service commands

celilo service list             # List container services (Proxmox, DO...)
celilo service add              # Add a container service
celilo service remove <name>

Machine commands

celilo machine list             # List known machines
celilo machine add              # Add a machine (VPS, Pi, bare metal)
celilo machine remove <host>

Authoring modules

A Celilo module is a directory with a manifest.yml and optional TypeScript hook scripts, Terraform templates, and Ansible playbooks.

my-module/
├── manifest.yml          # Module declaration
├── celilo/
│   └── types.d.ts        # Generated config types
├── scripts/
│   ├── package.json
│   ├── on-install.ts     # Hook scripts (TypeScript)
│   └── health-check.ts
├── terraform/            # Terraform templates (.tpl)
└── ansible/              # Ansible playbooks

Package your module for distribution:

celilo module build my-module   # Runs build.command from manifest
celilo module pack my-module    # Creates my-module-1.0.0+1.netapp

Built-in capabilities

  • public_web — HTTPS reverse proxy and static site hosting (Caddy)
  • dns_registrar — DNS A record registration (Namecheap, others)
  • dns_internal — Internal DNS resolution (Knot + Unbound)
  • firewall — Port forwarding and NAT (Greenwave, iptables)
  • idp — OIDC identity provider (Authentik)