Files
app-catalog-felhom.eu/templates/grocy/docker-compose.yml
T
admin b7f0f7cef5
gates / gates (push) Failing after 14m4s
Grocy and LubeLogger: two new apps, each with its complete record
60 template directories, 58 offered. Records: onboarding/grocy.md and
onboarding/lubelogger.md, all 61 checks answered, none open. Evidence:
felhom.eu/documentation/audits/new-apps-2026-10-10/.

Grocy 4.7.1 from lscr.io/linuxserver/grocy (grocy publishes no image of its
own). SQLite in one volume, no HDD, ~30 MiB idle, amd64 + arm64, Hungarian UI
91.6 %. First admin class 3: it starts with its documented admin/admin and
after_install replaces that password with a generated one.

LubeLogger v1.7.3 from ghcr.io/hargata/lubelogger. LiteDB in one volume, no
HDD, ~60 MiB idle, amd64 + arm64. First admin class 1 — and it has to be: the
image ships EnableAuth=false, and with that the middleware mints a ticket with
the IsRootUser role for every visitor. Measured on a default start: a stranger
got 200 on /, on /api/vehicles and on /Home/Settings and CREATED A VEHICLE.
The compose entrypoint exports EnableAuth=true and the SHA-256 of a generated
name and password, so the app's own login is on before its first byte (at t+1 s
nothing listening, at t+2 s /api/vehicles already 401).

Two defects found by the walk and fixed before publishing:

1. An after_install command may not contain `$`. The controller runs every
   element through os.Expand and refuses one naming anything outside env:, so
   PHP cannot be inlined. On 9202 the first attempt came back
   `[pw argv dsn db i t e s n q h] not declared in env or has no value — not
   run` and the app sat behind its install hold with admin/admin in place. The
   code now lives in a file the compose entrypoint writes. Written into
   REUSE.md's after_install row as a trap.

2. Grocy's persisted config.php does not follow the image. The image copies
   config-dist.php only when that file is absent, so a volume written by 4.6.0
   and started under 4.7.1 answered HTTP 500 on every page — AUTH_CLASS names a
   class 4.7 moved — while its log said migrations done. The entrypoint now
   deletes the file at every start. The 4.6.0 -> 4.7.1 edge failed before this
   and is proven after it, on both venues.

Ladders, written by upgrade-test.py --write-ladder from both verdicts:
  grocy       4.6.0 -> 4.7.1   bench proven, box proven (guarded Update, 41 s)
  lubelogger  v1.7.2 -> v1.7.3 bench proven, box proven (25.6 s)
Checklist 6.3 came from a real failure, not a forced one: before the config.php
fix the product undid the same step in 346 s with the data intact.

REUSE.md: a fifth healthcheck family (bash /dev/tcp) for an image with no HTTP
client at all, measured in both directions.

Tool fixes made on the way: check-onboarding.py crashed on a Windows console
while printing which ids were open; upgrade-test.py's read_text/write_text used
the platform encoding and wrote a cp1250 em dash into a template full of
Hungarian.
2026-10-10 12:31:34 +02:00

162 lines
9.0 KiB
YAML

# Grocy - Háztartásvezetés: készlet, bevásárlólista, házimunkák
# Domain: ${SUBDOMAIN}.${DOMAIN}
# Database: None (SQLite inside the app's own config volume — /config/data/grocy.db)
# RAM: ~30M (mem_limit: 384M) | Pi-compatible: Yes (amd64, arm64)
#
# Environment variables:
# DOMAIN - Your domain (e.g., demo-felhom.eu)
# ADMIN_PASSWORD - Generated at install; after_install replaces grocy's admin/admin with it
#
# Storage layout:
# Everything → grocy_config:/config (named volume, NVMe)
# /config/data/grocy.db the whole household's stock, recipes, chores
# /config/data/config.php settings (copied from config-dist.php on the first start)
# /config/data/storage product and recipe pictures the household uploads
# /config/felhom/ the one helper script the entrypoint writes (see below)
# /config/nginx /config/log /config/php /config/keys /config/www the image's own files
# No HDD mount: grocy keeps no bulk data. Measured 2026-10-10 — a fresh install is ~600 KB of
# database and nothing else, so the tier-1 backup unit (database + volumes) holds all of it.
#
# WHY linuxserver.io and not an image from grocy itself: grocy publishes NO image. `grocy/grocy` on
# Docker Hub is 404, and grocy's own README answers "How to run using Docker" with one link, to
# hub.docker.com/r/linuxserver/grocy. Four other catalog apps already come from this publisher.
# TRAP (checklist 6.5): linuxserver REBUILDS WEEKLY and re-pushes the plain version tag, so
# `4.7.1` changed digest from ls342 (2026-09-27) to ls343 (2026-10-04) with the same app inside.
# That is the same-tag re-push class — scripts/retest-floating.py covers it monthly.
#
# Run-identity: PUID/PGID 1000. nginx and php-fpm run their workers as `abc` (= 1000), measured.
#
# First-time setup:
# Default login: admin / admin — the box REPLACES that password at install (after_install in
# .felhom.yml) and shows the generated one on the app page. Measured on the bench 2026-10-10:
# default refused, generated signs in, a wrong one refused.
#
# WHY THE ENTRYPOINT WRITES A SCRIPT, and it is not the gokapi pattern (REUSE.md §3).
# The replacement has to run grocy's own hashing (`password_hash($password, PASSWORD_ARGON2ID)`,
# services/UsersService.php), and PHP is the only interpreter in this image — measured: no python, no
# perl, no ruby, no node. But an `after_install` command may not contain PHP code, because the
# controller EXPANDS every `$name` in it (`internal/stacks/after_install.go` `expandAfterInstall`,
# Go's `os.Expand`) and refuses the command naming anything it was not handed. Measured on 9202
# 2026-10-10, the first attempt at this template:
# after_install: [pw argv dsn db i t e s n q h] not declared in env or has no value — not run
# and the app stayed behind its install hold with the default password still in place. So the code
# lives in a FILE that the entrypoint writes, and `after_install` passes only the password.
# Unlike gokapi's entrypoint this seeds nothing of the app's own: the file is ours, outside grocy's
# tree, carries no secret and no version-pinned format, so an image update cannot collide with it.
# It is rewritten at every start, which is deliberate — a template fix reaches an installed app on
# its next restart instead of only on a fresh install.
# Every `$` in that block is written `$$`: docker compose interpolates the compose file before bash
# ever sees it. Verified by reading the file back inside the container (single `$`, no `$$` left).
#
# WHY THE ENTRYPOINT DELETES /config/data/config.php, and why that is not data loss.
# The image copies its own `config-dist.php` to `/config/data/config.php` ONLY IF THAT FILE IS ABSENT
# (`init-grocy-config`), so the file is written once on the first install and then NEVER FOLLOWS THE
# IMAGE AGAIN. Measured on the bench 2026-10-10: a volume written by 4.6.0, started under 4.7.1,
# answered **HTTP 500 on every page** —
# Invalid setting in config.php: Configured AUTH_CLASS "Grocy\Middleware\DefaultAuthMiddleware" does not exist
# because 4.7 moved that class to `Grocy\Middleware\Auth\DefaultAuthMiddleware`. The app had started,
# its migrations had run and its log said `[ls.io-init] done` — and it served nothing. That is the whole
# 4.6.0 → 4.7.1 upgrade, and it failed on the harness before this line existed.
# Removing the file makes it a DERIVED file instead of state: the image re-copies its own current
# defaults at every start, so config.php can never name a class the running code does not have. Nothing
# is lost — every instance setting grocy has can be set by a `GROCY_<NAME>` environment variable (its
# own config-dist.php documents that as a higher priority than the file), and the four a Hungarian
# household cares about are set above; each person's own choices (night mode, the start page) live in
# grocy's DATABASE, not in this file. Fixing the class name with `GROCY_AUTH_CLASS` instead was also
# measured and also serves — but it hard-codes an upstream class path that the next rename would break,
# and it would leave every other stale setting stale.
#
# Outbound traffic: none at start and none in the background. The only host the code can reach is
# world.openfoodfacts.org, and only when the household looks an unknown barcode up on purpose
# ("External barcode lookup"). Camera barcode SCANNING is ZXing in the browser and sends nothing.
# Turn the lookup off with GROCY_STOCK_BARCODE_LOOKUP_PLUGIN= (empty) if a household wants that.
services:
grocy:
image: lscr.io/linuxserver/grocy:4.7.1
container_name: grocy
restart: unless-stopped
environment:
- TZ=Europe/Budapest
- PUID=1000
- PGID=1000
# grocy picks the browser's locale first; this is the fallback. GROCY_* env beats
# /config/data/config.php (second priority vs third), measured 2026-10-10.
- GROCY_DEFAULT_LOCALE=hu
- GROCY_CURRENCY=HUF
- GROCY_ENERGY_UNIT=kcal
- GROCY_CALENDAR_FIRST_DAY_OF_WEEK=1
entrypoint:
- /bin/bash
- -c
- |
set -e
rm -f /config/data/config.php
mkdir -p /config/felhom
cat > /config/felhom/set-admin-password.php <<'FELHOM_PHP'
<?php
// Written by the template's entrypoint at every start. Holds NO secret: the password arrives
// as argv[1]. Replaces grocy's documented default admin/admin with the box's generated one.
$$pw = isset($$argv[1]) ? $$argv[1] : '';
if ($$pw === '') { fwrite(STDERR, "no password given\n"); exit(1); }
$$dsn = 'sqlite:/config/data/grocy.db';
$$db = null;
// grocy creates its SCHEMA on the first HTTP request, not at container start (measured on
// 4.6.0: /login answered 200 with a zero-byte database; / ran the migrations). So ask the app
// first, and keep asking — a command that needs an outside retry to work is unreadable.
for ($$i = 0; $$i < 30; $$i++) {
@file_get_contents('http://127.0.0.1/');
try {
$$t = new PDO($$dsn);
$$t->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$$t->query('SELECT 1 FROM users LIMIT 1');
$$db = $$t;
break;
} catch (Exception $$e) { sleep(1); }
}
if (!$$db) { fwrite(STDERR, "grocy schema never appeared\n"); exit(1); }
$$s = $$db->prepare('UPDATE users SET password = ? WHERE username = ?');
$$s->execute(array(password_hash($$pw, PASSWORD_ARGON2ID), 'admin'));
$$n = $$s->rowCount();
// PROVE it before saying so: a plain UPDATE can match no row and still exit 0, and then the
// app page would show a password that does not work.
$$q = $$db->prepare('SELECT password FROM users WHERE username = ?');
$$q->execute(array('admin'));
$$h = $$q->fetchColumn();
if ($$n === 1 && $$h && password_verify($$pw, $$h)) { echo "FELHOM_AFTER_INSTALL_OK\n"; }
else { fwrite(STDERR, "grocy admin password NOT set, rows=" . $$n . "\n"); exit(1); }
FELHOM_PHP
chmod 0644 /config/felhom/set-admin-password.php
exec /init
volumes:
- grocy_config:/config
networks:
- traefik-public
deploy:
resources:
limits:
memory: 384M
# BusyBox wget is the only HTTP client in this image (no curl). `/` answers 302, so the probe
# dials /login, which answers 200. Measured both ways 2026-10-10: rc=0 against the app,
# rc=1 against a dead port.
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:80/login"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
labels:
- "traefik.enable=true"
- "traefik.http.routers.grocy.rule=Host(`${SUBDOMAIN}.${DOMAIN}`)"
- "traefik.http.routers.grocy.entrypoints=websecure"
- "traefik.http.routers.grocy.tls=true"
- "traefik.http.routers.grocy.tls.certresolver=letsencrypt"
- "traefik.http.services.grocy.loadbalancer.server.port=80"
volumes:
grocy_config:
networks:
traefik-public:
external: true