fix: gitea boot persistence via quadlet (incident 2026-08-16)

unattended-upgrades rebooted dankclaw at 15:01; the legacy
podman-compose gitea stack had no systemd units, so rootless podman
did not auto-start the containers. Gitea was down ~1 hour.

- add quadlet units (dankclaw/gitea/quadlet/): gitea-postgres,
  gitea-server, gitea-caddy + gitea.network (DNS enabled; the default
  rootless podman network has dns_enabled=false)
- add gitea stack Caddyfile (upstream gitea-server:3000) and
  .env.example; live password stays in /opt/gitea/.env (0600)
- fix latent name bugs in main stack config: DATABASE_HOST
  gitea-postgres, Caddyfile upstream gitea-server
- docs: rewrite Gitea page for the quadlet stack (management, boot
  persistence, secrets, gotchas, incident log); fix port 3000 in
  stack table; Postgres connection notes
- legacy compose retired on target to /opt/gitea/.deprecated/
This commit is contained in:
Frank
2026-08-16 18:31:38 -07:00
parent 1e9d921b95
commit 04112e88e6
11 changed files with 198 additions and 45 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ hermes.dankclaw.local {
# --- Gitea (migrated) ---
gitea.dankclaw.local {
reverse_proxy gitea:3000
reverse_proxy gitea-server:3000
}
# --- Grafana ---
+1 -1
View File
@@ -129,7 +129,7 @@ services:
restart: unless-stopped
environment:
DATABASE_TYPE: postgres
DATABASE_HOST: postgres
DATABASE_HOST: gitea-postgres
DATABASE_NAME: gitea
DATABASE_USER: gitea
DATABASE_PASSWD: "${GITEA_DB_PASSWORD}"
+4
View File
@@ -0,0 +1,4 @@
# Gitea stack secrets. Copy to /opt/gitea/.env on dankclaw (mode 0600).
# The live password never enters this repo.
GITEA_DB_PASSWORD=changeme
+7
View File
@@ -0,0 +1,7 @@
# Gitea stack Caddyfile (dedicated Caddy container, quadlet-managed).
# Lives on the host at /opt/gitea/data/caddy/Caddyfile.
# Upstream is the quadlet container name gitea-server (port 3000 inside).
gitea.franksdank.cloud {
reverse_proxy gitea-server:3000
}
@@ -0,0 +1,31 @@
# Quadlet unit: Gitea Caddy reverse proxy (rootless, boot-persistent).
#
# Install path on dankclaw: ~/.config/containers/systemd/gitea-caddy.container
# Unit name: gitea-caddy.service (podman 5.x drops the container- prefix)
#
# Serves gitea.franksdank.cloud (public domain, DNS pending). The LAN
# reaches Gitea directly on port 3000. This file holds no secrets.
# See docs/Dankclaw/Gitea.md.
[Unit]
Description=Gitea Caddy reverse proxy container
Wants=gitea-server.service
After=gitea-server.service network.target
[Container]
ContainerName=gitea-caddy
Network=gitea.network
Image=docker.io/library/caddy:latest
PublishPort=80:80
PublishPort=443:443
Volume=/opt/gitea/data/caddy/Caddyfile:/etc/caddy/Caddyfile
Volume=/opt/gitea/data/caddy/site:/srv
Volume=/opt/gitea/data/caddy/config:/config
Volume=/opt/gitea/data/caddy/data:/root/.local/share/caddy
[Service]
TimeoutStartSec=900
Restart=always
[Install]
WantedBy=default.target
@@ -0,0 +1,31 @@
# Quadlet unit: Gitea PostgreSQL (rootless, boot-persistent).
#
# Install path on dankclaw: ~/.config/containers/systemd/gitea-postgres.container
# Unit name: gitea-postgres.service (podman 5.x drops the container- prefix)
#
# The DB password lives in /opt/gitea/.env on the host (mode 0600).
# This file holds no secrets. See docs/Dankclaw/Gitea.md.
# The [Service] EnvironmentFile= line lets systemd expand
# ${GITEA_DB_PASSWORD} in the generated ExecStart.
[Unit]
Description=Gitea PostgreSQL container
After=network.target
[Container]
ContainerName=gitea-postgres
Network=gitea.network
Image=docker.io/library/postgres:16-alpine
EnvironmentFile=/opt/gitea/.env
Environment=POSTGRES_USER=gitea
Environment=POSTGRES_PASSWORD=${GITEA_DB_PASSWORD}
Environment=POSTGRES_DB=gitea
Volume=/opt/gitea/data/gitea-postgres:/var/lib/postgresql/data
[Service]
EnvironmentFile=/opt/gitea/.env
TimeoutStartSec=900
Restart=always
[Install]
WantedBy=default.target
@@ -0,0 +1,44 @@
# Quadlet unit: Gitea web server (rootless, boot-persistent).
#
# Install path on dankclaw: ~/.config/containers/systemd/gitea-server.container
# Unit name: gitea-server.service (podman 5.x drops the container- prefix)
#
# The [Service] EnvironmentFile= line lets systemd expand
# ${GITEA_DB_PASSWORD} in the generated ExecStart.
# The DB password lives in /opt/gitea/.env on the host (mode 0600).
# This file holds no secrets. See docs/Dankclaw/Gitea.md.
#
# Ports (live): 3000 = HTTP UI/API, 2222 = git SSH. The docs table in
# Dankclaw Stack.md historically said 3001; the live host port is 3000.
[Unit]
Description=Gitea web server container
Wants=gitea-postgres.service
After=gitea-postgres.service network.target
[Container]
ContainerName=gitea-server
Network=gitea.network
Image=docker.io/gitea/gitea:latest
EnvironmentFile=/opt/gitea/.env
Environment=DATABASE_TYPE=postgres
Environment=DATABASE_HOST=gitea-postgres
Environment=DATABASE_NAME=gitea
Environment=DATABASE_USER=gitea
Environment=DATABASE_PASSWD=${GITEA_DB_PASSWORD}
Environment=DATABASE_PORT=5432
Environment=USER_UID=1000
Environment=USER_GID=1000
PublishPort=0.0.0.0:3000:3000
PublishPort=2222:22
Volume=/opt/gitea/data/gitea-gitea:/data
Volume=/etc/timezone:/etc/timezone:ro
Volume=/etc/localtime:/etc/localtime:ro
[Service]
EnvironmentFile=/opt/gitea/.env
TimeoutStartSec=900
Restart=always
[Install]
WantedBy=default.target
+10
View File
@@ -0,0 +1,10 @@
# Quadlet network: private bridge for the Gitea stack (rootless).
#
# Install path on dankclaw: ~/.config/containers/systemd/gitea.network
# Unit name: gitea-network.service (podman network name: systemd-gitea)
#
# DNS is enabled by default for quadlet networks. This matters: the
# automatic "podman" default network has dns_enabled=false, so container
# names (gitea-postgres, gitea-server, gitea-caddy) would not resolve.
[Network]
+1 -1
View File
@@ -23,7 +23,7 @@ graph LR
| ComfyUI | comfyui.dankclaw.local | 8188 | [[ComfyUI]] |
| OpenWebUI | chat.dankclaw.local | 3000 | [[OpenWebUI]] |
| Hermes | hermes.dankclaw.local | 8090 | [[Hermes]] |
| Gitea | gitea.dankclaw.local | 3001 | [[Gitea]] |
| Gitea | gitea.dankclaw.local | 3000 | [[Gitea]] |
| Grafana | grafana.dankclaw.local | 3002 | [[Grafana]] |
| Prometheus | prometheus.dankclaw.local | 9090 | [[Prometheus]] |
| Homepage | dankclaw.local | 80/443 | — |
+64 -40
View File
@@ -1,63 +1,87 @@
# Gitea
Gitea is a lightweight self-hosted Git service with issues, pull requests, and a web UI. It already runs on Dankclaw as a podman-compose stack at `/opt/gitea/` and is being migrated into this repository.
Gitea is a self-hosted Git service with issues, pull requests, and a web UI. On Dankclaw it runs as three Quadlet-managed containers in the rootless podman session of user `dank`.
**Host:** Dankclaw — **Hostname:** `gitea.dankclaw.local` — **Port:** 3001 — **Docs:** https://docs.gitea.com
**Host:** Dankclaw — **LAN access:** `http://dankclaw:3000` (or `http://192.168.0.143:3000`) — **Public domain:** `gitea.franksdank.cloud` (DNS pending) — **Docs:** https://docs.gitea.com
## Facts (from live inventory + knowledge base)
## Containers
| Item | Value |
|------|-------|
| Image | `docker.io/gitea/gitea:latest` |
| DB image | `docker.io/library/postgres:16-alpine` |
| Gitea port | 3000 → 3001 (host, proxied) |
| SSH port | 2222 → 22 (host) |
| DB | postgres on the podman network (internal 5432) |
| Current location | `/opt/gitea/podman-compose.yml` on Dankclaw |
| Container | Unit | Image | Purpose |
|-----------|------|-------|---------|
| `gitea-postgres` | `gitea-postgres.service` | `docker.io/library/postgres:16-alpine` | Database |
| `gitea-server` | `gitea-server.service` | `docker.io/gitea/gitea:latest` | Web UI, API, git (HTTP :3000, SSH :2222) |
| `gitea-caddy` | `gitea-caddy.service` | `docker.io/library/caddy:latest` | Reverse proxy for the public domain |
## Migration Plan
The containers share the private bridge network `systemd-gitea`. The quadlet network file `gitea.network` enables DNS, so the containers resolve each other by name. The automatic `podman` default network has DNS disabled. Do not run the Gitea containers on it.
1. Copy the live `/opt/gitea/podman-compose.yml` into `dankclaw/compose.yaml` (redacted: secrets moved to `.env`).
2. Keep the bind-mount data at `/opt/gitea/data/` so repos survive.
3. Commit the compose, push, and pull on Dankclaw.
4. Replace the live stack by bringing up the repo-managed compose.
## Management
## Env (from the live compose, redacted)
Run all commands as `dank` on Dankclaw.
```yaml
environment:
DATABASE_TYPE: postgres
DATABASE_HOST: postgres
DATABASE_NAME: gitea
DATABASE_USER: gitea
DATABASE_PASSWD: "${GITEA_DB_PASSWORD}" # from .env, never committed
USER_UID: "1000"
USER_GID: "1000"
| Action | Command |
|--------|---------|
| Start all | `systemctl --user start gitea-postgres gitea-server gitea-caddy` |
| Stop all | `systemctl --user stop gitea-caddy gitea-server gitea-postgres` |
| Restart Gitea only | `systemctl --user restart gitea-server` |
| Status | `systemctl --user status gitea-postgres gitea-server gitea-caddy` |
| Container logs | `podman logs -f gitea-server` |
| Unit logs | `journalctl --user -u gitea-server -f` |
The units start in dependency order:
```mermaid
graph LR
N[gitea-network.service] --> P[gitea-postgres.service]
P --> S[gitea-server.service]
S --> C[gitea-caddy.service]
```
## Volumes
## Boot persistence
| Container path | Host path (live) |
|----------------|------------------|
| `/data` | `/opt/gitea/data/gitea-gitea` |
| `/var/lib/postgresql/data` | `/opt/gitea/data/gitea-postgres` |
| `/root/.local/share/caddy` | `/opt/gitea/data/caddy/data` |
User-level linger is enabled for `dank` (`loginctl enable-linger dank`). The units start at host boot without a login session. Each unit carries `[Install] WantedBy=default.target`. The podman generator applies it at boot; no symlink is needed.
## Healthcheck
The containers use the quadlet fresh-start model. A stop removes the container (`podman run --rm --replace`). The next start recreates it. The bind-mounted data survives.
Gitea serves `/api/v1/version` and the web UI on `/`.
## Secrets
## Backup
The database password lives in `/opt/gitea/.env` on the host (mode 0600). The quadlet files reference it with `EnvironmentFile=`. The password never enters this repository. Placeholders live in [`dankclaw/gitea/.env.example`](gitea/.env.example).
Back up `/opt/gitea/data/` (tar) or `pg_dump` the gitea database. See the knowledge base page [[Gitea Data Persistence]].
Do not change the password in place. Change the PostgreSQL role first, then update `/opt/gitea/.env` and the `[database] PASSWD` line in the app.ini (see Gotchas), then restart the stack.
## Configuration files
| File | Location | Repo copy |
|------|----------|-----------|
| Quadlet units | `~/.config/containers/systemd/gitea-{postgres,server,caddy}.container`, `gitea.network` | [`dankclaw/gitea/quadlet/`](gitea/quadlet/) |
| Caddyfile | `/opt/gitea/data/caddy/Caddyfile` | [`dankclaw/gitea/Caddyfile`](gitea/Caddyfile) |
| Secrets | `/opt/gitea/.env` (0600) | [`dankclaw/gitea/.env.example`](gitea/.env.example) |
After editing a repo copy, copy it to the target, then run `systemctl --user daemon-reload` and restart the affected units.
## Data volumes
| Container path | Host path | Contents |
|----------------|-----------|----------|
| `/data` | `/opt/gitea/data/gitea-gitea` | Git repositories, app.ini, attachments |
| `/var/lib/postgresql/data` | `/opt/gitea/data/gitea-postgres` | PostgreSQL data |
| `/etc/caddy/Caddyfile`, `/root/.local/share/caddy` | `/opt/gitea/data/caddy/...` | Caddy config and TLS state |
Back up `/opt/gitea/data/` (tar) or `pg_dump` the database. See the knowledge base page [[Gitea Data Persistence]].
## Gotchas
- The live compose contains a real DB password — never commit it (test-enforced).
- Gitea and postgres both run as UID 1000; keep the bind mounts owned by `dank`.
- DNS `gitea.franksdank.cloud` → 71.227.227.172 is pending; the LAN hostname works now.
- **app.ini wins over env vars.** Gitea writes `/data/gitea/conf/app.ini` (host: `/opt/gitea/data/gitea-gitea/gitea/conf/app.ini`) on first start. The `[database] HOST` and `PASSWD` lines persist and override the `DATABASE_HOST`/`DATABASE_PASSWD` env vars. When you rename the database container, edit the `HOST` line.
- **The files in the data directory are owned by host UID 100999**, not `dank` (rootless ID mapping: container uid 1000 → host 100999). Edit them inside the container with `podman exec --user 1000 gitea-server ...`.
- **The host auto-reboots.** `unattended-upgrades` reboots Dankclaw when a package needs it. The quadlet units make Gitea survive that. See the 2026-08-16 incident below.
- **Port 3000 vs 3001.** The live host port is 3000. The full-stack [`dankclaw/compose.yaml`](../compose.yaml) maps 3001. Align the two before deploying the full stack.
- **Legacy compose retired.** `/opt/gitea/podman-compose.yml` is retired to `/opt/gitea/.deprecated/`. Do not run it; its containers would collide with the quadlet units on ports 3000, 2222, 80, and 443.
## Incident: 2026-08-16 — down after unattended reboot
`unattended-upgrades` rebooted Dankclaw at 15:01 on 2026-08-16. The Gitea containers ran through podman-compose with `restart: unless-stopped` and no systemd units. Rootless podman does not auto-start containers at boot without units. Gitea stayed down until a manual start at 16:02. The fix: migrate the stack to quadlet units with linger (this page). After the fix, verify on the next reboot.
## See Also
- [[Dankclaw Stack]]
- Knowledge base: [[Gitea]] (tools), [[Gitea Data Persistence]], [[Gitea Architecture]]
- [[Postgres]]
- Knowledge base: [[Gitea]] (tools), [[Gitea Management]], [[Gitea Data Persistence]], [[Docker and Podman]]
+4 -2
View File
@@ -29,17 +29,19 @@ services:
## Gitea Connection
Gitea reaches the database over the podman network:
Gitea reaches the database over the `systemd-gitea` network (quadlet-managed live, compose-managed after the full-stack deploy):
```yaml
environment:
DATABASE_TYPE: postgres
DATABASE_HOST: postgres
DATABASE_HOST: gitea-postgres
DATABASE_NAME: gitea
DATABASE_USER: gitea
DATABASE_PASSWD: "${GITEA_DB_PASSWORD}"
```
The live `app.ini` on the host overrides env vars. When you rename the database container, update its `[database] HOST` line. See [[Gitea]] Gotchas.
## Migration
The live Gitea stack on Dankclaw already runs this image with the data at `/opt/gitea/data/gitea-postgres`. The migration keeps the bind mount in place; see [[Gitea]].