MakerFLOSS/slides/tappaas/how-to/new-module/index.md
Lars Rossen aab16d8189
All checks were successful
Build docs site / build (push) Successful in 48s
Build slides / build (push) Successful in 1m8s
slides(tappaas): the session builds the merged podman module
Podman and Portainer are one module now — engine plus GUI — so the deck follows:
podman.json, vmid 812, both sockets on the update.sh slide, and the console at
https://lab1.makerfloss.eu because --proxyDomain makes the module the
environment's gateway rather than podman.lab1.makerfloss.eu.

All commands verified against a live install on lab1.
2026-08-25 09:35:35 +02:00

11 KiB

marp theme class paginate title description
true gaia invert true How to implement a TAPPaaS module Live build of the podman module — OrangeMaker, 24 August

Getting a module onto TAPPaaS

Live build of a container host — Podman, with a console

OrangeMaker · 24 August


The plan

  1. What TAPPaaS is — a few diagrams, ten minutes, no deeper
  2. What a module actually is — a json contract and three scripts
  3. Build one live — the lab1 environment, then podman into it
  4. Prove it works — tests, then the web console
  5. 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 host with a Portainer console, TAPPaaS login",
  "vmname": "podman", "vmid": 812,
  "dependsOn": ["cluster:vm", "templates:debian", "backup:vm",
                "network:proxy", "identity:identity"],
  "identity": { "oidcRedirectPaths": ["/"],
                "secretsEnv": "/etc/secrets/podman.env" },
  "config": {
    "cluster:vm": { "cores": 2, "memory": "2048", "diskSize": "32G",
                    "image": "debian-13-generic-amd64.qcow2" },
    "network:proxy": { "proxyPort": 9443, "proxyUpstreamTls": true }
  }
}

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 built the VM, templates:debian apt-upgraded it, network:proxy published the name — and identity:identity already created the OIDC application and dropped its client credentials on the VM.

Nothing here is install-only, so 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:

  1. finds the node hosting the VM via pvesh, resolves its IP through the guest agent
  2. apt install podman, then enables both sockets: the rootful one is the Docker-compatible API Portainer speaks; the rootless one is for people who ssh in
  3. pulls portainer-ce:lts and runs it against the socket + a named volume
  4. creates a break-glass admin, password into /etc/secrets/podman-admin
  5. reads /etc/secrets/podman.env, fetches the OIDC discovery document, and PUTs the three endpoints into /api/settings → login becomes OAuth

Re-runnable throughout: the container is recreated only when the image digest actually changed, and an existing admin password is never regenerated.


test.sh — the one that earns trust

Check How
VM reachable IP resolves via the guest agent
container API rootful podman.socket is active
app running podman container inspect says running
API alive /api/status returns a version
identity intact /api/settings/public reports method 3 = OAuth

That last one is the check worth stealing. It catches the failure nobody notices: identity quietly falling back to local accounts, so the app still works while no longer being the login you thought it was.

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:vm built it from the Debian 13 cloud image
  • OS prep — templates:debian did apt + guest agent
  • A VLAN, an interface, DHCP, firewall rules — the zone came with the environment
  • https://lab1.makerfloss.eu, valid cert — network:proxy
  • An OIDC application, a group binding and client credentials — identity:identity
  • 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 add lab1 --from-zone makerfloss   # --check first for a dry run
network-manager show lab1              # vlan 299, 10.2.99.0/24, access-to
network-manager reconcile              # dry-run: drift on all 4 planes

environment-manager add lab1 --display "MakerFLOSS lab" --owner makerfloss \
    --zone lab1 --domain lab1.makerfloss.eu
environment-manager show lab1

add reconciles every plane itself. A tenant with its own VLAN, firewall posture and DNS names — lab1.makerfloss.eu outside, lab1.internal inside.


Demo 2 — install the module

site-manager repository add forgejo.makerfloss.eu/TAPPaaS/makerfloss --branch main

module-manager module add podman --environment lab1 \
    --proxyDomain lab1.makerfloss.eu        # publish at the environment's own name
module-manager module show podman-lab1      # note the -lab1 suffix

Outside the default environment the module is podman-lab1 — add takes the base name, everything after it takes the effective one. --proxyDomain makes it the environment's gateway instead of podman.lab1.makerfloss.eu.

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-lab1

Then open https://lab1.makerfloss.eu and press Sign in.

  • No login form. You are already you — Authentik, via OIDC.
  • Not in devops? No console. The group binding is the access gate.
  • Other machines in lab1 join as Environments: one agent container on :9001.

Create a container. Start it. Read its logs. From a browser, as yourself.


Where this module lives

makerfloss/src/containers/podman/

One sibling next to it: komodo/ — the GPL alternative, scripts still to write. Podman is the engine; Portainer is the GUI running as a container on it.

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
Private repo — forgejo.makerfloss.eu/TAPPaaS/makerfloss ours, today's podman

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.