15 KiB
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.skLast 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-nason the host (confirmed via livels /mnt/pve/). CT 111 (Paperless) uses/mnt/pve/spendlik-nas/data/paperlessas 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/(onlyspendlik-naspresent) 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_SECRETwith: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 argumentlines for/uploadsin 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. Usecollections.spendlik.skas the filename here, notkoillection.
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.sksubdomains are now CNAME records pointing at the rootspendlik.sk. Only the rootspendlik.skA 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.sk → spendlik.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.sk → 95.102.127.184).
✅ No DDNS updater step needed for this subdomain.
ddns-update.shon 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:
- Set your timezone to
Europe/Bratislavain profile settings - Set the display currency if tracking purchase values
- 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-saleetc. to group across all five collections at once.
Backup
The only things that need backing up:
- PostgreSQL database — contains all collection metadata
- 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 |