Claude_Homelab/114_koillection_deployment.md

14 KiB
Raw Blame History

114 — Koillection Deployment Guide

Status: IN PROGRESS — CT created 2026-08-11, app stack + nginx reverse proxy live on HTTP, SSL/DNS/Authelia pending (Phase 7+) 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

Still in CT 101:

certbot --nginx -d collections.spendlik.sk

⚠️ Always inspect the config after certbot:

cat /etc/nginx/sites-available/collections.spendlik.sk

Check for duplicate server_name directives and missing closing braces. Fix manually if needed.

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

Phase 8 — DNS Record

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.

In WebSupport admin panel:

  1. Add CNAME record: collectionsspendlik.sk
  2. Check both DNS management pages
  3. Note the numeric record ID
  4. Add to 00_index.md DNS table

No DDNS updater step is 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: certbot's HTTP-01 challenge (Phase 7) needs this DNS record to already resolve publicly. If Phase 7 hasn't been attempted yet, do Phase 8 (DNS) before Phase 7 (SSL) — the guide lists them in this order for documentation clarity, but in practice DNS must propagate first or certbot will fail domain validation.


Phase 9 — Authelia Protection (CT 102)

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

- domain: collections.spendlik.sk
  policy: two_factor

Koillection has its own internal login system. Authelia adds a second layer before users even reach the login page. Since this is a personal single-user instance, you may prefer Authelia bypass and rely on Koillection's own login instead — your call.

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