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

This commit is contained in:
Spendlik 2026-08-11 11:14:23 +00:00
parent d45c9d203b
commit 886c9b010b

View File

@ -1,6 +1,6 @@
# 114 — Koillection Deployment Guide # 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 > CT ID: 114 · IP: 192.168.1.114
> Domain: `collections.spendlik.sk` > Domain: `collections.spendlik.sk`
> Last updated: 2026-08-11 > 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/<service>`). Be consistent. > ⚠️ The NAS path follows the same convention as Paperless (`/volume1/proxmox/data/<service>`). 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. > **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. 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 ```bash
mkdir -p /opt/koillection mkdir -p /opt/koillection
@ -169,11 +178,20 @@ HTTPS_ENABLED=0
> Generate `APP_SECRET` with: `openssl rand -hex 16` > 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 ```bash
nano docker-compose.yml 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 ```yaml
services: services:
@ -181,12 +199,14 @@ services:
image: koillection/koillection:latest image: koillection/koillection:latest
container_name: koillection container_name: koillection
restart: unless-stopped restart: unless-stopped
entrypoint: ["sh", "-c", "rm -rf /tmp && ln -s /uploads/.phptmp /tmp && exec sh /app/public/docker/entrypoint.sh"]
ports: ports:
- "8080:80" - "8080:80"
env_file: env_file:
- .env - .env
volumes: volumes:
- /uploads:/uploads - /uploads:/uploads
- ./upload-tmp.ini:/usr/local/etc/php/conf.d/zz-upload-tmp.ini:ro
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
@ -217,7 +237,7 @@ docker compose up -d
docker compose logs -f 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 ```bash
docker compose ps 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. 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). Opened `https://collections.spendlik.sk` from mobile data (hairpin NAT — never test from LAN). Admin account created successfully at `/first-connection`.
On first load, Koillection will prompt you to create an admin account. Do so, then:
Recommended, still worth confirming in profile settings if not already done:
1. Set your timezone to `Europe/Bratislava` in profile settings 1. Set your timezone to `Europe/Bratislava` in profile settings
2. Set the display currency if tracking purchase values 2. Set the display currency if tracking purchase values
3. Set visibility defaults (private by default is fine for a personal instance) 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 ## 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: Recommended collection structure. Create each as a top-level Collection:
### 🚗 Hot Wheels ### 🚗 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. > ⚠️ 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 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 | | 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 | | `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 | | `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 | | 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 | | 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 | | 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) |