Claude_Homelab/114_koillection_deployment.md

15 KiB
Raw Blame History

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+) CT ID: 114 · IP: 192.168.1.114 Domain: collections.spendlik.sk Last updated: 2026-08-11


Overview

Koillection is a self-hosted collection manager for tracking physical collections of any kind. No pre-built metadata scrapers — metadata is added freely per item, with custom fields via templates. MIT licensed.

Collections planned for this instance:

  • 🚗 Hot Wheels (series, year, colour, condition, variants)
  • 🧱 LEGO (set number, theme, piece count, minifigures, completion status)
  • 🦇 Batmobiles (source media, scale, manufacturer, condition)
  • 📚 Comics (title, issue, publisher, language, condition)
  • 📄 Paper Models (designer, scale, subject, format — physical printed copies)

Stack: koillection/koillection (PHP/Symfony + Vue.js, served via FrankenPHP/Caddy) + PostgreSQL 16. Uploads (item photos) bind-mounted to NAS for data safety.


Resource Allocation

Resource Allocation
CT ID 114
IP 192.168.1.114
CPUs 1
RAM 512 MB
Disk 8 GB (app + DB only; photos on NAS)
Template Debian 13 (trixie) — created from 13.1-2 (current template at time of creation; 13.6-1 is now the default for new deployments, but this doesn't matter post-creation — see Proxmox LXC Templates.md)
Privileged Yes (Docker requires it)
Nesting Enabled (features: nesting=1)

NAS Mount Planning

Photos uploaded to Koillection land in /uploads inside the container. This will be bind-mounted from the NAS at:

/volume1/proxmox/data/koillection/uploads

Create this directory on the NAS before deployment:

# On the Synology NAS (SSH or File Station)
mkdir -p /volume1/proxmox/data/koillection/uploads

⚠️ 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.


Phase 1 — Create LXC Container DONE (2026-08-11)

pct create 114 local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst \
  --hostname koillection \
  --cores 1 \
  --memory 512 \
  --swap 512 \
  --rootfs local-lvm:8 \
  --net0 name=eth0,bridge=vmbr0,ip=192.168.1.114/24,gw=192.168.1.1 \
  --unprivileged 0 \
  --features nesting=1 \
  --ostype debian \
  --start 1

Enter the container:

pct enter 114

Phase 2 — Base Setup DONE (2026-08-11)

apt update && apt upgrade -y
apt install -y nano curl ca-certificates gnupg lsb-release

Phase 3 — Install Docker DONE (2026-08-11)

install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/debian \
  $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null

apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

Verify:

docker run --rm hello-world

Phase 4 — NAS Bind Mount DONE (2026-08-11)

Add the NAS uploads path as a Proxmox bind mount. Exit the container first:

exit

On the Proxmox host:

pct set 114 --mp0 /mnt/pve/spendlik-nas/data/koillection/uploads,mp=/uploads,shared=1

Verified 2026-08-11 against live ls /mnt/pve/ (only spendlik-nas present) and cross-checked against CT 111 (Paperless)'s actual documented host mount (/mnt/pve/spendlik-nas/data/paperless) — this replaces an earlier placeholder path in this guide that would have failed (wrong storage name).

Re-enter the container and verify the mount is visible:

pct enter 114
ls -la /uploads

Confirmed: /uploads mounted, 777 nobody:nogroup, empty — ready for the app to write to.


Phase 5 — Deploy Koillection DONE (2026-08-11)

mkdir -p /opt/koillection
cd /opt/koillection
nano .env

Paste (fill in a strong password for DB_PASSWORD):

DB_DRIVER=pdo_pgsql
DB_NAME=koillection
DB_HOST=db
DB_PORT=5432
DB_USER=koillection
DB_PASSWORD=CHANGE_ME
DB_VERSION=16
APP_ENV=prod
APP_DEBUG=0
APP_SECRET=CHANGE_ME_32CHAR_RANDOM_STRING
PHP_TZ=Europe/Bratislava
HTTPS_ENABLED=0

Generate APP_SECRET with: openssl rand -hex 16

nano docker-compose.yml

Paste:

services:
  koillection:
    image: koillection/koillection:latest
    container_name: koillection
    restart: unless-stopped
    ports:
      - "8080:80"
    env_file:
      - .env
    volumes:
      - /uploads:/uploads
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    container_name: koillection-db
    restart: unless-stopped
    env_file:
      - .env
    environment:
      - POSTGRES_DB=${DB_NAME}
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - ./volumes/postgresql:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-q", "-d", "koillection", "-U", "koillection"]
      timeout: 45s
      interval: 10s
      retries: 10

Start:

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):

docker compose ps
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.


Phase 6 — nginx Reverse Proxy (CT 101) DONE (2026-08-11)

Enter CT 101 — this must be done on CT 101, not CT 114. nginx does not and should not exist on CT 114 itself.

pct enter 101
nano /etc/nginx/sites-available/collections.spendlik.sk

Verified 2026-08-11 against live ls /etc/nginx/sites-available/ on CT 101 — every existing vhost is named by full domain (paperless.spendlik.sk, jellyfin.spendlik.sk, vault.spendlik.sk, etc.), not by short service name. Use collections.spendlik.sk as the filename here, not koillection.

Paste:

server {
    listen 80;
    server_name collections.spendlik.sk;

    location / {
        proxy_pass http://192.168.1.114:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        client_max_body_size 20M;
    }
}

client_max_body_size 20M — item photos can be large. Adjust upward if needed.

Enable and reload:

ln -s /etc/nginx/sites-available/collections.spendlik.sk /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

Confirmed: nginx -t → syntax ok, config test successful, reload applied cleanly.


Phase 7 — SSL Certificate DONE (2026-08-11)

Still in CT 101:

certbot --nginx -d collections.spendlik.sk

Certificate issued successfully, expires 2026-11-09, auto-renewal scheduled by certbot.

Config inspected after issuance — no corruption this time: two server_name collections.spendlik.sk; lines are expected (one in the port-80 redirect block, one in the port-443 SSL block, matching the verified-good structure already used by CT 111 Paperless). Brace counts balance correctly in both blocks.

Also set HTTPS_ENABLED=1 in /opt/koillection/.env in CT 114, then restart:

# In CT 114
cd /opt/koillection
nano .env   # set HTTPS_ENABLED=1
docker compose restart koillection

⚠️ Required — without this, Koillection generates internal links as http://, which combined with the nginx 80→443 redirect causes a redirect loop.


Phase 8 — DNS Record DONE (2026-08-11)

Updated 2026-08-10: All *.spendlik.sk subdomains are now CNAME records pointing at the root spendlik.sk. Only the root spendlik.sk A record holds an IP — WebSupport rejects any duplicate IP value elsewhere in the zone. Do not create an A record for this subdomain.

CNAME record created: collections.spendlik.skspendlik.sk, TTL 600, record ID 340219244. Added to 00_index.md DNS table.

Verified resolving via nslookup collections.spendlik.sk on CT 101 (canonical name → spendlik.sk95.102.127.184).

No DDNS updater step needed for this subdomain. ddns-update.sh on CT 108 only updates the root A record on IP change; this CNAME resolves through automatically.

⚠️ Note for future deployments: certbot's HTTP-01 challenge (Phase 7) needs DNS to already resolve publicly — do DNS before attempting SSL if it hasn't propagated yet. In this deployment, DNS (Phase 8) was done before Phase 7 for exactly this reason, even though the guide lists them in this numeric order for documentation clarity.


Phase 9 — Authelia Protection (CT 102) — SKIPPED by decision (2026-08-11)

Decision: bypass Authelia, rely on Koillection's own login only. Personal single-user instance — Koillection's built-in authentication is sufficient, and no Authelia middleware was ever added to the CT 101 nginx vhost (Phase 6), so there is nothing to add or remove. No /etc/authelia/configuration.yml changes were made for this domain.

Reference: steps to add Authelia later if this decision changes

Enter CT 102, edit /etc/authelia/configuration.yml. Add to access_control.rules:

- domain: collections.spendlik.sk
  policy: two_factor

Restart Authelia after editing:

docker compose restart

Add the Authelia middleware to the nginx vhost in CT 101 (follow the pattern from other protected services).


Phase 10 — First Login & Initial Setup

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:

  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)

Phase 11 — Collection Setup

Recommended collection structure. Create each as a top-level Collection:

🚗 Hot Wheels

Suggested item fields (via Template):

  • Series / Line
  • Year of release
  • Colour
  • Casting name
  • Country of manufacture
  • Condition (Mint / Good / Played)
  • Treasure Hunt (yes/no)

🧱 LEGO

Suggested item fields:

  • Set number
  • Theme
  • Sub-theme
  • Piece count
  • Minifigure count
  • Year
  • Completion status (Sealed / Built / Parts only)
  • Instruction booklet present (yes/no)

💡 The set number field makes cross-referencing with kocka-novinky.sk and Brickset API straightforward.

🦇 Batmobiles

Suggested item fields:

  • Source (Film / TV / Comics / Game)
  • Year of appearance
  • Manufacturer (Hot Wheels / Corgi / LEGO / custom)
  • Scale
  • Condition

📚 Comics

Suggested item fields:

  • Title / Series
  • Issue number
  • Publisher
  • Language
  • Year
  • Condition (Mint / Very Good / Good / Fair)
  • Story arc

📄 Paper Models (physical hardcopy)

Suggested item fields:

  • Designer / Publisher
  • Subject (aircraft, ship, building…)
  • Scale
  • Format (magazine supplement / standalone / kit)
  • Build status (Unbuilt / Built / Display)

💡 Tags are cross-collection in Koillection — tag items with #display, #wishlist, #for-sale etc. to group across all five collections at once.


Backup

The only things that need backing up:

  1. PostgreSQL database — contains all collection metadata
  2. NAS uploads directory — contains all item photos (already on NAS, covered by NAS backup)

Add a daily DB dump to cron in CT 114:

crontab -e

Add:

0 3 * * * docker exec koillection-db pg_dump -U koillection koillection > /opt/koillection/backups/koillection-$(date +\%Y\%m\%d).sql 2>/dev/null
mkdir -p /opt/koillection/backups

⚠️ Always back up the database before upgrading Koillection — the developer notes that data migrations can occasionally have edge cases.


Gotchas

Issue Fix
Photos not saving Verify /uploads bind mount is writable inside the container
App not starting Check docker compose logs koillection — usually a DB connection issue on first boot
certbot corrupts nginx config Always inspect after issuance
Large photo uploads rejected Increase client_max_body_size in nginx vhost
HTTPS redirect loop Set HTTPS_ENABLED=1 in .env and restart the koillection container after SSL is in place
DNS record type Use CNAME → spendlik.sk, never a per-subdomain A record (see Phase 8)
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
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