Self-hosted manager for a coin and paper money collection. Runs in Docker, stores everything in PostgreSQL on your own machine, and works from a phone browser: photograph a coin or banknote and the server crops it and cleans it up for you.
The image is on Docker Hub as mfrankovic/kolektor , built for amd64 and arm64 , so a Raspberry Pi works as well as a NUC.
git clone https://github.com/marinfrankovic/Kolektor.git cd Kolektor cp .env.example .env # set POSTGRES_PASSWORD and KOLEKTOR_SECRET_KEY (openssl rand -hex 32) chmod 600 .env docker compose up -d That pulls the published image. To build it yourself from the checkout instead, run docker compose up -d --build . To pin a release rather than follow latest , set KOLEKTOR_IMAGE=mfrankovic/kolektor:1.0 in .env .
Upgrading is docker compose pull && docker compose up -d . The database migrates itself on start; your photos and rows are untouched.
Open http://<host-ip>:8100 . That is the whole installation. A custom domain, HTTPS and a reverse proxy are all optional extras, covered further down.
If you point KOLEKTOR_MEDIA_DIR at a directory you created yourself, hand it to the container user first ( chown -R 10001:10001 <dir> ), otherwise uploads fail with a permission error. deploy/deploy.sh does this for you.
The first time you open the app it asks how you want to open it from then on:
Either choice can be changed later in Settings → Access , in both directions. Switching modes signs out every existing session; your collection is untouched.
To skip the first-run screen entirely (useful for an unattended deploy), set both KOLEKTOR_INITIAL_USER_EMAIL and KOLEKTOR_INITIAL_USER_PASSWORD in .env before the first start. The account is created in password mode and the wizard never appears.
On a phone, open the app in the browser and choose Add to home screen ; it installs as a PWA and the same form uses the camera directly. Photos taken while offline are stored in the browser and uploaded when the connection returns.
The server crops the coin or note out of the background, straightens it, and generates thumbnail, preview and display sizes plus a perceptual hash. Colours and sharpness are left alone unless you set KOLEKTOR_AUTOENHANCE=true .
On the item page each photo has a ⟳ Retake button that shoots or picks a replacement and keeps the same role; the old file is only deleted once the new one has uploaded. If an item is missing its obverse or reverse (face or back for a note) a dashed + slot appears in its place, so a piece you photographed only from one side is easy to spot and finish.
Photographing tip: plain dark background, even light, fill the frame, hold the camera parallel to the piece.
A full record has around 60 fields, and most collectors use a fraction of them. Settings → Item fields lists every optional one as a checkbox, grouped the same way the item page is: item details, coin details, banknote details, acquisition, sale. Untick what you do not use and it disappears from the item page. A group whose fields are all unticked drops its whole card.
Seven fields cannot be hidden, because an item without them describes nothing: type, status, title, country, denomination, currency unit and year .
Hiding a field never touches your data. Anything you already typed stays in the database and comes back the moment you tick the box again. The choice is stored in the browser, so each device can show a different amount of detail.
Everything lives in .env . The defaults are chosen for a LAN install.
Photos are stored on disk, not in the database. Back up both.
Only do this if you want to reach the collection from outside your network. The app works without it.
KOLEKTOR_BEHIND_PROXY=true KOLEKTOR_COOKIE_SECURE=true KOLEKTOR_PUBLIC_BASE_URL=https://kolektor.example.com docker compose up -d . If your nginx runs in a container, keep the resolver 127.0.0.11; line and the variable upstream from the sample config. Without them nginx resolves the app container's IP once at startup and starts returning 502 the first time you recreate the app.
A containerised nginx also has to share a Docker network with the app, otherwise it cannot resolve kolektor-app at all. Add the proxy's network to the app service and declare it as external:
app : networks : - default - proxy_net networks : default : proxy_net : external : true Note that KOLEKTOR_COOKIE_SECURE=true makes the session cookie HTTPS-only, so signing in over plain http://<host-ip>:8100 stops working. Use the domain from then on.
With a password-protected instance exposed to the internet, also consider putting an authentication proxy in front of it.
deploy/backup.sh writes a gzipped pg_dump and a media manifest into a directory of your choice, then prunes anything older than 14 days:
crontab -e 30 2 * * * KOLEKTOR_BACKUP_DIR=/mnt/backup/kolektor /path/to/Kolektor/deploy/backup.sh Restore:
gunzip -c kolektor-20260101-0230.sql.gz | docker exec -i kolektor-db psql -U kolektor -d kolektor Tests The suite covers services, the API, authentication and the first-run modes, imaging, statistics, the worker, remote image fetching, and a dedicated set of security tests (path traversal, upload sniffing, SSRF, SQL injection, rate limiting, session handling, security headers).
cd backend python -m venv .venv && .venv/bin/pip install -r requirements-dev.txt .venv/bin/pytest # 203 tests .venv/bin/ruff check . .venv/bin/bandit -c pyproject.toml -r app .venv/bin/pip-audit -r requirements.txt On Windows use .venv\Scripts\pytest.exe .
.github/workflows/ci.yml runs all of the above on every push and pull request, then builds the frontend and the Docker image. Nothing merges without a green run.
# backend cd backend uvicorn app.main:app --reload # http://127.0.0.1:8000, API docs at /api/docs # frontend cd frontend npm install npm run dev # http://127.0.0.1:5174, proxies /api to the backend Without KOLEKTOR_DATABASE_URL the backend expects Postgres on db:5432 ; point it at a local instance or run docker compose up -d db .
backend/app/ main.py FastAPI app, security headers, SPA fallback models.py SQLAlchemy models (Postgres, SQLite-compatible for tests) schemas.py Pydantic request/response models fetching.py Guarded download of images the user linked to seed.py Countries, historical entities, optional first user worker.py Polls the job table, processes images imaging/ detect, enhance, pipeline routers/ auth, items, images, stats, reference backend/tests/ pytest suite frontend/src/ i18n/ English and Croatian dictionaries lib/ date formatting, theme, item field visibility pages/ Setup, Login, Collection, ItemNew, ItemEdit, MapView, Stats, Settings deploy/ optional nginx vhost, deploy and backup scripts Adding a language means adding one dictionary in frontend/src/i18n/dictionaries.ts and one entry to UI_LANGUAGES in backend/app/models.py .
Passwords are hashed with argon2id. Session tokens are random, stored only as an HMAC, and sent in an HttpOnly cookie. Failed logins are rate limited per IP. Uploads are checked by magic bytes, not by file extension, and re-encoded before being served. Images imported from a URL only reach public addresses: the scheme must be http or https, the hostname is resolved and checked against private, loopback, link-local and multicast ranges on every redirect hop, redirects stop at three, and the download has a timeout and a size cap. EXIF, including GPS, is stripped from every derivative image. HSTS is emitted only when you have actually terminated TLS, so a LAN install cannot lock itself out of plain HTTP. In no login mode there is no authentication at all. Do not expose that instance to the internet. Disclaimer Kolektor is a personal project, published in case it is useful to someone else. It comes with no warranty and no support commitment. Read the points below before you trust it with a collection you care about.
MIT . Do what you like with it; keep the copyright notice.
Issues and pull requests are welcome, though this is a spare-time project and replies may be slow. Run the test suite and ruff before opening a pull request; CI will run them anyway.
Hacker News
news.ycombinator.com