Installed portainer into lab1 on the live system; several slide commands were wrong. The verb is 'network-manager add', not 'network-manager zone add', and there is no 'srv' zone here — the live service zone is 'makerfloss'. Status verbs need the effective name 'portainer-lab1', while 'module add' takes the base name. The published host is portainer-lab1.lab1.makerfloss.eu, not portainer.lab1.makerfloss.eu. Adds the repository registration to Demo 2, which the deck had never shown.
11 KiB
| marp | theme | class | paginate | title | description |
|---|---|---|---|---|---|
| true | gaia | invert | true | How to implement a TAPPaaS module | Live build of a Portainer module — OrangeMaker, 24 August |
Getting a module onto TAPPaaS
Live build of a Portainer container console
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, thenportainerinto 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["portainer — 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
portainer/
├── portainer.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": "Portainer CE — container console with TAPPaaS login",
"vmname": "portainer", "vmid": 813,
"dependsOn": ["cluster:vm", "templates:debian", "backup:vm",
"network:proxy", "identity:identity"],
"identity": { "oidcRedirectPaths": ["/"],
"secretsEnv": "/etc/secrets/portainer.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:
- finds the node hosting the VM via
pvesh, resolves its IP through the guest agent apt install podman, then enables the rootfulpodman.socket— that socket is the Docker-compatible API Portainer speaks- pulls
portainer-ce:ltsand runs it against the socket + a named volume - creates a break-glass admin, password into
/etc/secrets/portainer-admin - reads
/etc/secrets/portainer.env, fetches the OIDC discovery document, andPUTs 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: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://portainer-lab1.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 portainer --environment lab1
module-manager module list # what is deployed
module-manager module show portainer-lab1 # note the -lab1 suffix
Outside the default environment the module is portainer-lab1 — add takes
the base name, everything after it takes the effective one.
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 portainer-lab1
Then open https://portainer-lab1.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
lab1join 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/portainer/
Two siblings next to it: podman/ (one host, local login) and komodo/ (GPL,
scripts still to write). Same job, three answers.
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 portainer |
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.