Ein gültiges TLS-Zertifikat setzt grundsätzlich einen aus dem Internet erreichbaren Dienst voraus. Rein internen Netzwerken fehlt dieser per Definition. Rootless Podman in Verbindung mit Quadlets und Caddy als Reverseproxy umgeht diese Einschränkung durch eine DNS-Challenge, die keinen offenen Port benötigt.
Ein Quadlet bindet einen rootless Podman-Container als regulären systemd-Dienst ein. Der Dienst startet mit dem System und wird nach einem Absturz automatisch neu gestartet.
Wiki.js dient im Folgenden als Beispiel für einen Dienst hinter diesem Setup. Die erstellten Quadlet- und Caddy-Dateien lassen sich auf jeden weiteren Dienst übertragen, der einen HTTP-Port bereitstellt.
Voraussetzungen
Das Setup erfordert Podman, eine eigene Domain bei einem DNS-Provider mit API und einen API-Key des Providers für diese Domain.
Die Einrichtung von rootless Podman ist im Beitrag zum Shiny Server beschrieben.
In Frage kommt jeder DNS-Provider mit eigener API, etwa Cloudflare, Route 53, DigitalOcean oder Hetzner.
Ein A-Record für wiki.example.com zeigt auf die interne IP des Hosts, etwa 192.168.1.10. Der Name löst damit öffentlich auf, während die Adresse dahinter nur im eigenen Netz erreichbar ist.
Der API-Key erhält die minimal notwendigen Rechte. Für die DNS-Challenge genügt ein Schlüssel, der TXT-Einträge für die eigene Domain anlegen kann. Darüber hinaus benötigt der Schlüssel keine Berechtigungen.
Caddy wird später die Ports 80 und 443 belegen. Der Kernel behandelt die Ports 1 bis 1023 als privilegiert und lässt sie nur von root binden. Für rootless Podman lässt sich diese Grenze verschieben. Der folgende Befehl erledigt das:
echo "net.ipv4.ip_unprivileged_port_start=80" \ | sudo tee /etc/sysctl.d/99-podman-ports.confsudo sysctl --systemDanach laufen Podman und der Container weiterhin unter dem angemeldeten Benutzer. Die Verschiebung gilt systemweit.
Bei einer Firewall auf dem Host benötigen die Ports 80 und 443 eine Freigabe.
Zuletzt erhält der Stack einen eigenen Benutzer am Host.
sudo useradd -m podman-svcsudo loginctl enable-linger podman-svcsudo -u podman-svc -ienable-linger startet für den Benutzer eine dauerhafte systemd-Instanz, auch ohne Anmeldung. Alle folgenden Befehle laufen in dieser Shell und erfordern keine root-Rechte.
Zwei Variablen müssen in der Shell gesetzt werden, sie zeigen auf das Laufzeitverzeichnis des Benutzers.
export XDG_RUNTIME_DIR=/run/user/$(id -u)export DBUS_SESSION_BUS_ADDRESS=unix:path=$XDG_RUNTIME_DIR/busOhne sie erreichen weder podman noch systemctl --user die Benutzerinstanz. Der Caddy-Build bricht dann ab.
DNS-Challenge
Die DNS-Challenge legt einen TXT-Eintrag in der Zone an, den die Zertifizierungsstelle beim Nameserver abfragt. Der Host muss dafür keinen Port öffnen und ist während der Ausstellung nicht aus dem Internet erreichbar.
Caddy mit DNS-Modul
Der offizielle Caddy-Container enthält keine DNS-Provider-Module. Für eine DNS-Challenge wird mit xcaddy ein eigener Caddy gebaut, der das Modul des jeweiligen Providers einschließt. Eine Liste der Module findet sich unter github.com/caddy-dns. PROVIDER ist der Repo-Name in Kleinbuchstaben und erscheint weiter unten in der Caddyfile.
Die Containerfile liegt in einem eigenen Verzeichnis, etwa ~/caddy-dns.
mkdir -p ~/caddy-dnsFROM mirror.gcr.io/library/caddy:2-builder AS builder
RUN xcaddy build --with github.com/caddy-dns/PROVIDER
FROM mirror.gcr.io/library/caddy:2
COPY --from=builder /usr/bin/caddy /usr/bin/caddyDer Build läuft in diesem Verzeichnis:
cd ~/caddy-dnspodman build -t caddy-dns .Caddyfile
Die Caddyfile liegt in ~/.config/caddy/Caddyfile.
mkdir -p ~/.config/caddywiki.example.com { tls { dns PROVIDER {env.DNS_API_TOKEN} } reverse_proxy wikijs:3000}Secrets
API-Key und Datenbankpasswort werden als Podman-Secrets angelegt, damit sie nicht in den Unit-Dateien stehen:
printf '%s' 'API-TOKEN' | podman secret create dns_api_token -printf '%s' 'DB-PASSWORT' | podman secret create wiki_db_password -Quadlet
Alle folgenden Dateien liegen in ~/.config/containers/systemd/, wo Podman nach Quadlet-Units sucht.
mkdir -p ~/.config/containers/systemdwiki.network gibt den Containern des Stacks ein eigenes Netz, in dem sie sich über ihren Namen finden.
[Unit]Description=Netz für Wiki.js
[Network]NetworkName=wikiErforderlich sind vier Volume-Units, die sich nur im VolumeName unterscheiden.
[Volume]VolumeName=wiki-db[Volume]VolumeName=wiki-data[Volume]VolumeName=caddy-data[Volume]VolumeName=caddy-configcaddy-data speichert die Zertifikate. Ohne dieses Volume holt Caddy sie bei jedem Neustart erneut.
wiki-db.container ist die Datenbank. Sie veröffentlicht keinen Port. Erreichbar ist sie nur im Netz wiki.
[Unit]Description=PostgreSQL für Wiki.js
[Container]ContainerName=wiki-dbImage=docker.io/library/postgres:17-alpineNetwork=wiki.networkVolume=wiki-db.volume:/var/lib/postgresql/dataSecret=wiki_db_password,type=mount,target=/run/secrets/db_passwordEnvironment=POSTGRES_DB=wikiEnvironment=POSTGRES_USER=wikiEnvironment=POSTGRES_PASSWORD_FILE=/run/secrets/db_password
[Service]Restart=always
[Install]WantedBy=default.targetwikijs.container liest das Passwort über DB_PASS_FILE aus demselben Secret.
[Unit]Description=Wiki.jsRequires=wiki-db.serviceAfter=wiki-db.service
[Container]ContainerName=wikijsImage=ghcr.io/requarks/wiki:2Network=wiki.networkVolume=wiki-data.volume:/wiki/dataSecret=wiki_db_password,type=mount,target=/run/secrets/db_passwordEnvironment=DB_TYPE=postgresEnvironment=DB_HOST=wiki-dbEnvironment=DB_PORT=5432Environment=DB_NAME=wikiEnvironment=DB_USER=wikiEnvironment=DB_PASS_FILE=/run/secrets/db_password
[Service]Restart=always
[Install]WantedBy=default.targetcaddy.container ist der einzige Container mit PublishPort. Der API-Key wird als Umgebungsvariable in den Container übergeben. Das Caddyfile liest ihn als {env.DNS_API_TOKEN}.
[Unit]Description=CaddyRequires=wikijs.serviceAfter=wikijs.service
[Container]ContainerName=caddyImage=localhost/caddy-dns:latestNetwork=wiki.networkPublishPort=80:80PublishPort=443:443Volume=%h/.config/caddy/Caddyfile:/etc/caddy/Caddyfile:ro,ZVolume=caddy-data.volume:/dataVolume=caddy-config.volume:/configSecret=dns_api_token,type=env,target=DNS_API_TOKEN
[Service]Restart=always
[Install]WantedBy=default.targetOhne ContainerName benennt Podman die Container mit dem Präfix systemd-. Dann findet Caddy den Host wikijs nicht.
Starten
systemctl --user daemon-reloadsystemctl --user start caddycaddy startet wikijs und wiki-db über die Abhängigkeiten.
Wiki.js ist danach unter https://wiki.example.com erreichbar und legt beim ersten Aufruf das Administratorkonto an.
