install.sh, update.sh and test.sh get one slide each, with the real content of each — including why a 401 from Cockpit is a passing test and why test.sh uses curl -s rather than --fail. Corrects the identity story: Cockpit is not an OIDC client and cannot be configured into one, so the contract shows identity:accessControl and the demo slide is honest that Authentik gates the URL while Cockpit still asks for a local account. Full analysis in the module's DESIGN.md.
10 KiB
| marp | theme | class | paginate | title | description |
|---|---|---|---|---|---|
| true | gaia | invert | true | How to implement a TAPPaaS module | Live build of a Podman module — OrangeMaker, 24 August |
Getting a module onto TAPPaaS
Live build of a Podman container host
OrangeMaker · 24 August
The plan
- What TAPPaaS is — a few diagrams, ten minutes, no deeper
- What a module actually is — a json contract and three scripts
- Build one live — the
lab1environment, thenpodmaninto it - Prove it works — tests, then the web console
- You get a login — your own identity on this system
You leave with: a mental model, a module you watched get built, and an account.
The shape of a TAPPaaS site
flowchart LR
sat["satellite VPS<br/>public IP · ingress<br/>off-site backup"]
subgraph z0["mgmt · zone mgmt"]
cicd["tappaas-cicd"]
net["network"]
clu["cluster"]
bak["backup"]
idp["identity"]
end
subgraph z1["default · zone srv"]
nc["nextcloud"]
ha["home-assistant"]
end
subgraph z2["lab1 · zone lab1"]
pod["podman — today"]
end
sat --> net
cicd --> nc
cicd --> pod
The words we will use
| Word | Meaning |
|---|---|
| Module | The smallest deployable unit — its own VM, its own json contract. Foundation and apps are modules. |
| Environment | A tenant. mgmt and one named after the site always exist; add more freely. Today we add lab1. |
| Zone | A VLAN with a firewall policy. Modules land in their environment's zone. |
| Mothership | tappaas-cicd — where the managers run and where you type. |
| Satellite | Optional VPS, for sites with no usable public IP. |
Everything is a module. There is one model to learn, not eight.
Developing modules: dev instance, private branch
flowchart RL
subgraph Development["Development repository"]
dev_main["main"]
end
subgraph Upstream["Upstream repository"]
up_main["main"]
up_stable["stable"]
up_main -->|merge| up_stable
end
subgraph InstanceDev["TAPPaaS instance · dev"]
localDev["clone of Upstream"]
localDev_dev["clone of Development"]
end
subgraph InstanceProd["TAPPaaS instance · prod"]
localProd["clone of Upstream"]
end
localProd -->|pull| up_stable
localDev -->|pull| up_main
localDev_dev -->|push| dev_main
dev_main -->|PR| up_main
Our setup tonight
flowchart RL
subgraph t["TAPPaaS · codeberg"]
stable["stable"]
main["main"]
end
subgraph c["Community · codeberg"]
cm["main"]
end
subgraph f["forgejo.makerfloss.eu"]
fm["main"]
end
subgraph inst["Our TAPPaaS · tappaas-cicd"]
lt["clone of TAPPaaS"]
lc["clone of Community"]
lf["clone of MakerFLOSS"]
end
lt -->|pull| main
lc -->|pull| cm
lf -->|pull| fm
classDef tracked stroke:#ff8a80,stroke-width:3px
class main tracked
We track main, not stable — this is a lab, we want the new things.
A module is a contract and three scripts
podman/
├── podman.json # the contract — what it is, needs, provides
├── install.sh # put the software in the VM (once)
├── update.sh # keep it patched (on schedule)
├── test.sh # prove it still works (gates updates)
├── README.md # what it is
└── INSTALL.md # what automation cannot do for you
That is the whole surface. Everything else is the platform's job.
The contract, for real
{
"description": "Podman container host — Debian 13 VM with rootless Podman + Cockpit",
"vmname": "podman", "vmid": 812,
"dependsOn": ["cluster:vm", "templates:debian", "backup:vm",
"network:proxy", "identity:accessControl"],
"config": {
"cluster:vm": { "cores": 2, "memory": "2048", "diskSize": "20G",
"image": "debian-13-generic-amd64.qcow2" },
"network:proxy": { "proxyPort": 9090, "proxyUpstreamTls": true,
"proxyUpstreamHttp1": true, "proxyAllowedZones": ["mgmt"] }
}
}
dependsOn is the whole trick: five services, declared — not scripted.
install.sh — called once, at add
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "${SCRIPT_DIR}/update.sh" "$@"
By the time it runs, cluster:vm has built the VM and templates:debian has
apt-upgraded it and installed the guest agent.
For podman there is nothing install-only — install is update. Five lines is a
perfectly good install.sh.
update.sh — where the work is
Runs on the mothership, not on the VM. It:
- finds the node hosting the VM via
pvesh— HA-safe, the VM may have moved - resolves the VM's IP through the Proxmox guest agent, then waits for SSH
apt-get install podman podman-compose slirp4netns uidmap cockpit cockpit-podman- enables the rootless
podman.socket,loginctl enable-linger,cockpit.socket - records
/etc/tappaas-podman.version - polls
https://localhost:9090until the console answers, then prints the URL
Every step is re-runnable: apt is a no-op when current, the marker is overwritten,
the sockets are enable --now. That is what "idempotent" has to mean in practice.
test.sh — the one that earns trust
| Check | How |
|---|---|
| VM reachable | IP resolves via the guest agent |
| podman installed | version marker + podman --version actually runs |
| plugin present | dpkg -s cockpit-podman |
| console alive | :9090 answers 200, 302, 401 or 403 |
A login page is a pass — pre-auth, 401 is the healthy answer.
Prints N passed, M failed and exits non-zero on any failure. Note curl -s, not
--fail: --fail exits 22 on 4xx and would corrupt the -w '%{http_code}' we read.
Run on demand, and again before every update is merged. If test.sh is honest,
unattended updates are safe — that is the entire deal.
What you did not have to write
- A VM —
cluster:vmbuilt it from the Debian 13 cloud image - OS prep —
templates:debiandid apt + guest agent - A VLAN, an interface, DHCP, firewall rules — the zone came with the environment
https://podman.lab1.makerfloss.euwith a real certificate —network:proxy- An access gate —
identity:accessControl: only the bound groups reach the URL - Nightly backup to PBS, scheduled updates, health reporting
A few lines of json bought all of it.
Demo 1 — an environment of our own
network-manager zone add lab1 --from-zone srv
network-manager show lab1 # vlan tag, subnet, access-to
network-manager reconcile # dry-run: drift on all 4 planes
network-manager reconcile --apply # converge OPNsense, Proxmox, switch, AP
environment-manager add lab1 --display "MakerFLOSS lab" \
--zone lab1 --domain lab1.makerfloss.eu
environment-manager list
environment-manager show lab1
A tenant with its own VLAN, firewall posture and DNS names —
lab1.makerfloss.eu outside, lab1.internal inside.
Demo 2 — install the module
module-manager module add podman --environment lab1
module-manager module list # what is deployed
module-manager module show podman # the resolved config, cascade applied
Watch the order: dependencies resolve first, then the VM, then the network, then
install.sh. Install order is computed from every module's dependsOn — nobody
maintains a list.
Demo 3 — prove it, then look at it
module-manager module test podman
Then open https://podman.lab1.makerfloss.eu — Cockpit, with the Podman page.
- Reachable from the
mgmtzone only. Not from the internet, by declaration. - Authentik gates the URL: no
devopsgroup, no console. Then Cockpit asks again — it is not an OIDC client, and a local Unix account is not optional for it. - Two logins, honestly.
DESIGN.mdin the repo says what it would take to fix.
Where this module lives
Community/src/larsrossen/containers/podman/
Three homes for a module, all first-class:
| Home | For |
|---|---|
| TAPPaaS repo, by pull request | modules the whole project should carry |
| Community repo | yours, shared, no gatekeeping — today's podman |
Private repo — e.g. forgejo.makerfloss.eu |
yours, not shared |
Adding a repository is one site-manager repository add.
Your turn — identity on this TAPPaaS
people-manager user add <you> --email <you>@example.org \
--roles user --groups devops
people-manager reconcile # preview
people-manager reconcile --apply # push to Authentik
authentik-manager user-recovery-link <you> # one-time URL: set your own password
One login per person, roles via groups. Nobody types a password into a chat window.
Take it further
- Develop a module — tappaas.org/generated/develop-a-module
- Every json field — tappaas.org/generated/schemas
- Zones and the access model — tappaas.org/generated/zones
- Repository topology — tappaas.org/generated/design/cicd-git
- This deck — slides.makerfloss.eu/tappaas/how-to/new-module
Pick an app you want on a sovereign box. Open an issue first — someone may already be packaging it.