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
- Maintenance log — per-firearm cleaning, repair, and part-replacement entries, with a configurable due-for-cleaning reminder
- Range sessions — per-firearm session log with date, location, rounds fired, and running totals
- Photo attachments — up to 12 photos per firearm, stored locally in the
data volume (
/data/photos) - Dashboard and stats — recent activity, type/caliber/make breakdowns, acquisition trends, average price by year, cleaning-due list
- 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 data directory — the SQLite database and photo files live together
under
/data; back up that one directory withcp - 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 on the host — the SQLite
database at ./data/app.db and any firearm photos under ./data/photos —
so backing up that one directory backs up your whole 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
Release images are scanned before publication, exercised through an end-to-end login and inventory flow, and signed keylessly with Sigstore. Install cosign and verify the image before deployment:
cosign verify ghcr.io/gogorichielab/ppcollection:latest \
--certificate-identity-regexp 'https://github.com/Gogorichielab/PPCollection/.github/workflows/release.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.comSuccessful verification confirms that the image was signed by this repository's release workflow. Pin a version tag or digest for reproducible deployments.
Application-generated logs in production deployments, including the Docker
image, emit one JSON object per line to stdout or stderr. This works directly
with docker logs, journald, Loki, Vector, and other collectors. HTTP access
records use this schema:
| Field | Description |
|---|---|
ts |
ISO 8601 timestamp |
level |
info, warn, or error |
event |
Stable event name, such as http.request or login.success |
id |
Correlation ID shared by the response, access log, and related audit events |
method, url, status |
HTTP request and response details |
durationMs, contentLength |
Response timing and size when available |
remoteAddress, userAgent |
Request source metadata |
Clients may send an X-Request-ID header containing 1-128 letters, numbers,
periods, underscores, colons, or hyphens. Valid values are returned unchanged in
the response header; missing or invalid values are replaced with a UUID. Health
requests return a correlation header but are omitted from access logs to avoid
probe noise. Audit logs redact usernames and serial numbers unless
AUDIT_VERBOSE=true.
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.
