Seed the MakerFLOSS devops repo with the podman module

README, a module-catalog.json validated against TAPPaaS's
module-catalog-fields.json, and the podman module copied verbatim from
Community/src/larsrossen/containers/podman into a flat src/containers/ layout
(the catalog carries the explicit moduleJson path, so layout is free).
This commit is contained in:
Lars Rossen 2026-08-22 18:46:24 +02:00
commit 1dba304085
9 changed files with 482 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.DS_Store

35
README.md Normal file
View file

@ -0,0 +1,35 @@
# makerfloss
The MakerFLOSS **DevOps repository** for experimental TAPPaaS modules.
Modules are developed and tried out here, on the TAPPaaS system at
[makerfloss.eu](https://makerfloss.eu), before they are proposed upstream to
[TAPPaaS](https://codeberg.org/TAPPaaS/TAPPaaS) or
[Community](https://codeberg.org/TAPPaaS/Community). Expect things to be
half-built, renamed, or removed.
## Layout
```text
src/module-catalog.json # the registry a TAPPaaS instance reads
src/containers/podman/ # one directory per module
```
## Using it from a TAPPaaS instance
Register the repository on the `tappaas-cicd` mothership, then install a module
from it:
```bash
site-manager repository add makerfloss \
--url https://forgejo.makerfloss.eu/TAPPaaS/makerfloss.git \
--branch main
module-manager module add podman --environment <env>
```
## Modules
| Module | What it is | Status |
| --- | --- | --- |
| [podman](src/containers/podman) | Debian 13 VM with rootless Podman + the Cockpit web console | incomplete |

View file

@ -0,0 +1,110 @@
# Podman container host — Installation
Primary audience: TAPPaaS admin. Steps the scripts cannot automate.
## Prerequisites
- `tappaas@tappaas-cicd` with the usual cluster SSH/sudo access.
- The dependencies are pulled in automatically (`cluster:vm`, `templates:debian`,
`backup:vm`, `network:proxy`).
- For the friendly HTTPS name to present a valid certificate, the TAPPaaS wildcard must be in
OPNsense Trust (run `acme-setup.sh` once — see the platform INSTALL). Until then the internal
endpoint still works, with a cert warning (Cockpit also ships a self-signed cert).
## Install
```bash
cd /home/tappaas/Community/src/larsrossen/containers/podman
install-module.sh podman
```
This automatically:
- creates a Debian 13 VM (vmid 812, 2 vCPU / 2 GB / 20 GB) and OS-preps it;
- installs `podman`, `podman-compose`, `cockpit` and `cockpit-podman` from apt;
- enables the rootless podman socket (with linger) and the Cockpit console on `:9090`;
- publishes the internal reverse-proxy name `podman.<domain>` (console reachable from the
`mgmt` zone only, no internet).
### Placement / zone
The module JSON sets **no `zone0`**, so the VM lands in the **default environment's zone**
(ADR-007 P5: with `zone0` unset, install resolves the zone from
`config/environments/<env>.json → network.zone`). To place it in a specific environment — and
therefore its associated zone — pass `--environment` at install time:
```bash
install-module.sh podman --environment <env> # VM joins <env>'s network.zone
```
An explicit `zone0` in the JSON would override this, so it is intentionally omitted.
## Post-install (manual — required for the web login)
The Cockpit console authenticates against **Linux (PAM) accounts on the VM**. The cloud-init
`tappaas` user has SSH-key auth and **no password**, so it cannot log in to the web console
until you give it one (or create a dedicated admin user).
**Option A — give the `tappaas` user a password:**
```bash
ssh tappaas@<vm-ip> 'sudo passwd tappaas'
```
**Option B — create a dedicated admin user (recommended for shared access):**
```bash
ssh tappaas@<vm-ip>
sudo useradd -m -s /bin/bash -G sudo podadmin && sudo passwd podadmin
sudo loginctl enable-linger podadmin # keep its rootless podman socket alive
```
Then open **`https://podman.<domain>`** (or `https://<vm-ip>:9090`), log in with that account,
and choose **Podman containers** in the left menu (Cockpit starts the user's podman service on
first use).
## Using it
- CLI: `ssh tappaas@<vm-ip>` then `podman run …`, `podman ps`, `podman images`.
- Compose files: `podman-compose up -d` in a directory with a `compose.yaml`.
- Web: manage/start/stop containers, pull images and inspect logs from the Cockpit
**Podman containers** page.
## Verification
```bash
test-module.sh podman
```
Manual checks:
| Check | Expected |
|-------|----------|
| `ssh tappaas@<vm-ip> podman --version` | prints the Podman version |
| `https://podman.<domain>` from a `mgmt` browser | Cockpit login page loads |
| Same URL from an out-of-policy zone or the internet | 403 (blocked) |
| Cockpit → **Podman containers** after login | the Podman page renders |
## Troubleshooting
**Web login rejected for `tappaas`**
The account has no password by default — set one (Option A above) or use a dedicated admin
user (Option B). Cockpit cannot log in a key-only account.
**Podman page in Cockpit says the service is not running**
Cockpit starts the *per-user* podman service on first open; if it does not, on the VM run
`systemctl --user start podman.socket` as that user, and ensure `loginctl enable-linger <user>`
was set so the socket survives logout.
**Friendly name unreachable**
Confirm you are in the `mgmt` zone (others get 403 by design). Check the Caddy route:
`network:proxy test-service.sh podman`. Re-apply with `install-module.sh podman --force`.
**Friendly name loads a blank page (valid cert, no UI)**
Cockpit rides a WebSocket; behind a TLS reverse proxy Caddy must talk HTTP/1.1 to the upstream.
This module sets `proxyUpstreamHttp1: true` (os-caddy `HttpVersion=http1`). If you see a blank
page, verify the handler has HTTP Version = HTTP/1.1 and re-apply:
`install-module.sh podman --force`. The direct console `https://<vm-ip>:9090` always works.
**Re-run / upgrade**
`install-module.sh podman --force` is idempotent: apt re-runs (a no-op when already current)
and the version marker `/etc/tappaas-podman.version` is refreshed.

View file

@ -0,0 +1,60 @@
# Podman — container host with a web console
Primary audience: TAPPaaS operator who wants a plain place to run OCI containers.
A **Debian 13 (trixie) VM with [Podman](https://podman.io) installed** — the daemonless,
rootless-capable container engine (a drop-in for `docker`). On top of the engine it adds the
**Cockpit web console** with the **`cockpit-podman`** plugin, so containers, images and pods
can be managed from a browser instead of only the CLI. Cockpit's console has a **login
screen**, so per the module brief it is published internally as `https://podman.<domain>` via
`network:proxy`.
## What you get
| Capability | Access from | How |
|------------|-------------|-----|
| Podman container engine (rootless + CLI) | on the VM | `ssh tappaas@<vm-ip>` then `podman …` |
| Cockpit web console (login + Podman page) | `mgmt` zone | `https://podman.<domain>` (internal) |
| Direct console | LAN reachable from `mgmt` | `https://<vm-ip>:9090` |
| `podman-compose` for compose files | on the VM | `podman-compose up -d` |
## What is not included
- **No pre-loaded containers.** This is a clean host — you bring the images/compose files.
- **No internet exposure.** The friendly name is reachable only from the `mgmt` zone; it is
not published to the internet (no inbound NAT/port-forward).
- **No Docker daemon / Kubernetes.** Podman is daemonless; there is no `dockerd` and no k8s
control plane here (Podman can generate/play Kubernetes YAML, but that is out of scope).
- **No default web login.** The console authenticates against Linux (PAM) accounts on the VM;
the cloud-init `tappaas` user has no password until you set one — see [INSTALL.md](./INSTALL.md).
## Access (internal only)
`network:proxy` publishes **`https://podman.<domain>`** (split-horizon DNS → Caddy → the VM's
`:9090`, HTTPS upstream), with an access-list permitting only the **`mgmt`** zone — **not
reachable from the internet**. Cockpit rides WebSockets, so the route is created with
`proxyUpstreamHttp1: true` (Caddy talks HTTP/1.1 to the upstream); the direct
`https://<vm-ip>:9090` always works too.
## Dependencies
| Depends on | Purpose |
|------------|---------|
| `cluster:vm` | Creates the Debian 13 VM from the cloud image |
| `templates:debian` | OS prep (apt update/upgrade + qemu-guest-agent) |
| `backup:vm` | Scheduled VM backup to PBS |
| `network:proxy` | Internal-only reverse proxy for `podman.<domain>` |
## Placement
The module sets no explicit `zone0`, so the VM is placed in the **default environment's zone**;
install it with `--environment <env>` to put it in a specific environment and its associated
zone instead (see [INSTALL.md](./INSTALL.md#placement--zone)).
## Sizing
2 vCPU / **2 GB** RAM / 20 GB disk — enough for the engine, Cockpit and a handful of light
containers. Bump memory/disk to match whatever you actually run on it (container images and
volumes land on the VM disk, thin-provisioned on ZFS).
For installation steps see [INSTALL.md](./INSTALL.md).

View file

@ -0,0 +1,17 @@
#!/usr/bin/env bash
#
# podman module install — thin wrapper.
#
# The VM itself is created by the cluster:vm provider (Debian 13 cloud image),
# then OS-prepped by the templates:debian provider (apt update + qemu-guest-agent
# via update-os.sh). This script applies the module-specific step: installing
# Podman + the Cockpit web console onto the Debian VM. All real work lives in
# update.sh (install == update for this module).
#
# Usage: install.sh <module-name>
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "${SCRIPT_DIR}/update.sh" "$@"

View file

@ -0,0 +1,37 @@
{
"description": "Podman container host — a Debian 13 VM with rootless Podman + the Cockpit web console (cockpit-podman) for managing containers, images and pods from a browser",
"version": "0.1.0",
"appVersion": "5.4",
"releaseDate": "2026-08-18",
"maintainer": "@larsrossen",
"status": "Development",
"vmname": "podman",
"vmid": 812,
"vmtag": "TAPPaaS,Community,Containers",
"ports": [
{ "port": 9090, "protocol": "TCP", "description": "Cockpit web console UI + login (HTTPS)" }
],
"dependsOn": ["cluster:vm", "templates:debian", "backup:vm", "network:proxy"],
"provides": [],
"config": {
"cluster:vm": {
"bios": "seabios",
"ostype": "l26",
"cores": 2,
"memory": "2048",
"diskSize": "20G",
"storage": "tanka1",
"imageType": "img",
"image": "debian-13-generic-amd64.qcow2",
"imageLocation": "https://cdimage.debian.org/cdimage/cloud/trixie/latest",
"cloudInit": "true",
"bridge0": "lan"
},
"network:proxy": {
"proxyPort": 9090,
"proxyUpstreamTls": true,
"proxyUpstreamHttp1": true,
"proxyAllowedZones": ["mgmt"]
}
}
}

81
src/containers/podman/test.sh Executable file
View file

@ -0,0 +1,81 @@
#!/usr/bin/env bash
#
# podman module test — health checks for the Podman container-host VM.
#
# Verifies (from tappaas-cicd, over the Proxmox guest agent + SSH):
# - the VM is reachable
# - podman is installed (version marker + working binary)
# - the cockpit-podman plugin is present
# - the Cockpit web console answers (HTTPS, :9090)
#
# Usage: test.sh <module-name>
#
set -uo pipefail
. /home/tappaas/bin/common-install-routines.sh
MODULE="${1:-podman}"
readonly MGMT="mgmt"
VMID="$(get_config_value 'vmid')"
VMNAME="$(get_config_value 'vmname' "${MODULE}")"
PASS=0; FAIL=0
ok() { info " ${GN}✓${CL} $1"; PASS=$((PASS+1)); }
no() { error " ✗ $1"; FAIL=$((FAIL+1)); }
[[ -n "${VMID}" && "${VMID}" != "null" ]] || die "no vmid for ${MODULE}"
PRIMARY="$(get_primary_node_fqdn 2>/dev/null || echo "tappaas1.${MGMT}.internal")"
NODE="$(ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new "root@${PRIMARY}" \
"pvesh get /cluster/resources --type vm --output-format json 2>/dev/null" \
| jq -r --arg v "${VMID}" '.[] | select(.vmid==($v|tonumber)) | .node' 2>/dev/null | head -1)"
[[ -n "${NODE}" ]] || NODE="$(get_config_value 'node' "$(get_node_hostname 0)")"
IP="$(ssh -o BatchMode=yes "root@${NODE}.${MGMT}.internal" \
"qm guest cmd ${VMID} network-get-interfaces" 2>/dev/null \
| jq -r '.[] | select(.name | test("^lo$") | not) | ."ip-addresses"[]? | select(."ip-address-type"=="ipv4") | ."ip-address"' 2>/dev/null \
| grep -v '^127\.' | head -1)"
info "${BOLD}podman test${CL} (${VMNAME}, VM ${VMID} on ${NODE})"
if [[ -z "${IP}" ]]; then
no "could not resolve VM IP via guest agent"
info "Result: ${PASS} passed, ${FAIL} failed"
exit 1
fi
ok "VM IP resolved (${IP})"
vm() { ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 "tappaas@${IP}" "$@"; }
# Version marker + working podman binary
GOT_VER="$(vm "cat /etc/tappaas-podman.version 2>/dev/null" || true)"
if [[ -n "${GOT_VER}" ]]; then
ok "podman version marker present (${GOT_VER})"
else
no "podman version marker missing (not installed?)"
fi
if vm "command -v podman >/dev/null 2>&1 && podman --version >/dev/null 2>&1"; then
ok "podman binary works ($(vm "podman --version 2>/dev/null" || echo '?'))"
else
no "podman binary not working"
fi
# cockpit-podman plugin installed
if vm "dpkg -s cockpit-podman >/dev/null 2>&1"; then
ok "cockpit-podman plugin installed"
else
no "cockpit-podman plugin not installed"
fi
# Cockpit web console answers on :9090 (login page pre-auth is a pass).
# Plain -s (no --fail): --fail exits 22 on 4xx and would corrupt the -w code.
code="$(vm "curl -sk -o /dev/null -w '%{http_code}' https://localhost:9090/ 2>/dev/null" || echo 000)"
if [[ "${code}" =~ ^(200|302|401|403)$ ]]; then
ok "Cockpit console answers on :9090 (HTTP ${code})"
else
no "Cockpit console did not answer on :9090 (HTTP ${code})"
fi
echo ""
info "Result: ${PASS} passed, ${FAIL} failed"
[[ "${FAIL}" -eq 0 ]]

124
src/containers/podman/update.sh Executable file
View file

@ -0,0 +1,124 @@
#!/usr/bin/env bash
#
# podman module update — install/upgrade Podman + the Cockpit web console on the
# Debian 13 VM.
#
# Podman is the daemonless, rootless-capable container engine; it ships in the
# Debian 13 (trixie) main repo, so no external download is needed. To give it the
# browser interface the module promises, we also install Cockpit and its
# cockpit-podman plugin — Cockpit serves an HTTPS admin console with a PAM LOGIN
# SCREEN on :9090, and cockpit-podman adds the "Podman containers" page for
# managing containers, images and pods. network:proxy publishes that console as
# https://podman.<domain> (internal, mgmt zone only).
#
# This runs on tappaas-cicd: it resolves the VM's IP via the Proxmox guest agent,
# then over SSH installs the packages from apt. Idempotent: it re-runs apt every
# time (apt is a no-op when already current) and records a version marker.
#
# Usage: update.sh <module-name>
#
set -euo pipefail
. /home/tappaas/bin/common-install-routines.sh
MODULE="${1:-podman}"
readonly MGMT="mgmt"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly MARKER="/etc/tappaas-podman.version" # on the VM: records installed podman version
VMNAME="$(get_config_value 'vmname' "${MODULE}")"
VMID="$(get_config_value 'vmid')"
[[ -n "${VMID}" && "${VMID}" != "null" ]] || die "no vmid for ${MODULE}"
# ── Locate the node hosting the VM (HA-safe) and resolve its IP ───────
PRIMARY="$(get_primary_node_fqdn 2>/dev/null || echo "tappaas1.${MGMT}.internal")"
NODE="$(ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new "root@${PRIMARY}" \
"pvesh get /cluster/resources --type vm --output-format json 2>/dev/null" \
| jq -r --arg v "${VMID}" '.[] | select(.vmid==($v|tonumber)) | .node' 2>/dev/null | head -1)"
[[ -n "${NODE}" ]] || NODE="$(get_config_value 'node' "$(get_node_hostname 0)")"
[[ -n "${NODE}" ]] || die "could not locate the node hosting VM ${VMID}"
get_vm_ip() {
ssh -o BatchMode=yes "root@${NODE}.${MGMT}.internal" \
"qm guest cmd ${VMID} network-get-interfaces" 2>/dev/null \
| jq -r '.[] | select(.name | test("^lo$") | not) | ."ip-addresses"[]? | select(."ip-address-type"=="ipv4") | ."ip-address"' 2>/dev/null \
| grep -v '^127\.' | head -1
}
info "${BOLD}Installing Podman + Cockpit${CL} on ${VMNAME} (VM ${VMID}, node ${NODE})"
IP=""
for _ in $(seq 1 18); do IP="$(get_vm_ip)"; [[ -n "${IP}" ]] && break; sleep 10; done
[[ -n "${IP}" ]] || die "could not resolve ${VMNAME} IP via guest agent (is qemu-guest-agent up? templates:debian installs it)"
info " VM IP: ${IP}"
# SSH helper: run a command on the VM as the tappaas user.
vm() { ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 "tappaas@${IP}" "$@"; }
# Wait for SSH (cloud-init may still be finishing).
for _ in $(seq 1 40); do vm "exit 0" 2>/dev/null && break; sleep 3; done
vm "exit 0" 2>/dev/null || die "SSH to tappaas@${IP} not available"
# ── Friendly URL for the operator ────────────────────────────────────
DOMAIN="$(get_variant_config "" 2>/dev/null | jq -r '.domain // empty' || true)"
PROXY_DOMAIN="$(get_config_value 'proxyDomain' "${VMNAME}${DOMAIN:+.${DOMAIN}}")"
COCKPIT_DIRECT_URL="https://${IP}:9090"
if [[ -n "${PROXY_DOMAIN}" ]] && getent hosts "${PROXY_DOMAIN}" >/dev/null 2>&1; then
COCKPIT_URL="https://${PROXY_DOMAIN}"
else
[[ -n "${PROXY_DOMAIN}" ]] && info " ${PROXY_DOMAIN} does not resolve yet — using the direct URL"
COCKPIT_URL="${COCKPIT_DIRECT_URL}"
fi
# ── 1. Install podman + cockpit + cockpit-podman (apt) ───────────────
# podman-compose/slirp4netns give rootless networking + compose-file support;
# cockpit + cockpit-podman provide the web console with a login screen on :9090.
info " Installing podman, podman-compose, cockpit, cockpit-podman (apt)..."
vm "sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq" || die "apt-get update failed"
vm "sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \
podman podman-compose slirp4netns uidmap \
cockpit cockpit-podman \
curl ca-certificates" \
|| die "failed to install podman/cockpit packages"
PODMAN_VER="$(vm "podman --version 2>/dev/null | awk '{print \$3}'" || true)"
[[ -n "${PODMAN_VER}" ]] || die "podman did not install correctly (no version reported)"
info " podman version: ${PODMAN_VER}"
# ── 2. Enable podman socket + Cockpit web console ────────────────────
# Rootless podman API socket for the tappaas user (lets cockpit-podman talk to it
# without root), and the Cockpit HTTPS console on :9090.
info " Enabling the rootless podman socket + Cockpit web console..."
vm "systemctl --user enable --now podman.socket 2>/dev/null || true"
vm "sudo loginctl enable-linger tappaas 2>/dev/null || true" # keep the user socket alive after logout
vm "sudo systemctl enable --now cockpit.socket" || die "failed to enable cockpit.socket"
# Record version marker.
vm "echo '${PODMAN_VER}' | sudo tee ${MARKER} >/dev/null" || warn "could not write version marker"
# ── 3. Wait for the Cockpit console to answer (port 9090) ────────────
info " Waiting for the Cockpit console to come up (https://${IP}:9090)..."
UP=0
for _ in $(seq 1 18); do
code="$(vm "curl -fsk -o /dev/null -w '%{http_code}' https://localhost:9090/ 2>/dev/null" || echo 000)"
[[ "${code}" =~ ^(200|302|401|403)$ ]] && { UP=1; break; }
sleep 5
done
echo ""
if [[ "${UP}" -eq 1 ]]; then
info "${GN}✓ Podman ${PODMAN_VER} + Cockpit console installed${CL}"
else
warn "Packages installed but the Cockpit console did not answer yet — it may still be starting."
fi
echo ""
info "${BOLD}═══ Next steps ═══${CL}"
info " ${BOLD}Console:${CL} ${COCKPIT_URL} (direct: https://${IP}:9090)"
info " Log in with a Linux account on the VM. The cloud-init ${BOLD}tappaas${CL} user has"
info " SSH-key auth and no password, so set one first for the web login:"
info " ssh tappaas@${IP} 'sudo passwd tappaas'"
info " Then open the console and pick ${BOLD}Podman containers${CL} in the left menu."
info " See INSTALL.md for the details (and how to add a dedicated admin user instead)."

17
src/module-catalog.json Normal file
View file

@ -0,0 +1,17 @@
{
"description": "MakerFLOSS DevOps Module Registry — experimental TAPPaaS modules",
"foundationModules": [],
"applicationModules": [
{
"moduleName": "podman",
"repo": "community",
"moduleJson": "src/containers/podman/podman.json",
"vmid": 812,
"stack": "infrastructure",
"category": "containers",
"status": "incomplete"
}
],
"proxmoxTemplates": [],
"testModules": []
}