Radicale: the first new app through the checklist — template, record, fixture, first ladder step 3.8.0 -> 3.8.1
gates / gates (push) Successful in 2s

Calendar and contacts (CalDAV/CardDAV), ghcr.io/kozea/radicale:3.8.1. Login file written from the generated password on
the first start only (R-765: rewriting it at every start lost the household's login on a restore). onboarding/radicale.md
complete; bench + 9202 proven; FIRST-ADMIN, README, Hungarian freeze.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-01 16:58:56 +02:00
parent 55b8c8a147
commit 195129cbb1
8 changed files with 349 additions and 1 deletions
+13
View File
@@ -1,3 +1,16 @@
## Radicale — the first app through the new-app checklist (2026-10-01, evening)
- **`templates/radicale/`** — calendar and contacts (CalDAV/CardDAV), the Radicale project's own image
`ghcr.io/kozea/radicale:3.8.1`. The image ships no config and Radicale 3 lets nobody in by default (`denyall`), so the
start command writes a bcrypt login file from the box's generated password on the FIRST start only, then runs
Radicale with htpasswd auth. First admin class 1; no setup gate (401 from the first answer). Its first ladder step
3.8.0 -> 3.8.1 proven on the bench (swap 0) and on 9202, written by `upgrade-test.py --write-ladder`.
- **Found and fixed before publishing (R-765):** the first template rewrote the login file at EVERY start; a remove +
restore then left the household with a password no page shows (a restore regenerates `type: password` values by
design, expecting the app's own login to come back with its data). Written once, it does — measured.
- `onboarding/radicale.md` (every id done or n/a), fixture `Radicale` in `upgrade_fixtures_box.py` (CalDAV seed,
three negative controls), FIRST-ADMIN row, README tables, Hungarian freeze (`--add-app`).
## wger hidden until R-762 and R-763 are fixed (2026-10-01, late afternoon)
- Operator ruling 2026-10-01: `templates/wger/.felhom.yml` gets `lifecycle: hidden` — not offered for new installs; an
+1
View File
@@ -70,6 +70,7 @@ asks the probe first where there is one. Open sign-up is closed by the box after
| plex | 2 | the household's plex.tv claim token | – | fine | R |
| privatebin | 5 | anonymous pastes by design | – | fine | R |
| **radarr** | 4 | first visitor sets auth | **setup gate**, opened by the household's press („Kész, beállítottam", confirm first); no sign-up to close (single user / no accounts); `/initialize.json` hands anyone the API key BEFORE the setup — the gate hides it; after, it needs a login | **GATED** — catalog, 2026-09-29 (decisions 46–47) | **M 9202**: stranger → gate page / 401; household reached the first-setup screen; gate opened after the setup; the app answered after (`felhom.eu/documentation/audits/gate-rollout-2026-09-29/`) |
| **radicale** | 1 | the box generates the password at install; the container writes Radicale's login file from it on the FIRST start only (Radicale 3 ships with `auth = denyall`) | — (no first-run screen, no sign-up route) | **NEW 2026-10-01** — catalog, onboarding record `onboarding/radicale.md` | **M 9202** (drill catalog): no login 401 from the first answer; wrong 401; right 207; 15 wrong tries → right at once (no lock, ~1.3 s delay each); remove + restore → the household's login comes back with the data (`felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/`) |
| **rallly** | 4 | magic link to any e-mail | **setup gate**, the household's press; magic link by e-mail — not provable on 9202 | **GATED** — catalog, 2026-09-29; the opening NOT proven | **M 9202**: stranger → gate page / 401, household reached the first-setup screen (`felhom.eu/documentation/audits/gate-rollout-2026-09-29/`) |
| **recipe-importer** | 4 | our own image, open until set | **setup gate**, opened by the household's press („Kész, beállítottam", confirm first); no sign-up to close (single user / no accounts); our own app: open until a password is set in its settings — the confirm says so | **GATED** — catalog, 2026-09-29 (decisions 46–47) | **M 9202**: stranger → gate page / 401; household reached the first-setup screen; gate opened after the setup; the app answered after (`felhom.eu/documentation/audits/gate-rollout-2026-09-29/`) |
| **romm** | 4 | first unauthenticated `POST /api/users` creates the user (harness); the stale `admin / admin` note is **removed** (2026-09-29) | **setup gate**, opened by probe `/api/heartbeat` → `SYSTEM.SHOW_SETUP_WIZARD` = false; the app itself refuses a stranger's second first-admin / sign-up call | **GATED** — catalog, 2026-09-29 (decisions 46–47) | **M 9202**: stranger → gate page / 401; household reached the first-setup screen; gate opened after the setup; the app answered after (`felhom.eu/documentation/audits/gate-rollout-2026-09-29/`) |
+2
View File
@@ -320,6 +320,7 @@ block + the matching compose `${VAR}` lines.
| Plex | None (file) | 512M / 2048M | no | `${HDD_PATH}/media/` | plex.* |
| PrivateBin | None (file) | 30M / 128M | yes | -- | paste.* |
| Radarr | None (file) | 150M / 512M | yes | `${HDD_PATH}/media/` | radarr.* |
| Radicale | None (file) | 40M / 128M | yes | -- | calendar.* |
| Rallly | PostgreSQL | 50M / 256M | yes | -- | poll.* |
| RomM | MariaDB + Redis | 300M / 1024M | no | `${HDD_PATH}/storage/romm/` | arcade.* |
| Jellyseerr | None (file) | 100M / 384M | yes | -- | requests.* |
@@ -378,6 +379,7 @@ block + the matching compose `${VAR}` lines.
| Plex | yes | yes | PLEX_CLAIM |
| PrivateBin | yes | -- | -- |
| Radarr | yes | yes | -- |
| Radicale | yes | -- | RADICALE_PASSWORD (password) |
| Rallly | yes | -- | SECRET_PASSWORD, DB_PASSWORD |
| RomM | yes | yes | DB_PASSWORD, MYSQL_ROOT_PASSWORD, ROMM_AUTH_SECRET_KEY |
| Jellyseerr | yes | -- | -- |
+73
View File
@@ -0,0 +1,73 @@
# Onboarding record — radicale
app: radicale
opened: 2026-10-01
template_at: the commit that publishes this record (ghcr.io/kozea/radicale:3.8.1; the step 3.8.0 -> 3.8.1 proven on both venues)
<!--
Evidence root: felhom.eu/documentation/audits/new-apps-2026-10-01/ — FIT.md (group 0), bench/radicale/ (bench 9401,
swap 0), box/radicale/ (scratch guest 9202, drill catalog), undo/box/radicale/ (the forced-fail case), shots/ (8.3).
box/radicale-attempt1/ is the FIRST template (login file rewritten at every start): a remove + restore locked the
household out (2.5) — the template was changed, and every box and bench row below was measured again on the second.
-->
0.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/A/A1-github-facts.txt — GPL-3.0; the project's own image, pulled, never redistributed
0.2 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/A/A1-github-facts.txt — v3.8.1 on 2026-09-24, 13 releases in 2026, 10 open issues
0.3 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/FIT.md — `3.8.0`, `3.8.1` version tags; amd64, arm64, arm/v7
0.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — no start-time network job (the start command is ours, read in the compose); no phone-home found in the image's config or code
0.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/install.txt — healthy 22 s after the press with nothing fetched but the image
0.6 | n/a | Radicale serves CalDAV and CardDAV over plain HTTP on one port
0.7 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — a phone client's discovery through traefik over https: well-known 301, PROPFIND 207, the principal and the calendar list; no setup gate stands in the way
0.8 | done | app-catalog-felhom.eu/templates/radicale/.felhom.yml — tagline + use_cases: the family calendar and contacts on its own server
0.9 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/bench/radicale/evidence/MV-radicale/verdict.json — runs on the bench; no public name is read
1.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/G/gates-radicale.txt — image-pins and image-resolvable exit 0
1.2 | n/a | Radicale stores files, no database engine anywhere
1.3 | n/a | no MariaDB or PostgreSQL service in this template
1.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/bench/radicale/evidence/MV-radicale/verdict.json ; felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/step.txt — the previous release 3.8.0 seeded, stepped INTO the pin 3.8.1, read back on both venues (file storage; no migration switch exists)
1.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — PID 1 is `radicale` itself, the project's own server
1.6 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S1-radicale-reads.txt — the only secret is the login; no image config ships and auth defaults to `denyall`; the template sets htpasswd + bcrypt from the generated password
1.7 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S1-radicale-reads.txt — the image's entrypoint is `radicale --hosts …` with no switches; the template's own start command is the whole start-up
1.8 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — an unknown page answers 401 with no debug output
1.9 | done | app-catalog-felhom.eu/templates/radicale/docker-compose.yml — the password is NOT a data_key: the login file is written once and lives on the data volume, so a restore brings the old login back with the calendars (measured, 2.5); marking it would make a restore after a remove refuse
2.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/restore.txt — the seed came back from the volume backup alone after the app and its volume were removed (the persistence question answered by a restore)
2.2 | done | app-catalog-felhom.eu/templates/radicale/docker-compose.yml — one named volume (NVMe): `collections/` + the login file
2.3 | n/a | needs_hdd false: no drive path to classify; the tier-1 unit holds the one volume
2.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — runs as uid 1000 `radicale`; the volume and the 0600 login file are its own
2.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/restore.txt — the update's per-app backup, remove keeping backups, the household's restore button, the event read back with the ORIGINAL login (attempt 1 failed here: box/radicale-attempt1/restore.txt)
2.6 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/restore.txt ; felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/remove.txt — keep-backups and with-data both delete the volume; no drive data exists
2.7 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — 92 KB after the seed; a family's calendars stay in megabytes
2.8 | n/a | Radicale stores no uploaded files: events and contacts are text, read back over CalDAV/CardDAV (2.5)
3.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/install.txt — class 1: the box generates the password, the login works on a fresh install (C1)
3.2 | n/a | no known default login: without the template's config Radicale lets nobody in (`denyall`)
3.3 | n/a | no first-run screen: every request needs the login from the first answer
3.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — no sign-up route; a stranger's MKCOL on a new principal answers 401
3.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/poll.txt — 25 stranger polls from the press: 404 until routed, then 401; never in
3.6 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — 15 wrong tries: no lock, ~1.3 s delay each (Radicale's own); the right password works at once
3.7 | done | app-catalog-felhom.eu/templates/radicale/.felhom.yml — one shared login (no user admin in Radicale); `add_people` says each family member uses it and makes their own calendar. Changing the password is not offered in Radicale
3.8 | done | app-catalog-felhom.eu/templates/radicale/docker-compose.yml — the password is read from the environment inside the program, never pasted into code
3.9 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — CalDAV and CardDAV clients' calls through traefik: right 207/201, wrong 401; the browser UI logs in with the same pair (S2 screenshots)
4.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S1-radicale-reads.txt — wget is in the image (checked one tool per run); `127.0.0.1:5232/.web/`
4.2 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/G/gates-radicale.txt — probe-matches-compose exit 0
4.3 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/install.txt — healthy 22 s after the press, 0 restarts
4.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — paused: the controller read `stopped`, then `running` after unpause
4.5 | done | app-catalog-felhom.eu/templates/radicale/docker-compose.yml — `container_name: radicale`, the only service
5.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/bench/radicale/mem-radicale.csv — bench, swap 0, from birth: peak anon ~26 MiB of 128M, 0 kills
5.2 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/bench/radicale/evidence/MV-radicale/verdict.json — 10-min watch: anon peak ~20 %, 0 kills, 0 restarts
5.3 | done | app-catalog-felhom.eu/templates/radicale/.felhom.yml — mem_limit 128M = the one service's 128M
5.4 | n/a | Radicale is a Python app, no self-sizing heap
5.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S1-radicale-reads.txt — pi_compatible true (arm64 + arm/v7); the image is ~97 MB
6.1 | done | app-catalog-felhom.eu/scripts/upgrade_fixtures_box.py — `Radicale`: a calendar and an event over CalDAV, with three negative controls
6.2 | done | app-catalog-felhom.eu/templates/radicale/.felhom.yml — the first ladder step 3.8.0 -> 3.8.1, written by `upgrade-test.py --write-ladder` from both verdicts
6.3 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/undo/box/radicale/step.txt — a drill step to an image that exits: verifying 305 s, the box undid it by itself, 3.8.1 back, the event read back (on attempt 1's template; the undo is the box's, the start command played no part)
6.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/bench/radicale/evidence/MV-radicale/verdict.json — no file changes at start (no mark)
6.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/A/A1-github-facts.txt — plain `x.y.z` from v3.0 to v3.8.1
7.1 | n/a | Radicale sends no mail at all; there is nothing to map
8.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/G/gates-radicale.txt — copy-i18n exit 0 (frozen with --add-app)
8.2 | done | app-catalog-felhom.eu/templates/radicale/.felhom.yml — tagline, use_cases, first_steps, add_people; each step followed on 9202 (the web login, MKCALENDAR, the phone discovery calls)
8.3 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S3-assets.txt — radicale-logo.svg and three screenshots answer 200 on felhom.eu; boxes get them with the next hub release (R-766)
8.4 | done | app-catalog-felhom.eu/README.md — both tables; FIRST-ADMIN row; category productivity; catalog_since
8.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S3-assets.txt — the website's count, read
9.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/G/gates-radicale.txt — every gate exit 0, the runtime volume gate included
9.2 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/install.txt ; felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — a fresh install from the drill catalog, as household and stranger
9.3 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/README.md — every row done or n/a; the one defect found (attempt 1, 2.5) was fixed before publishing
9.4 | done | app-catalog-felhom.eu/onboarding/radicale.md — published in one commit with this record (the onboarding gate)
+24 -1
View File
@@ -832,6 +832,27 @@
"deploy_fields[SUBDOMAIN].label": "Aldomain",
"description": "Automatikus film letöltő és rendszerező"
},
"radicale": {
"app_info.add_people": "A Radicale-ban egy közös bejelentkezés van. A családtagod ugyanezzel a névvel és jelszóval veszi fel a telefonjára, és saját naptárat hoz létre magának.",
"app_info.first_steps[0]": "Nyisd meg a calendar.DOMAIN címet, és jelentkezz be a Beállítások oldalon látható névvel és jelszóval",
"app_info.first_steps[1]": "Hozz létre egy naptárat és egy címjegyzéket (\"Create new addressbook or calendar\")",
"app_info.first_steps[2]": "Androidon telepítsd a DAVx5-öt, és add meg: https://calendar.DOMAIN, a nevet és a jelszót",
"app_info.first_steps[3]": "iPhone-on: Beállítások, Naptár, Fiókok, Új fiók, Egyéb, CalDAV-fiók - szerver: calendar.DOMAIN",
"app_info.tagline": "Naptár és névjegyek a saját szervereden - a telefonod szinkronizál vele",
"app_info.use_cases[0]": "A telefonod naptára és névjegyei a saját szervereden, nem a Google-nél vagy az Apple-nél",
"app_info.use_cases[1]": "Közös családi naptár, amit mindenki lát a telefonján",
"app_info.use_cases[2]": "Androidon a DAVx5, iPhone-on és Macen a beépített naptár szinkronizál vele",
"app_info.use_cases[3]": "Thunderbird és más asztali naptárak is használhatják",
"deploy_fields[DOMAIN].description": "A szerver domain neve",
"deploy_fields[DOMAIN].label": "Domain",
"deploy_fields[RADICALE_PASSWORD].description": "Telepítéskor generált jelszó. A telefonod naptára és névjegyei is ezzel jelentkeznek be.",
"deploy_fields[RADICALE_PASSWORD].label": "Jelszó",
"deploy_fields[RADICALE_USER].description": "Ezzel a névvel jelentkezel be a telefonodon és a böngészőben",
"deploy_fields[RADICALE_USER].label": "Felhasználónév",
"deploy_fields[SUBDOMAIN].description": "Az alkalmazás aldomainje",
"deploy_fields[SUBDOMAIN].label": "Aldomain",
"description": "Naptár és névjegyek a saját szervereden (CalDAV / CardDAV)"
},
"rallly": {
"app_info.first_steps[0]": "Nyisd meg a poll.DOMAIN címet a böngészőben",
"app_info.first_steps[1]": "Hozz létre egy új szavazást",
@@ -1162,5 +1183,7 @@
"description": "ShareX/Flameshot szerver - screenshot és fájlmegosztás"
}
},
"reasons": {}
"reasons": {
"radicale": "new app 2026-10-01 through NEW-APP-CHECKLIST.md: te-form, no kérjük; reviewed by CC against the operator rules"
}
}
+56
View File
@@ -1875,6 +1875,61 @@ class UptimeKuma:
return found
# =============================================================================================
class Radicale:
"""Radicale (2026-10-01, new app through NEW-APP-CHECKLIST.md). THE FRONT DOOR is CalDAV itself, the route every
phone and desktop calendar uses: HTTP basic auth with the box's generated login (`RADICALE_USER`, default
"csalad"; `RADICALE_PASSWORD`). Seed: MKCALENDAR a calendar, PUT one event (a unique UID and SUMMARY); read back
with GET. Negative controls on every readback: no credentials 401, a wrong password 401, an event never created
404 — so a read that says "found" cannot be an open door or a catch-all."""
sub = "calendar"
@staticmethod
def _creds(w):
g = w.GENERATED.get("radicale") or {}
return g.get("RADICALE_USER") or "csalad", g.get("RADICALE_PASSWORD") or ""
def seed(self, w, sub, say):
if not w.wait_app(sub, "/.web/", want=("200",), tries=60):
self.tried = "/.web/ never answered 200"
return None
user, pw = self._creds(w)
cal = "felhom-" + secrets.token_hex(4)
uid = "ev-" + secrets.token_hex(6) + "@felhom"
summary = "Felhom teszt " + secrets.token_hex(3)
rc, code, _ = w.app_curl(sub, f"/{user}/{cal}/", "-u", f"{user}:{pw}", method="MKCALENDAR")
say(f" radicale: MKCALENDAR /{user}/{cal}/ http={code}")
if code != "201":
self.tried = f"MKCALENDAR -> {code}"
return None
ics = ("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//felhom//fixture//EN\r\nBEGIN:VEVENT\r\n"
f"UID:{uid}\r\nDTSTAMP:20261001T100000Z\r\nDTSTART:20261002T100000Z\r\nSUMMARY:{summary}\r\n"
"END:VEVENT\r\nEND:VCALENDAR\r\n")
rc, code, _ = w.app_curl(sub, f"/{user}/{cal}/{uid}.ics", "-u", f"{user}:{pw}", "-H", "Content-Type: text/calendar",
data=ics, method="PUT")
say(f" radicale: PUT event http={code}")
if code != "201":
self.tried = f"PUT event -> {code}"
return None
return {"user": user, "pw": pw, "cal": cal, "uid": uid, "summary": summary}
def verify(self, w, sub, t, say):
if not w.wait_app(sub, "/.web/", want=("200",), tries=60):
say(" radicale: /.web/ never answered")
return False
path = f"/{t['user']}/{t['cal']}/{t['uid']}.ics"
rc, c_none, _ = w.app_curl(sub, path)
rc, c_wrong, _ = w.app_curl(sub, path, "-u", f"{t['user']}:{t['pw']}x")
rc, c_absent, _ = w.app_curl(sub, f"/{t['user']}/{t['cal']}/never-{secrets.token_hex(3)}.ics", "-u", f"{t['user']}:{t['pw']}")
if c_none != "401" or c_wrong != "401" or c_absent != "404":
say(f" radicale: READBACK UNUSABLE — no auth {c_none}, wrong password {c_wrong}, absent event {c_absent}")
return False
rc, code, out = w.app_curl(sub, path, "-u", f"{t['user']}:{t['pw']}")
ok = code == "200" and f"SUMMARY:{t['summary']}" in (out or "")
say(f" radicale: readback http={code} found={ok} (controls: no auth {c_none}, wrong {c_wrong}, absent {c_absent})")
return ok
FIXTURES = {
"uptime-kuma": UptimeKuma(),
"crafty-controller": Crafty(),
@@ -1911,4 +1966,5 @@ FIXTURES = {
"wishlist": Wishlist(),
"claper": Claper(),
"calcom": Calcom(),
"radicale": Radicale(),
}
+116
View File
@@ -0,0 +1,116 @@
# =============================================================================
# .felhom.yml - App metadata for felhom-controller
# =============================================================================
# Radicale — the Radicale project's own image (ghcr.io/kozea/radicale). Onboarding record: onboarding/radicale.md
# (NEW-APP-CHECKLIST.md). First admin: CLASS 1 — the box generates the password at install; the container writes the
# login file from it on the first start only (see the compose header). No setup gate: there is no first-run screen, and a
# request without the right login answers 401 from the first second.
# --- Display info (shown on dashboard) ---
display_name: "Radicale"
description: "Naptár és névjegyek a saját szervereden (CalDAV / CardDAV)"
category: "productivity"
subdomain: "calendar"
slug: "radicale"
# catalog_since: the date THIS repo last changed this app's pinned images. Any commit that
# changes an image: line must set this to the same day (see CLAUDE.md).
catalog_since: "2026-10-01"
# --- Resource hints (displayed on deploy screen) ---
resources:
mem_request: "40M"
mem_limit: "128M" # one service: radicale 128M
pi_compatible: true
needs_hdd: false
# --- Deploy wizard fields ---
deploy_fields:
- env_var: DOMAIN
label: "Domain"
type: domain
description: "A szerver domain neve"
locked_after_deploy: true
- env_var: SUBDOMAIN
label: "Aldomain"
type: subdomain
default: "calendar"
required: true
locked_after_deploy: true
description: "Az alkalmazás aldomainje"
- env_var: RADICALE_USER
label: "Felhasználónév"
type: text
default: "csalad"
required: true
locked_after_deploy: true
description: "Ezzel a névvel jelentkezel be a telefonodon és a böngészőben"
- env_var: RADICALE_PASSWORD
label: "Jelszó"
type: password
generate: "password:24"
locked_after_deploy: true
description: "Telepítéskor generált jelszó. A telefonod naptára és névjegyei is ezzel jelentkeznek be."
# --- Customer-facing info ---
app_info:
tagline: "Naptár és névjegyek a saját szervereden - a telefonod szinkronizál vele"
add_people: "A Radicale-ban egy közös bejelentkezés van. A családtagod ugyanezzel a névvel és jelszóval veszi fel a telefonjára, és saját naptárat hoz létre magának."
docs_url: "https://radicale.org/v3.html"
use_cases:
- 'A telefonod naptára és névjegyei a saját szervereden, nem a Google-nél vagy az Apple-nél'
- 'Közös családi naptár, amit mindenki lát a telefonján'
- 'Androidon a DAVx5, iPhone-on és Macen a beépített naptár szinkronizál vele'
- 'Thunderbird és más asztali naptárak is használhatják'
first_steps:
- 'Nyisd meg a calendar.DOMAIN címet, és jelentkezz be a Beállítások oldalon látható névvel és jelszóval'
- 'Hozz létre egy naptárat és egy címjegyzéket ("Create new addressbook or calendar")'
- 'Androidon telepítsd a DAVx5-öt, és add meg: https://calendar.DOMAIN, a nevet és a jelszót'
- 'iPhone-on: Beállítások, Naptár, Fiókok, Új fiók, Egyéb, CalDAV-fiók - szerver: calendar.DOMAIN'
# --- Controller health probe (dials what the compose healthcheck dials) ---
healthcheck:
checks:
- type: api
port: 5232
path: "/.web/"
expect:
status: 200
i18n:
en:
description: 'Calendar and contacts on your own server (CalDAV / CardDAV)'
app_info:
tagline: 'Calendar and contacts on your own server - your phone syncs with it'
add_people: "Radicale has one shared login. Your family member adds the same name and password on their phone and makes their own calendar."
use_cases:
- 'Your phone calendar and contacts on your own server, not with Google or Apple'
- 'A shared family calendar that everyone sees on their phone'
- 'On Android DAVx5, on iPhone and Mac the built-in calendar syncs with it'
- 'Thunderbird and other desktop calendars can use it too'
first_steps:
- 'Open calendar.DOMAIN and sign in with the name and password shown on the settings page'
- 'Create a calendar and an address book ("Create new addressbook or calendar")'
- 'On Android install DAVx5 and enter: https://calendar.DOMAIN, the name and the password'
- 'On iPhone: Settings, Calendar, Accounts, Add Account, Other, CalDAV account - server: calendar.DOMAIN'
deploy_fields:
- env_var: DOMAIN
label: 'Domain'
description: 'The server domain name'
- env_var: SUBDOMAIN
label: 'Subdomain'
description: 'The subdomain this app answers on'
- env_var: RADICALE_USER
label: 'User name'
description: 'You sign in with this name on your phone and in the browser'
- env_var: RADICALE_PASSWORD
label: 'Password'
description: 'Generated at install. Your phone calendar and contacts sign in with it too.'
# update_ladder — the test record: one tested step per line, oldest first (JSON flow mappings,
# `09-update-architecture.md` §6.4 part 4). WRITTEN BY scripts/upgrade-test.py, never by hand;
# gated by scripts/check-test-record.py. An image: move without a proven entry here is refused.
update_ladder:
- {"from": {"radicale": "ghcr.io/kozea/radicale:3.8.0"}, "to": {"radicale": "ghcr.io/kozea/radicale:3.8.1"}, "digest": {"radicale": "sha256:54d9406cd30f9e206a9dd6d61dfac48da26b9afc37200e14f23beb8fafa7d11c"}, "verdict": "proven", "tested_at": "2026-10-01T14:47:56Z", "harness_version": 4, "evidence": "felhom.eu/documentation/audits/new-apps-2026-10-01/bench/radicale/evidence/MV-radicale/verdict.json", "box_evidence": "felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/step.txt", "memory_peak_pct": 20.0, "marks": {"files_may_change": false, "needs_person": null, "memory_tight": false}, "memory_basis": "anon", "memory_cgroup_peak_pct": 23.7}
+64
View File
@@ -0,0 +1,64 @@
# Radicale - Naptár és névjegyek (CalDAV / CardDAV szerver)
# Domain: ${SUBDOMAIN}.${DOMAIN}
# Database: None (file-based — every calendar and address book is a folder of .ics/.vcf files)
# RAM: ~40M (mem_limit: 128M) | Pi-compatible: Yes (amd64, arm64, arm/v7)
#
# Environment variables:
# DOMAIN - Your domain (e.g., demo-felhom.eu)
# RADICALE_USER - A bejelentkezési név (az űrlapon megadható, alapból "csalad")
# RADICALE_PASSWORD - A jelszó (telepítéskor generálva, az alkalmazás oldalán látható)
#
# The upstream image (ghcr.io/kozea/radicale, the Radicale project's own) ships NO config, and Radicale 3's
# default `auth type` is `denyall` — nobody can log in. So the start command writes the login file from the
# box's generated password (bcrypt, 0600, on the data volume) ON THE FIRST START ONLY, then starts Radicale with
# htpasswd auth. There is never a moment without a login (no install window, checklist 3.5). The password is
# read from the environment, never pasted into program code (checklist 3.8).
# FIRST START ONLY, measured 2026-10-01 on 9202: the box never puts a `type: password` value into a backup (an
# internet-reachable login; controller `PortableSecretEnvVars`) and makes a NEW one on a restore after a remove,
# expecting the app's own login to come back WITH ITS DATA. The first version rewrote the file at every start —
# a remove + restore then replaced the household's login with a value no page shows, and every phone lost its
# calendar. Written once, the login file lives on the data volume and comes back with the calendars.
services:
radicale:
image: ghcr.io/kozea/radicale:3.8.1
container_name: radicale
restart: unless-stopped
environment:
- TZ=Europe/Budapest
- RADICALE_USER=${RADICALE_USER}
- RADICALE_PASSWORD=${RADICALE_PASSWORD}
entrypoint:
- /bin/sh
- -c
- |
[ -s /var/lib/radicale/users ] || /app/bin/python -c 'import os, bcrypt; f = "/var/lib/radicale/users"; u = os.environ["RADICALE_USER"]; p = os.environ["RADICALE_PASSWORD"].encode(); open(f, "w").write(u + ":" + bcrypt.hashpw(p, bcrypt.gensalt()).decode() + "\n"); os.chmod(f, 0o600)' || exit 1
exec /app/bin/python /app/bin/radicale --hosts 0.0.0.0:5232 --auth-type htpasswd --auth-htpasswd-filename /var/lib/radicale/users --auth-htpasswd-encryption bcrypt --storage-filesystem-folder /var/lib/radicale/collections
volumes:
- radicale_data:/var/lib/radicale
networks:
- traefik-public
deploy:
resources:
limits:
memory: 128M
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:5232/.web/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
labels:
- "traefik.enable=true"
- "traefik.http.routers.radicale.rule=Host(`${SUBDOMAIN}.${DOMAIN}`)"
- "traefik.http.routers.radicale.entrypoints=websecure"
- "traefik.http.routers.radicale.tls=true"
- "traefik.http.routers.radicale.tls.certresolver=letsencrypt"
- "traefik.http.services.radicale.loadbalancer.server.port=5232"
volumes:
radicale_data:
networks:
traefik-public:
external: true