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.
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:
- Install Bun if it's not already present.
- Install
@celilo/cli,@celilo/event-bus, and@celilo/e2eas global Bun packages. - Print the line to add to your shell rc (
~/.zshrcfor zsh,~/.bashrcfor bash) socelilostays on your$PATHin new shells. - 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 (defaultroot@pam!celilo) and the token secret you just created - Default target node (default
pve) and storage (defaultlocal-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.
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.
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)