Getting Started for Individuals#
This guide walks through deploying and using MagStacker on your own hardware for personal firearm and magazine inventory management.
Overview#
MagStacker is a self-hosted web application that keeps track of your firearms and magazines: what you own, what fits what, and how much it all holds . As an individual owner, you get a practical set of tools without handing your inventory data to a third-party service:
- Track firearms and magazines with the fields that matter — caliber, capacity, labels, acquired date, serial number, and notes.
- See capacity at a glance — a running total of all your magazines, with counts broken down per caliber and per firearm.
- Label magazines for identification — assign short identifiers like
9mm-01that appear consistently throughout the UI and in exports. - Export to CSV for insurance documentation, records-keeping, or just a spreadsheet backup. Serial numbers are excluded from exports by design .
- Your data stays with you — MagStacker runs on a machine you control (a homelab server, a NAS, a spare always-on box) and stores everything in your own database. There is no cloud account, no subscription, and no vendor who can revoke your access .
MagStacker also supports multi-user sharing features — useful if you want to share specific items with family members — but for a single-person deployment, none of that complexity is required to get started.
This guide is for individual owners running MagStacker for a personal collection. If you're deploying for a club, range, or any multi-user institutional context, see the Getting Started for Clubs and Ranges guide instead.
Prerequisites#
Before starting, make sure you have:
-
Docker and Docker Compose (v2+) installed on a machine you control — a homelab server, a NAS, a spare always-on PC, whatever you keep running . The stack runs as three containers: Postgres, a one-shot migrate/seed service, and the Next.js app.
-
opensslto generate the two required secret files (the database password and the authentication signing key). Most Linux and macOS systems have this installed already.openssl rand -hex 24 # generates a database password openssl rand -hex 32 # generates a signing secret -
Basic terminal familiarity — you'll need to run a handful of shell commands to set things up, but nothing beyond copy-paste.
-
Optional but recommended: a TLS-terminating reverse proxy (Caddy, nginx, or Traefik) if you plan to access MagStacker from outside localhost. MagStacker uses session cookies for authentication; exposing it over plain HTTP is a security risk . For purely local access on a single machine this can wait, but it's worth setting up before you put it on a home network.
No database administration knowledge is required. You don't need to install Postgres or Bun — everything runs inside Docker .
Installation#
1. Get the code#
Clone the repository:
git clone https://github.com/unclesp1d3r/mag_stacker.git
cd mag_stacker
If you only want the Docker setup files without the full source, you can download docker-compose.yml and .env.example directly and work from that directory.
2. Create the secret files#
MagStacker uses Docker secrets for the two values that must never appear in plaintext environment variables: the database password and the authentication signing key . They are stored as plain files in a secrets/ directory, mounted read-only into the containers that need them.
From the repo root (the directory containing docker-compose.yml), run:
mkdir -p secrets
openssl rand -hex 24 > secrets/postgres_password.txt
openssl rand -hex 32 > secrets/better_auth_secret.txt
Use
-hex, not-base64. The database password is embedded unescaped in a PostgreSQL connection URL. Base64 characters like/,+, and=are not valid unescaped in a URL, and will silently break the connection .
docker compose up will refuse to start with the error secret ... not found until both files exist . The secrets/ directory is gitignored — never commit these files.
3. Copy the environment file#
cp .env.example .env
The .env file is also gitignored. Open it and fill in the values described in the next section before starting the stack .
Configuration#
Open .env in a text editor. Most defaults are fine for a personal deployment; the values you must set are marked below .
Database#
POSTGRES_USER=magstacker # database username — default is fine
POSTGRES_DB=magstacker # database name — default is fine
POSTGRES_HOST_PORT=5544 # host port Postgres is published on
POSTGRES_HOST_PORT defaults to 5544 rather than the standard 5432 to avoid clashing with any Postgres instance you might already have running on the host. Change it if 5544 is taken by something else. The app container always reaches the database by service name on port 5432 internally, so this only affects local tooling access .
The database password lives in secrets/postgres_password.txt — not here.
Authentication#
BETTER_AUTH_URL=http://localhost:3000 # URL you will browse to
ADMIN_EMAIL=admin@example.com # your email — bootstraps the first admin account
ADMIN_PASSWORD=change-me # your admin password
BETTER_AUTH_URLmust exactly match the origin you open in your browser. If you access MagStacker athttp://192.168.1.50:3000, set it to that. If you later put it behind a reverse proxy athttps://mags.home.example, update this value and restart the stack. A mismatch causes Better Auth to reject requests with a 403 .ADMIN_EMAILandADMIN_PASSWORDare used once to seed your first operator account. Use a strong password — this account has full access to the system .
The signing secret lives in secrets/better_auth_secret.txt — not here.
Storage#
UPLOAD_DIR=/data/uploads
Leave this at the default when running with Docker Compose. It maps to the magstacker-uploads named volume inside the container .
Application#
APP_HOST_PORT=3000 # host port the app is published on
MAGSTACKER_VERSION=latest # Docker image tag
APP_HOST_PORT defaults to 3000. If that port is already in use on your machine, change it (e.g., APP_HOST_PORT=8080) — your reverse proxy or browser address will then use that port instead.
For MAGSTACKER_VERSION, latest is fine for getting started. Pin it to a specific release (e.g., 1.2.3) when you want reproducible updates .
Minimal .env for a personal deployment#
# Database
POSTGRES_USER=magstacker
POSTGRES_DB=magstacker
POSTGRES_HOST_PORT=5544
# Auth
BETTER_AUTH_URL=http://localhost:3000
ADMIN_EMAIL=your@email.com
ADMIN_PASSWORD=a-strong-password-here
# App
APP_HOST_PORT=3000
MAGSTACKER_VERSION=latest
Everything else (logging, upload path) has sensible defaults and can be left unset to start.
Quick Start#
Start the stack#
From the directory containing your docker-compose.yml and .env files, start the stack:
docker compose up --build -d
The stack starts in a strict order enforced by health checks and completion conditions :
db— Postgres starts and waits until healthy.migrate— applies all database migrations and seeds your first admin account fromADMIN_EMAIL/ADMIN_PASSWORD. Seeding is idempotent: the admin is created only on an empty database and skipped on subsequent runs .app— starts once the migrate service completes successfully.
Verify migration ran#
Check that the migration service successfully created your admin account:
docker compose logs migrate
Expected output:
Created admin account for <your-email>.
If ADMIN_EMAIL / ADMIN_PASSWORD were not set before the first run, the seed is skipped and the stack still comes up . Set them in .env and re-run docker compose up -d to seed.
Sign in#
Open http://localhost:3000/login (or the URL you configured in BETTER_AUTH_URL) and sign in with the credentials you set in ADMIN_EMAIL and ADMIN_PASSWORD .
Add your first firearm#
Navigate to the Firearms section and add a firearm with :
- Manufacturer — the make of the firearm
- Model — the model name
- Caliber — used to match compatible magazines and drive caliber summaries
- Serial number (optional) — treated as sensitive data throughout the application; never included in CSV exports
- Nickname / Notes (optional) — useful for distinguishing individual units
Add magazines#
Navigate to the Magazines section and add magazines with :
- Brand and Model
- Caliber
- Base capacity and Extension rounds — both are tracked, and effective capacity is computed as the sum
- Label (optional) — a short identifier like
AR-01. Labels appear throughout the UI and in CSV exports - Acquired date (optional)
- Notes (optional)
Link magazines to a firearm#
After adding both firearms and magazines, link each magazine to the firearm(s) it fits using the compatibility mapping. The order you set is the order it appears throughout the application — in lists, in the summary, and in exports .
Bulk-add a labeled batch#
For adding many magazines at once, use the bulk-add feature. Provide a shared template (brand, model, caliber, capacity), a label prefix, and the quantity to create .
Example: prefix AR-, quantity 60 creates magazines labeled AR-01 through AR-60.
How it works :
- Labels are zero-padded:
AR-01,AR-02, ...,AR-09,AR-10. The padding width grows automatically when the sequence crosses 99 → 100. - The counter picks up where it left off. If
AR-01throughAR-30already exist and you add 10 more with the same prefix, they start atAR-31— no manual tracking required. - All magazines in a batch are created in a single transaction, so a partial failure leaves no orphaned records.
- If you leave the prefix blank, no labels are applied to the batch.
View the summary#
The Summary screen shows :
- A running total of all magazines
- Per-caliber counts — how many magazines you have in each caliber
- Per-firearm counts — how many magazines are linked to each firearm
The summary is viewer-relative: you see counts only over items you own or have been granted access to .
Export to CSV#
Use the Export function to download your inventory as a CSV file. The export includes :
| Column | Notes |
|---|---|
| Brand/Model | |
| Caliber | |
| Base Capacity | |
| Extension Rounds | |
| Effective Capacity | Computed from base + extension |
| Label | |
| Acquired Date | YYYY-MM-DD format |
| Notes | |
| Compatible Firearms | Visible-resolved firearm names in ordinal order |
Serial numbers are never included in CSV exports . This is intentional — serial numbers are treated as sensitive data and excluded from all export paths.
Formula injection protection is applied to every cell before it's written. A cell that starts with =, +, -, or @ is prefixed with an apostrophe so it doesn't execute as a formula when someone opens the file in a spreadsheet application .
Like all reads, exports are viewer-relative: you export only the items you can see .
Backups#
Everything — every firearm, magazine, compatibility link, and your account — lives in Postgres. A standard pg_dump is all you need :
docker compose exec db pg_dump -U "$POSTGRES_USER" -Fc -d "$POSTGRES_DB" > magstacker.dump
Restoring the dump brings back your entire inventory exactly as it was :
docker compose exec -T db pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" < magstacker.dump
Schedule backups with cron or your NAS's built-in scheduler. Store the dump files somewhere off the host machine — an external drive, a network share, or cloud storage.
Security Notes#
Use HTTPS#
MagStacker authenticates with session cookies. Sign-in sends your credentials over the connection, and a cookie is transmitted with every subsequent request. Do not expose the app's HTTP port directly to your home network or the internet .
For local access on a single machine (localhost only), this is low-risk. As soon as another device on your network needs to reach it, put MagStacker behind a TLS-terminating reverse proxy and update BETTER_AUTH_URL to the https:// address:
# In .env
BETTER_AUTH_URL=https://mags.home.example
Then restart the stack:
docker compose up -d
Common options for home setups :
- Caddy — automatic HTTPS with Let's Encrypt, minimal configuration.
- nginx — widely deployed, flexible.
- Traefik — integrates directly with Docker labels.
Also configure your proxy to forward the real client IP (e.g., via X-Real-IP) so the sign-in rate limiter keys on the actual client address rather than the proxy's address .
See docs/deployment.md for full production hardening details.
Protect your secrets#
secrets/better_auth_secret.txtsigns all session tokens. If you believe it's been exposed, overwrite the file with a new secret and restart theappandmigrateservices — this invalidates all active sessions .secrets/postgres_password.txtis only applied the first time the data volume is initialized. Changing this file after first run does not rotate an existing database's password — seesecrets/README.mdfor rotation notes .- Neither
secrets/nor.envshould ever be committed to version control. Both are gitignored by default.
User Management#
There is no public sign-up endpoint. All accounts are created by whoever runs the server — you — from the Accounts screen after signing in as the operator .
For personal use, you likely won't need any additional accounts. Your operator account owns all the inventory, and everything you add is visible to it.
If you want to share specific items with a family member, MagStacker supports per-item sharing at view or edit permission :
- Create an account for them from the Accounts screen.
- Share individual firearms or magazines with their account at the appropriate permission level.
- They see only what you've explicitly shared — not your full inventory.
- Revoke access at any time from the item's share settings; it takes effect on their next request.
Only the item's owner (you) can delete it, regardless of what permissions have been granted .
Next Steps#
- Deployment guide — production hardening details: TLS configuration, logging options, port configuration, and secret rotation procedures .
- Getting Started for Clubs and Ranges — if you later want to expand to multi-user access with staff, members, or volunteers, this guide covers the sharing model and role structure in depth .
- README — general project overview, feature summary, and developer setup .