From 886c9b010b11f11e08b2a9f5265bf315faa2efdc Mon Sep 17 00:00:00 2001 From: Spendlik Date: Tue, 11 Aug 2026 11:14:23 +0000 Subject: [PATCH] Document image-upload defect (hardcoded /tmp vs NFS /uploads rename() failure) and entrypoint-symlink fix; bake fix into canonical Phase 5 compose config; mark Phase 10 done, note Phase 11 smoke test --- 114_koillection_deployment.md | 75 ++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 10 deletions(-) diff --git a/114_koillection_deployment.md b/114_koillection_deployment.md index be7ef4b..9d32cc8 100644 --- a/114_koillection_deployment.md +++ b/114_koillection_deployment.md @@ -1,6 +1,6 @@ # 114 — Koillection Deployment Guide -> Status: **IN PROGRESS** — CT created 2026-08-11, app + nginx + SSL + DNS live, Authelia bypassed by decision, first login pending (Phase 10+) +> Status: **IN PROGRESS** — CT created 2026-08-11, app + nginx + SSL + DNS live, Authelia bypassed by decision, admin login done, image upload defect found + fixed. Remaining: create the 5 real collections (Phase 11). > CT ID: 114 · IP: 192.168.1.114 > Domain: `collections.spendlik.sk` > Last updated: 2026-08-11 @@ -55,6 +55,8 @@ mkdir -p /volume1/proxmox/data/koillection/uploads > ⚠️ The NAS path follows the same convention as Paperless (`/volume1/proxmox/data/`). Be consistent. > > **Host-side mount**: verified 2026-08-11 — the Proxmox storage ID is `spendlik-nas`, mounted at `/mnt/pve/spendlik-nas` on the host (confirmed via live `ls /mnt/pve/`). CT 111 (Paperless) uses `/mnt/pve/spendlik-nas/data/paperless` as its exact host-side bind-mount path — Koillection follows the identical pattern in Phase 4 below. +> +> ⚠️ **Also create a `.phptmp` subfolder under `/uploads` at the same time** — required by the image-upload fix documented after Phase 5. Doing this now, during initial NAS prep, avoids a second round-trip. --- @@ -140,9 +142,16 @@ ls -la /uploads Confirmed: `/uploads` mounted, `777 nobody:nogroup`, empty — ready for the app to write to. +**Also create the PHP temp-file directory here** (needed for the fix in Phase 5 below — for a fresh deployment following this guide top-to-bottom, do this now rather than discovering it's missing later): + +```bash +mkdir -p /uploads/.phptmp +chmod 1777 /uploads/.phptmp +``` + --- -## Phase 5 — Deploy Koillection ✅ DONE (2026-08-11) +## Phase 5 — Deploy Koillection ✅ DONE (2026-08-11, includes image-upload fix) ```bash mkdir -p /opt/koillection @@ -169,11 +178,20 @@ HTTPS_ENABLED=0 > Generate `APP_SECRET` with: `openssl rand -hex 16` +Create a PHP ini override for the temp directory (belt-and-suspenders alongside the entrypoint fix below — some code paths do respect this, even though the specific bug fixed here does not): + +```bash +cat > /opt/koillection/upload-tmp.ini << 'EOF' +upload_tmp_dir = /uploads/.phptmp +sys_temp_dir = /uploads/.phptmp +EOF +``` + ```bash nano docker-compose.yml ``` -Paste: +Paste **this exact version, which includes the image-upload fix baked in from the start** (see "Known Issue" section below for why): ```yaml services: @@ -181,12 +199,14 @@ services: image: koillection/koillection:latest container_name: koillection restart: unless-stopped + entrypoint: ["sh", "-c", "rm -rf /tmp && ln -s /uploads/.phptmp /tmp && exec sh /app/public/docker/entrypoint.sh"] ports: - "8080:80" env_file: - .env volumes: - /uploads:/uploads + - ./upload-tmp.ini:/usr/local/etc/php/conf.d/zz-upload-tmp.ini:ro depends_on: db: condition: service_healthy @@ -217,7 +237,7 @@ docker compose up -d docker compose logs -f ``` -Wait until the koillection container logs settle (Symfony/FrankenPHP startup — you'll see deprecation.INFO notices about API Platform `#[ApiResource]` shortName deduplication; these are harmless upstream framework warnings on this image version, not errors). Then verify with a proper status check, **not** a text grep against the homepage (the homepage is just a redirect, so grepping for "koillection" in it will always come back empty and looks like a false failure): +Wait until the koillection container logs settle (Symfony/FrankenPHP startup — you'll see deprecation.INFO notices about API Platform `#[ApiResource]` shortName deduplication and symfony/form; these are harmless upstream framework warnings on this image version, not errors). Then verify with a proper status check, **not** a text grep against the homepage (the homepage is just a redirect, so grepping for "koillection" in it will always come back empty and looks like a false failure): ```bash docker compose ps @@ -226,7 +246,38 @@ curl -sv http://localhost:8080 Expect both containers `healthy`, and the curl to show `HTTP/1.1 302 Found` with `Location: /first-connection` — that's Koillection's normal first-run redirect, confirming the app is up and reachable. -> ℹ️ `chown: Invalid argument` lines for `/uploads` in the startup logs are expected and harmless — same NAS-bind-mount ownership limitation already known from Paperless. Doesn't affect functionality. +Also verify the entrypoint fix actually took effect: + +```bash +docker compose exec koillection ls -la / | grep tmp +``` + +Should show `tmp -> /uploads/.phptmp` as a symlink. + +> ℹ️ `chown: Invalid argument` lines for `/uploads` (and now `/uploads/.phptmp`) in the startup logs are expected and harmless — same NAS-bind-mount ownership limitation already known from Paperless. Doesn't affect functionality. + +--- + +## 🐛 Known Issue: Image/Photo Uploads Fail with "rename(): Invalid argument" + +**Symptom**: Uploading any image (profile picture, collection photo, item photo) fails with a generic "critical error" in the UI. Everything else — creating collections, text fields, login — works fine. + +**Root cause**: Koillection's image-upload handler (used for profile pictures and collection/item photos) writes its temp file to a **hardcoded `/tmp` path**, then calls PHP's `rename()` to move it into `/uploads`. `/tmp` lives on the container's local overlay filesystem, while `/uploads` is NFS-mounted from the NAS — two different filesystems. Linux's `rename()` syscall cannot move a file across filesystem boundaries (this is `EXDEV` normally, but surfaces here as a generic "Invalid argument" through PHP/Symfony's wrapper). This is **not** a permissions or NAS-connectivity issue — the mount itself is healthy throughout. + +**What didn't work** (documented so this isn't re-attempted blind on a future upgrade): +- Setting `TMPDIR` env var — Koillection's upload handler doesn't consult it (hardcoded path, not `sys_get_temp_dir()`) +- Setting `upload_tmp_dir`/`sys_temp_dir` in `php.ini` — confirmed via `php -i` that the values loaded correctly, but the specific code path still ignored them +- Bind-mounting a second NFS path directly onto `/tmp` (`volumes: - /uploads/.tmp:/tmp`) — `stat` showed matching device IDs, but `rename()` still failed with the same `Invalid argument` error. Two separate mounts of the same NFS export are not treated as one filesystem by `rename()` on this NAS, even though they report identical device numbers. + +**What actually works**: replace `/tmp` with a **symlink** into a folder inside the already-mounted `/uploads` NFS export, so there's only ever one real mount involved and no ambiguity for `rename()`: + +```bash +rm -rf /tmp && ln -s /uploads/.phptmp /tmp +``` + +This is baked into the `entrypoint:` override in Phase 5's `docker-compose.yml` above, so it runs fresh on every container start/recreate — it does **not** persist through a plain `docker compose restart` alone if done manually outside the entrypoint, which is exactly why it needed to be wrapped into the entrypoint rather than run once by hand. + +**Verified working** 2026-08-11: created a test collection with an uploaded photo, then deleted it — full round trip succeeded with no errors. --- @@ -337,12 +388,11 @@ Add the Authelia middleware to the nginx vhost in CT 101 (follow the pattern fro --- -## Phase 10 — First Login & Initial Setup +## Phase 10 — First Login & Initial Setup ✅ DONE (2026-08-11) -Open `https://collections.spendlik.sk` from mobile data (hairpin NAT — never test from LAN). - -On first load, Koillection will prompt you to create an admin account. Do so, then: +Opened `https://collections.spendlik.sk` from mobile data (hairpin NAT — never test from LAN). Admin account created successfully at `/first-connection`. +Recommended, still worth confirming in profile settings if not already done: 1. Set your timezone to `Europe/Bratislava` in profile settings 2. Set the display currency if tracking purchase values 3. Set visibility defaults (private by default is fine for a personal instance) @@ -351,6 +401,8 @@ On first load, Koillection will prompt you to create an admin account. Do so, th ## Phase 11 — Collection Setup +**Smoke test passed 2026-08-11**: created a "Test" collection with an uploaded photo, confirmed it worked end-to-end (this is what caught and validated the fix for the image-upload defect above), then deleted it. The actual 5 planned collections below are not yet created. + Recommended collection structure. Create each as a top-level Collection: ### 🚗 Hot Wheels @@ -430,6 +482,8 @@ mkdir -p /opt/koillection/backups ``` > ⚠️ Always back up the database before upgrading Koillection — the developer notes that data migrations can occasionally have edge cases. +> +> ⚠️ **Note re: image-upload fix above** — if Koillection is ever upgraded to a new image version, re-verify the `entrypoint:` symlink workaround is still needed (a future upstream release may fix the hardcoded `/tmp` path) and re-test an image upload after any upgrade, before assuming it still works. --- @@ -446,8 +500,9 @@ mkdir -p /opt/koillection/backups | Wrong template filename | Verify exact template string with `pveam list local` before `pct create` — versions bump periodically | | Wrong NAS host mount path | Storage ID is `spendlik-nas`, mounted at `/mnt/pve/spendlik-nas` — verify with `ls /mnt/pve/` before trusting any guide's hardcoded path | | `curl \| grep koillection` shows nothing | Not a failure — the app root just 302-redirects to `/first-connection`, whose HTML doesn't contain the word "koillection". Use `docker compose ps` (expect `healthy`) and `curl -sv` (expect `302` + `Location: /first-connection`) instead | -| `deprecation.INFO` API Platform log spam on startup | Harmless upstream framework warnings (duplicate `#[ApiResource]` shortName), not errors — ignore | +| `deprecation.INFO` API Platform / symfony-form log spam on startup | Harmless upstream framework warnings, not errors — ignore | | `nginx: command not found` when following Phase 6 | You're inside CT 114 (koillection), not CT 101 (reverse-proxy). Check the shell prompt — nginx work always happens on CT 101, never on the app container itself | | Wrong nginx vhost filename | Use the full domain as the filename (`collections.spendlik.sk`), matching every other vhost on CT 101 — not the short service name | | certbot fails domain validation | DNS (Phase 8) must resolve publicly before certbot's HTTP-01 challenge (Phase 7) will succeed — do DNS first if it hasn't propagated yet | | No 2FA on collections.spendlik.sk | Intentional — Authelia was bypassed by decision (Phase 9). Security relies solely on Koillection's own login. Revisit if this ever becomes multi-user or exposed beyond personal use | +| Image uploads fail with "critical error" / `rename(): Invalid argument` | Hardcoded `/tmp` path in Koillection's upload handler colliding with NFS-mounted `/uploads` being a different filesystem — see "Known Issue" section above for full root cause and the entrypoint-symlink fix (already baked into this guide's Phase 5 compose file) |