Install with Docker¶
This page walks through a production-style Docker install on one host. It covers the data directory, Compose file, first admin seed, startup checks, and the mistakes that usually cause broken installs.
What you need¶
- Docker and Docker Compose installed on the host.
- A persistent host directory for BillTracker data.
- A DNS name or local URL you will use to reach the app.
- A strong admin password for first login.
- A strong
TOKEN_ENCRYPTION_KEYbefore storing bank or auth secrets.
For the examples below, the host data path is:
Adjust it if your server keeps app data somewhere else.
1. Create the data directories¶
mkdir -p /portainer/hosting/bill-tracker/data/db
mkdir -p /portainer/hosting/bill-tracker/data/backups
The container stores the SQLite database at /data/db/bills.db and managed
backups at /data/backups.
2. Generate a secret-encryption key¶
Use a long random value and keep it outside the database:
Set the output as TOKEN_ENCRYPTION_KEY. If you skip this, BillTracker still
encrypts secrets, but the encryption key is stored in the same SQLite database
as the encrypted data.
3. Write docker-compose.yml¶
This example publishes the app on host port 3030 and mounts persistent data
to /data.
services:
bill-tracker:
image: dream.scheller.ltd/null/bill-tracker:dev-v0.37.0
container_name: bill-tracker-main
restart: unless-stopped
ports:
- "3030:3000"
environment:
NODE_ENV: production
TZ: America/Chicago
# First start only. Remove or rotate these after setup.
INIT_ADMIN_USER: admin
INIT_ADMIN_PASS: replace-with-a-long-password
# Keep secrets decryptable across container replacement.
TOKEN_ENCRYPTION_KEY: replace-with-the-random-value-you-generated
# Recommended production CSRF/cookie posture.
CSRF_HTTP_ONLY: "true"
CSRF_SAME_SITE: "strict"
CSRF_SECURE: "true"
# Set these when using WebAuthn/FIDO2 hardware-key 2FA on HTTPS.
# WEBAUTHN_RP_ID: bills.example.com
# WEBAUTHN_ORIGIN: https://bills.example.com
volumes:
- /portainer/hosting/bill-tracker/data:/data
Pin deliberately
Pinning a version makes upgrades predictable. Use latest only if you
intentionally want the container to follow the newest published image.
4. Start the app¶
On a healthy start, the logs should show database migrations completing before
the web server begins listening on port 3000 inside the container.
5. Open BillTracker¶
Open the published port:
If you are working directly on the host, this is usually:
Log in with the seeded admin from INIT_ADMIN_USER and INIT_ADMIN_PASS, then
continue with First-run setup.
6. Remove or rotate the seed credentials¶
After the admin login works:
- Change the admin password in the UI.
- Create at least one regular user for bill tracking.
- Comment out
INIT_ADMIN_USERandINIT_ADMIN_PASS, or replace them with rotated emergency seed values kept in your password manager. - Restart the container.
Why remove seed values?
Seed values can update matching seeded account passwords during startup. Leaving them in Compose can surprise you later when a restart re-applies an old password.
What data must survive upgrades?¶
Keep this host directory:
At minimum, keep:
Do not store real data only inside the container filesystem. Containers are
replaceable; /data is the durable state.
How are migrations applied?¶
The Docker entrypoint runs:
before starting the server. Leave migrations enabled for normal installs.
Set RUN_DB_MIGRATIONS=false only for a deliberate recovery operation where
you are taking responsibility for migration timing.
Common install problems¶
| Symptom | Likely cause | What to check |
|---|---|---|
| Login page never loads | Port is not published or blocked | docker ps, firewall rules, reverse proxy target |
| App starts but data disappears after redeploy | /data was not mounted to a host path |
docker inspect bill-tracker-main and confirm the bind mount |
| Admin account is missing | Seed env vars were not present on first start | docker compose logs and INIT_ADMIN_* values |
| Browser rejects cookies | HTTPS/proxy headers are inconsistent | CSRF_SECURE, COOKIE_SECURE, X-Forwarded-Proto |
| WebAuthn fails | RP ID/origin do not match the public URL | WEBAUTHN_RP_ID, WEBAUTHN_ORIGIN, HTTPS |