Self-hosted, offline-first firearm inventory app — track and manage a personal collection locally with no cloud dependency. Node.js / Express + SQLite, no build step, one Docker command to run.
- Inventory CRUD — add, edit, duplicate, and delete firearm records with make, model, serial, caliber, type, condition, status, location, purchase details, warranty, and notes
- Disposition tracking — records sold/lost/stolen items with transferee, date, and reason; included in CSV exports
- Dashboard and stats — recent activity, type/caliber/make breakdowns, acquisition trends, average price by year
- Insurance report — print-friendly inventory with total purchase value
- Search, filter, sort — real-time across all fields; mobile rows collapse to cards
- CSV import & export — round-trippable, with a template
- Single-admin auth — bcrypt-hashed password, forced change on first login, CSRF on every form
- Fully offline — zero internet at runtime; the GitHub Releases update check is opt-in
- One SQLite file — back up your whole collection with
cp - Docker ready — multi-arch image on GHCR
docker run -d \
--name ppcollection \
-p 3000:3000 \
-e SESSION_SECRET="$(openssl rand -hex 32)" \
-e ADMIN_USERNAME=admin \
-e ADMIN_PASSWORD=YourSecurePassword \
-v "$(pwd)/data:/data" \
--restart unless-stopped \
ghcr.io/gogorichielab/ppcollection:latestOpen http://localhost:3000 and log in. You will be prompted to change the
password on first login. Your data lives in ./data/app.db on the host —
back up that directory to back up your collection.
Use an absolute host path for the volume (or a named volume like
-v ppcollection_data:/data). Docker treats a bare./dataas an anonymous volume and discards it whenever the container is recreated.Important when updating: reuse the exact same host directory or named volume every time you recreate the container. If the app asks you to change the initial admin password again after an update, it is almost always running against a new empty
/datamount instead of your existingapp.db. Stop the container and restore the previous mount before adding new records.
If an updated container looks like a brand-new install, do not complete the first-run password flow or add new inventory yet. That means the container is not seeing the same SQLite file it used before the update. Check the active mount and database file first:
docker inspect ppcollection --format '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}'
ls -lh ./data/app.dbFor Docker Compose, run updates from the same directory that owns the original
./data folder, or change the compose file to an absolute bind mount such as
/srv/ppcollection/data:/data. For docker run, always reuse the same
absolute bind mount (-v /srv/ppcollection/data:/data) or the same named volume
(-v ppcollection_data:/data). Recreating the container without that mount
creates a fresh database at /data/app.db, which makes the app correctly behave
like a first-time install.
Full docs live in the wiki:
- Installation — Docker, Docker Compose, and from-source setups
- Configuration — every environment variable, with production defaults
- Security — reverse-proxy setup,
TRUST_PROXY, secure cookies, CSRF, audit logs - Operations — health probe, graceful shutdown, backup and restore
- Upgrading — version-specific notes (v2.0.0 secure-cookies default, v2.0.1 session-secret guard)
- Architecture — code layout and request lifecycle
- Screenshots — full gallery
- FAQ — common questions and recovery procedures
See CONTRIBUTING.md and the
Contributing page
in the wiki. The project uses .github/CODEOWNERS to request maintainer
review automatically, especially for GitHub Actions, Docker runtime files,
release configuration, and database migrations.
Licensed under the Business Source License 1.1. Personal, non-commercial, self-hosted use is permitted at no charge. The license converts to Apache 2.0 on 2029-05-13.
