Releases: Partha-dev01/pocket-homeserver
Release list
v1.0.0 — first stable release
pocket-homeserver v1.0.0 — the first stable release.
A complete, opt-in personal cloud on a single unrooted Android phone: Matrix chat, a Cloudflare-tunnelled Caddy edge, and ~30 optional services — files & sync, productivity, calendar & passwords, media, a git forge, DNS-over-HTTPS, a mesh VPN, and more. Every web service is loopback-bound behind the tunnel, every embedded database lives on ext4, every module is OFF by default, and every pinned artifact is sha256-verified fail-closed.
The full feature history (v0.4.0 → v0.9.1) is in the changelog. This release closes the pre-1.0 audit's remaining coverage gaps. From here, breaking changes follow SemVer.
What landed for 1.0
- Universal loopback backstop. Beyond each service's config/env loopback assert, a post-start
sswildcard check now refuses to leave any service listening on a non-loopback address — extended from Forgejo + AdGuard to every Go/Node/Rust web listener (Navidrome, Vikunja, Kavita, Trilium, Audiobookshelf, Pingvin, Gatus, the Syncthing GUI, Vaultwarden, Dufs). It's a shared, port-scoped helper inscripts/lib/common.sh. This closes the raw-SYS_BINDclass — the reason Photoview was dropped — for the whole stack. Validated on arm64 against a real Go binary (loopback passes; a forced wildcard is detected, stopped, and its port freed). - Every pin re-verified. All 16 sha256-pinned artifacts were re-checked against current upstream bytes — every one matches.
- Honest dependency accounting. Pingvin builds from
smp46/pingvin-share-xbecause canonical upstream is archived and that fork is the maintained successor; audited at the pinned tag (no npm lifecycle hooks, stock dependencies, loopback patch intact).
Notes
- Two upstream-imposed residuals remain documented (injection-safe, Android-mitigated): FreshRSS
create-user.phpand the maddy per-user-IMAP password are--password-only on the command line. - All optional modules ship OFF; enable what you want via
setup.sh/ the in-panel app catalog.
🐧 See docs/SETUP.md for the zero-to-running walkthrough.
v0.9.1 — pre-1.0 hardening
Pre-1.0 hardening — fixes from a multi-agent security + correctness audit of the whole tree. All changes are backward-compatible; the SQLite relocation auto-migrates (with a backup).
Security
- No cleartext-secret leak path on the public repo:
.gitignorenow ignores.env.bak*/.env.tmp*, andtools/leak-scan.shgained a JWT-shaped backstop. - The MCP HTTP transport binds loopback only (
127.0.0.1) with a fail-closed assert. - Admin-panel log redaction now scrubs S3/R2/SMTP credentials (from the 0600
secrets/*.envfiles) and is applied to the/action+/confirmoutput. The SnappyMail admin password is hashed off-argv (via stdin). - Kavita + Audiobookshelf: the optional Matrix-SSO
forward_authblock moved inside the catch-allhandle {}so it can never be hoisted ahead of the OPDS / token-API exemption (caddy-validated). - Syncthing GUI and Vikunja API listeners gained fail-closed loopback asserts.
- Every ext4-vs-exFAT storage guard resolves the full real path (a symlinked leaf can no longer smuggle a SQLite DB onto the exFAT SD).
Changed
- SQLite databases for Linkding, Memos, Vikunja, and FreshRSS moved to ext4 (
$HOME/.pocket/<app>) — exFAT cannot do POSIX locks / atomic rename / durable fsync, which corrupts SQLite. An existing data dir on the SD is auto-migrated once (backed up first; the original is left in place to remove after verifying). Validated end-to-end on arm64 (WAL data intact). exobotpinsgradioinstead of--upgrade; the metrics sampler defaults OFF insetup.sh.
Fixed
- Admin panel: Dufs / FileBrowser / Syncthing now appear in the health + restart wiring, the Tailscale restart button resolves, and the restart-button row lists the v0.6–v0.9 apps.
See CHANGELOG.md for detail.
v0.9.0 — platform leverage & networking
Platform leverage & networking. A git forge, a DNS-over-HTTPS resolver, a bring-your-own reverse-proxy, a userspace mesh VPN, an in-panel app catalog, and an optional fail2ban-style rate-jail on the honeypot — all opt-in (ENABLE_* / RATE_JAIL_MODE, off by default), loopback-bound where they front a service, keeping any database/index/state on ext4.
Added
- Forgejo (
ENABLE_FORGEJO,git.${DOMAIN}) — single-binary git forge; sha256-pinned arm64 binary;HTTP_ADDR=127.0.0.1+ config assert + post-startsswildcard check; runs as an unprivileged user; SSH/registration/Actions off,INSTALL_LOCK; SQLite WAL + repos on ext4; first admin + secrets generated off-argv. git-HTTP/API/LFS need a CF Access service-token exemption. (docs/FORGEJO.md) - AdGuard Home (
ENABLE_ADGUARD,dns.${DOMAIN}) — filtering DoH resolver; UI + plain-HTTP DoH (/dns-query) on127.0.0.1:9129, resolver on9130; config assert + a post-startssaudit scoped to its own ports. Not a LAN:53sinkhole;/dns-queryneeds a CF Access path bypass. (docs/ADGUARD.md) - BYO reverse-proxy (
ENABLE_PROXY_ROUTES,PROXY_ROUTES) — publish any loopback service on its own subdomain; fail-closed loopback-target gate, injection-guard regex, collision check, authoritative stale-route sweep, fail-closedcaddy validate. (docs/PROXY_ROUTES.md) - Tailscale (
ENABLE_TAILSCALE) — userspace mesh VPN (no TUN/root) that sidesteps CGNAT; SOCKS5 + HTTP proxy on127.0.0.1:1055; auth key off-argv;GOMEMLIMITcap.⚠️ The tailnet bypasses the Cloudflare edge — the tailnet ACL is the only network gate. (docs/TAILSCALE.md) - App catalog / module manager (
ENABLE_APP_CATALOG) — enable + install a module from the admin panel; fixed in-code allow-list (request value never reaches argv), password re-auth + CSRF,ENABLE_*-only atomic0600.envwriter, detached installs, secret redaction at the single/logschokepoint. (docs/ADMIN.md) - Honeypot rate-jail (
RATE_JAIL_MODE, defaultoff) — fail2ban-style auth-failure-burst detector;enforcereuses the existing triple-gatedcf_block(degrades safely to alert-only without the blocking opt-in). (docs/HONEYPOT.md)
Validation
Verified against the real pinned arm64 binaries under qemu-aarch64: Forgejo boot + /api/healthz + admin-create flags; AdGuard boot + wizard-skip + plain-HTTP /dns-query + scoped loopback audit; Tailscale rootless userspace bring-up + up-flag validation; and caddy validate of all new vhosts. Live tailnet join, CF Access exemptions, and on-device first-run flows remain operator-owed.
Pre-release. Interfaces may still change before 1.0.
v0.8.0 — media tier (Navidrome / Kavita / Audiobookshelf)
Media tier
Three optional, self-hosted media servers — music, comics/ebooks, and audiobooks — all opt-in (ENABLE_*, off by default), loopback-bound, keeping their database/index/cache on ext4 ($HOME/.pocket/<app>) while the bulk library may live on the exFAT SD. Direct-play by default (no on-the-fly transcoding — a phone has no usable hardware transcode path; software transcode is the thermal/LMK heavy path and stays opt-in). Subsonic / OPDS / mobile API paths are reverse-proxied ahead of the optional auth gate and get a Cloudflare Access path exemption (or service token).
Added
- Navidrome (
ENABLE_NAVIDROME) →music.${DOMAIN}— a music-streaming server with its own web player and a Subsonic-compatible API. A single static Go binary (sha256-pinned). ForcesND_ADDRESS=127.0.0.1(Navidrome defaults to0.0.0.0) and asserts it. Subsonic/rest/*+/share/*are exempted from the gate. - Kavita (
ENABLE_KAVITA) →books.${DOMAIN}— a manga/comic/ebook server with an OPDS feed. A self-contained .NET arm64 build (sha256-pinned, needs systemlibicu72). Pre-seedsappsettings.jsonwithIpAddresses=127.0.0.1before first start (defaults to0.0.0.0,::) and asserts it; the JWTTokenKeyis generated off-argv. OPDS (/api/opds/*) is exempted. - Audiobookshelf (
ENABLE_AUDIOBOOKSHELF) →audiobooks.${DOMAIN}— an audiobook/podcast server with progress sync. Built from source from a pinned git tag (no arm64 release binary; first build is 15–40+ min, like Pingvin). ForcesHOST=127.0.0.1and asserts it; pins nativeffmpeg+ the nunicode SQLite extension (SKIP_BINARIES_CHECK=1). The mobile-app API paths are exempted. - docs/MEDIA.md — the three media apps with a per-app Resource & Risk section, the storage-tier and direct-play rules, the per-app Cloudflare Access exemptions, a photo-gallery roadmap note, and an honest "why Jellyfin is docs-only" note.
Roadmap (not in this release)
A photo gallery was scoped for this tier (candidate: Photoview) but is deferred to the roadmap. Its Go server hardcodes a 0.0.0.0 bind, and no userland mechanism available on this stack (proot on unrooted Android) can safely force it to loopback — Go issues bind() as a raw syscall that an LD_PRELOAD shim cannot intercept (verified: it listens on *:port with and without the shim), and ptrace / user-namespace / seccomp-notify rewriting is unavailable or unvalidatable here. Loopback-only binding is a non-negotiable security invariant in this stack, so a gallery ships only once a real loopback path exists. See docs/MEDIA.md.
Pre-release, part of the staged path to 1.0. All new apps are off by default. See the CHANGELOG.
v0.7.0 — productivity & security apps
Productivity & security apps. Four optional, ENABLE_*-gated apps (off by default), loopback-bound, each keeping its DB/index on ext4 ($HOME/.pocket/<app>), never on the exFAT SD. Clients that speak native/token auth (Bitwarden apps, CalDAV/CardDAV, the Wallabag API, Trilium's ETAPI/sync) use a Cloudflare Access service-token exemption, not the interactive login gate.
- Vaultwarden (
vault.) — Bitwarden-compatible password manager. Upstream ships no standalone binary, so the installer daemonlessly extracts the musl-static binary + version-locked web-vault from the official alpine image pinned by its arm64 manifest digest (each layer sha256-verified), then re-verifies the extracted binary against a self-derived sha256.ROCKET_ADDRESS=127.0.0.1+SIGNUPS_ALLOWED=falseasserted;ADMIN_TOKENunset;ENABLE_DB_WAL=true. See docs/VAULT.md. - Radicale (
dav.) — CalDAV/CardDAV/tasks. Python venv on ext4; bcrypt from a prebuilt aarch64 wheel only (fail-closed, never compiles);hostsforced to loopback + asserted; bcrypt htpasswd seeded off-argv; root-mounted vhost so.well-knowndiscovery works; collection root forced to ext4. The admin panel gains a/davQR connect-card (the QR carries only the URL + username, never the password). See docs/DAV.md. - Trilium (
wiki.) — notes/wiki, from the official first-party arm64 server tarball (bundled Node + prebuilt better-sqlite3, no node-gyp).TRILIUM_NETWORK_HOST=127.0.0.1forced + asserted (default is0.0.0.0); a fail-closed GLIBCXX boot-smoke;document.dbon ext4. See docs/NOTES.md. - Wallabag (
read.) — read-later, from the official bundled tarball (vendor/pre-installed, no composer), reusing php-fpm; SQLite on ext4; admin password fed on stdin (off-argv); upgrades back up the DB before migrations + clear/warm the prod cache. See docs/READLATER.md.
Each new doc carries a prominent Resource & Risk section.
Validated on real arm64 (qemu-aarch64 containers, real pinned artifacts): Trilium GLIBCXX boot + loopback bind, Wallabag fos:user stdin admin-seed + wallabag:install + cache:clear, Radicale bcrypt-wheel install + auth + loopback bind, Vaultwarden extract (binary sha256 exact-match) + run, and caddy validate of all four vhosts. CI gates green (leak-scan, shellcheck, py_compile, install --check).
Pre-release: interfaces may still change before 1.0.
Full changelog: CHANGELOG.md
v0.6.0 — personal cloud: files & sync
Personal cloud — files & sync. Serve your own files from the phone and sync them
peer-to-peer. Every module is opt-in (ENABLE_*, off by default), loopback-bound,
and keeps its secrets in 0600 files (or Syncthing's own config), never in .env.
Added
- Dufs (
ENABLE_DUFS) — a tiny stateless Rust file server (browser UI +
WebDAV) onfiles.${DOMAIN}(scripts/apps/dufs.sh). Read-only by default.
It pins the binary by sha256, forces the listener to127.0.0.1(dufs defaults
to0.0.0.0) and asserts the loopback bind fail-closed after rendering its
config, generates a per-deploy HTTP Basic credential (the$6$hash goes in the
0600 config; cleartext only in${DATA_DIR}/secrets/dufs.env, never on argv). - FileBrowser (
ENABLE_FILEBROWSER) — the classic v2 web file manager
(multi-user accounts + share links, no WebDAV) onfiles.${DOMAIN}
(scripts/apps/filebrowser.sh). Its BoltDB is pinned to ext4 (never the
exFAT SD), and the admin is seeded deterministically from.env
ADMIN_USER/ADMIN_PASSWORDoff-argv (a pre-hashed bcrypt import) — no
print-a-random-password-once lockout trap. - Mutually exclusive on
files.${DOMAIN}— Dufs and FileBrowser share the
hostname, so enabling both dies fail-closed;./setup.shkeeps Dufs and
disables the other if you pick both. - Syncthing (
ENABLE_SYNCTHING) — peer-to-peer folder sync
(scripts/steps/89-install-syncthing.sh). It sidesteps the Cloudflare tunnel
entirely (so the ~100 MB body cap is irrelevant — the large-data path); its web
GUI stays loopback-only (no public vhost; reach it via
ssh -L 8384:127.0.0.1:8384). TheHOME(config + cert + SQLite index DB)
is forced to ext4 with a fail-closed assert against an SD path, and a random GUI
password is set off-argv (syncthing generatereads it from stdin, never on the
command line). docs/FILES.md— the files & sync guide, including the mandated why-not-
Nextcloud / why-no-SMB rationale, the Dufs-vs-FileBrowser chooser, the
Cloudflare Tunnel ~100 MB upload cap + workarounds, the WebDAV service-token
recipe, the ext4-vs-exFAT storage split, the Quantum-fork note, and a Resource &
Risk section. Cross-linked fromdocs/SECURITY.md(the edge body cap) and
docs/APP_AUTH.md(non-browser clients need a service token).- Version pins for all three in
config/versions.env(DUFS_*,FILEBROWSER_*,
SYNCTHING_*), each sha256-verified fail-closed.
Fixed
config/versions.envnow actually ships. The central version/checksum
manifest (added in 0.4.0) was caught by the*.envline in.gitignoreand was
never committed, so a fresh clone had no manifest forcommon.sh,ops/update.sh,
ops/doctor.sh, anddocs/UPDATING.mdto operate on (installs still worked via
each step's inline${VAR:-default}fallback). It is now un-ignored and tracked —
public version pins + sha256s only, no secrets.
v0.5.0 — reliability & ops
Reliability & ops — see your phone's health over time, get told when something breaks, get backups off the device, and manage Matrix users from the panel. Every piece is opt-in (ENABLE_*, off by default) and adds no inbound surface.
Added
- Observability (
ENABLE_METRICS) — a supervised metrics sampler records CPU/mem/swap/load/disk/temp/battery + the DEGRADED count once a minute into a capped JSONL ring on ext4. The admin panel gains a /metrics page (sparklines + a 24h health strip), a DEGRADED-aware /problems view + dashboard banner, a run doctor button, and a filter + line-count on the log viewer. Seedocs/OBSERVABILITY.md. - Crash-loop alerts —
setup.shwiresPOCKET_ALERT_CMD(none / ntfy / healthchecks / Matrix). The Matrix channel shipsops/alert-matrix.sh, reading its token from a 0600 file. - Off-device encrypted backup (
ENABLE_OFFSITE_BACKUP) — push the age-encrypted archives to any S3-compatible bucket (R2 / B2 / S3 / Wasabi / MinIO) via a dependency-free SigV4 client (ops/offsite-s3.py+offsite-push.sh); no rclone/aws/boto3. Refuses plaintext, mirrors retention, wired into the backup daemon, panel, andpocket.sh. Seedocs/BACKUPS.md. - Matrix user management (
ENABLE_USER_ADMIN) — a panel /users page +ops/user-*.sh(list/create/reset-password/suspend/unsuspend/deactivate/invite) driven through continuwuity's admin command room (lib/matrix_admin.py). Each write op needs CSRF + password re-auth + audit; deactivation needs a typed confirm. Seedocs/USERS.md.
Fixed
- The admin panel launcher now exports all the
ENABLE_*flags (andMCP_TRANSPORT) the panel reads — previously the cloud-bots / exobot / stickers / adminbot / email / mcp / filter health rows and the admin-bot widget never appeared even when enabled.
Operator-owed verification: the Matrix user-management and offsite-push paths talk to the live homeserver / object store and are best exercised on-device.
Full notes: CHANGELOG.md
v0.4.0 — platform foundation
First milestone toward 1.0: the platform/reliability foundation that the new modules will build on.
Added
config/versions.env— one central manifest for every pinned version + sha256 (sourced after.env; your.envstill overrides).scripts/ops/update.sh— dry-run-by-default updates that back up the manifest, snapshot the Matrix DB, verify the new artifact fail-closed, restart, health-check, and roll back automatically on a crash loop. Tier-aware (binary/source/app/static/schema). Seedocs/UPDATING.md.scripts/ops/doctor.sh— read-only preflight/self-test (config, exFAT-vs-ext4 storage tiers, proot userland, Termux addons, duplicate ports, loopback reachability, DEGRADED markers); never prints secrets.- CI (
.github/workflows/ci.yml): ShellCheck + py_compile + a blocking leak-scan gate +install --checksmoke. SECURITY.md, issue/PR templates, and a versioning/release policy../pocket.shgains Update and Doctor menu items.
Fixed
install.sh --checknow exits 0 on success.
Full notes in CHANGELOG.md.
v0.3.3
Added
- Crash-loop resilience for every supervised service.
supervise()in
scripts/lib/common.shnow respawns with exponential backoff
(POCKET_RESPAWN_MIN..POCKET_RESPAWN_MAX, default 5s..300s) instead of a
fixed 5s, treats a child that stays up>= POCKET_HEALTHY_SECS(default 60s) as
healthy (resets backoff), and afterPOCKET_CRASHLOOP_FAILS(default 5) rapid
failures raises a machine-readable DEGRADED marker and fires an optional
one-shot alert. A corrupt-DB crash loop can no longer silently hammer storage
for hours unnoticed. - Crash-loop alerting hook
POCKET_ALERT_CMD(optional, off by default): a
shell command run once when any service goes DEGRADED, with
$POCKET_ALERT_SERVICE/$POCKET_ALERT_RC/$POCKET_ALERT_FAILSin the
environment (never on argv). Wire it to healthchecks.io, ntfy, Matrix, etc. - DEGRADED visibility in the admin panel +
/health. Crash-looping services
show an amber pulsing dot and a "crash-looping" badge instead of flapping green;
the Matrix row adds a "DB may be corrupt; runscripts/ops/restore.sh" hint.
The marker auto-clears on a healthy run or a manual restart. - Configurable Matrix-DB backup cadence
BACKUP_DB_CADENCE
(daily|weekly|monthly), now defaulting to daily so an unclean-kill DB
corruption costs at most ~1 day of data (the DB tar is small; the heavy rootfs
stays monthly). docs/RESILIENCE.md— the failure modes (unclean-kill RocksDB corruption,
silent crash loops), what the stack does automatically, alerting setup, and
recovery viaops/restore.sh. Plus an OFF-by-default, documented
rocksdb_recovery_modeblock inconfig/conduwuit.toml.tmpl.
v0.3.2 — audit fixes
Fixed
- First-user creation now actually works. The setup wizard wrote a
MATRIX_REGISTRATION_TOKENto.envthat nothing consumed (registration is
baked closed in the homeserver config), anddocs/SETUP.mdtold you to register
with that dead token. Removed the inertMATRIX_ALLOW_REGISTRATION/
MATRIX_REGISTRATION_TOKEN/MATRIX_ALLOW_FEDERATIONvars and rewrote the
first-user flow aroundscripts/ops/rotate-registration-token.sh(which opens
token-gated signup and prints a working token) in the wizard and SETUP.md. - Landing portal install no longer aborts.
84-install-landing.shonly
substituted__LANDING_ROOT__, leaving literal${DOMAIN}/${CADDY_PORT}/
${CADDY_BIND}in the rendered vhost (the heredoc can't re-expand them) →
caddy validatefailed. The renderer now substitutes all of them, and the
auth-gateway port is templated (__AUTHGW_PORT__) instead of hardcoded. - Operator admin bot regains its env-dependent commands. Its launcher sourced
only the secrets file, soDATA_DIR/POCKET_LOG_DIR/MATRIX_SERVER_NAMEwere
empty and!invite-token/!private-listplus the audit log silently failed.
The launcher now exports them (matching the exobot launcher). - Honeypot SQLite no longer lands on the exFAT SD card (where its own code
warns WAL/locking misbehaves). The watcher now pointsHP_DBat an internal
ext4 path under$HOME/.pocket(overridable viaPOCKET_HONEYPOT_DB). - Email install no longer 404s on the Maddy download — the arch string is now
aarch64(upstream's name for arm64), notarm64. - Setup wizard completeness: it now prompts for the honeypot and the
scheduled-backup daemon (previously enableable only by hand-editing.env),
warns that SearXNG / IT-Tools / Gatus have no built-in login and must sit behind
Cloudflare Access, and writes the fullMCP_ALLOWED_LOGSlist (fixes a drift
introduced in 0.3.1). - No-auth backends are pinned to loopback. FreshRSS and SnappyMail php-fpm
pools (and their Caddy upstreams/probes) now bind127.0.0.1explicitly instead
of followingCADDY_BIND, so they cannot be exposed on the LAN if a user sets
CADDY_BIND=0.0.0.0. - exobot UI is no longer force-supervised on every bring-up (it is managed
on-demand by the waker), restoring its lazy-start / idle-stop behaviour.
Changed
- Docs:
docs/SETUP.mdnow walks through creating one Cloudflare Tunnel
public hostname per exposed service and protecting them with Cloudflare Access,
and includes the literalpkg install git/git clonefirst steps;
docs/SECURITY.mdreflects the shipped (optional) honeypot and email backend;
docs/ARCHITECTURE.mdcorrects the Matrix hostname tochat.${DOMAIN}; the
README docs index andscripts/README.mdare refreshed. Added
ADMINWEB_SECURE_COOKIEto.env.example.