b7f0f7cef5
gates / gates (push) Failing after 14m4s
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.
162 lines
9.0 KiB
YAML
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
|