librelad 65167463f9 fix(chat apps): tag every service so its IP actually substitutes
Installing rocketchat failed with

    invalid IPv4 address: ParseAddr("IP_DATA_2"): unable to parse IP

ipUpdateComposeTags allocates one IP per SERVICE_TAG_N annotation and fills
IP_TAG_i only where SERVICE_TAG_i exists. The four new apps tagged only their
primary service, so every sidecar — matrix's postgres, mattermost's postgres,
rocketchat's mongo, and fifteen of stoat's sixteen — kept a literal IP_DATA_n
in the deployed compose and docker refused to create the container.

Tag every service that carries an ipv4_address, index-aligned with its IP_TAG.
For stoat that also meant moving caddy from SERVICE_TAG_1 to _6 so the indices
line up with the IPs rather than the reading order.

mastodon had the same latent break (IP_TAG_2 and _3 untagged) and is fixed the
same way — it would have failed on first install for the same reason.

SERVICE_TAG carries the compose *key*, not container_name: 'libreportal app
restart <app> <service>' passes it to 'docker compose restart', which only
understands keys.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:48:09 +01:00
..

MoneyApp

Self-hosted money management for one or more users in a household. Recurring rules, projected ledger, budgets, savings goals + compound-interest calculator, and a 12-month forecast with what-if sliders.

Stack: Next.js 15 + SQLite (Drizzle ORM) + Auth.js. Runs as a single Docker container.

Requirements

  • Docker 20.10+ with Compose v2 (docker compose version should print v2.x or v5.x)
  • Port 3000 free on the host

Install

# 1. From inside the MoneyApp folder, create the environment file
cp .env.example .env

# 2. Edit .env and set AUTH_SECRET to a random 32-byte string.
#    Generate one with:
echo "AUTH_SECRET=$(openssl rand -base64 32)" >> .env

# 3. Build and start
docker compose up -d --build

The first build takes 3-5 minutes (it compiles better-sqlite3 natively and runs next build inside the container). Subsequent starts are instant.

Open http://localhost:3000 and sign up. The first account becomes the household owner.

Daily commands

What Command
Start docker compose up -d
Stop docker compose down (data preserved)
Logs docker compose logs -f
Restart after pulling new code docker compose up -d --build
Shell inside container docker compose exec moneyapp sh

Updating

Replace the source folder with the new version (keep your .env and the moneyapp-data Docker volume), then:

docker compose up -d --build

Migrations run automatically on container start.

Backup

The entire database is one SQLite file at /data/moneyapp.db inside the container, mounted from the named volume moneyapp-data.

# Snapshot
docker compose exec moneyapp sh -c 'cat /data/moneyapp.db' > backup-$(date +%F).db

# Restore (with the app stopped)
docker compose down
docker run --rm -v moneyapp_moneyapp-data:/data -v "$PWD":/host alpine \
  sh -c 'cp /host/backup-2026-05-07.db /data/moneyapp.db'
docker compose up -d

Accessing from other devices on the LAN

By default the container listens on 0.0.0.0:3000, so from the same network just use the host's IP: http://<host-ip>:3000. For HTTPS or remote access, put a reverse proxy (Caddy, Traefik, nginx) in front.

If you do put it behind a domain, update AUTH_URL in .env to the public URL — Auth.js uses it to construct callback URLs.

Troubleshooting

  • set AUTH_SECRET in .env on up — you skipped step 2 above. Create .env and set AUTH_SECRET.
  • Port 3000 already in use — stop whatever else is on 3000, or change the host-side port in docker-compose.yml (e.g. "3001:3000").
  • Database looks empty after a rebuild — that means the volume was removed. docker compose down keeps it; docker compose down -v deletes it. Restore from a backup if needed.

Project layout

MoneyApp/
├── Dockerfile           multi-stage build (deps → build → runtime)
├── docker-compose.yml   service + volume definition
├── .dockerignore
├── .env.example         template; copy to .env and fill in
├── README.md
└── app/                 the Next.js project
    ├── src/             application code
    ├── drizzle/         generated SQL migrations
    ├── public/          static assets
    ├── package.json + lock
    └── *.ts/.mjs        build configs (tsconfig, tailwind, postcss, etc.)

The deployment files stay at the root so it's clear what the installer interacts with. Everything inside app/ is the application itself — the Dockerfile copies it in during the build.