The main sweep — ~260 call sites across ~100 files move from string
concatenation on a single root to appDir/storageAppDirs/storageAppConfigs.
On a single-root install the resolved paths are identical, so this is a
no-op until a location is registered.
Enumerators were the interesting half. `for d in "$containers_dir"/*/`
appears in the menus, the registry/artifact scanners and the DNS setup —
and a shell glob cannot list a rootless 751 tree at all, which is the
same bug config_find_file.sh already documents in a comment. Routing them
through storageAppDirs (which enumerates as the owning user) fixes that
alongside the multi-root work.
Three places needed judgement rather than substitution:
db_app_scan.sh deletes database rows and port allocations for apps whose
folder is missing, and reaps "empty" app dirs. With a storage location
unmounted, every app on it looks exactly like that. Each of those
branches now gates on appStorageAvailable first — an app on an unplugged
drive is skipped with a notice, never deleted.
instance_create.sh rewrites cloned hooks so an instance touches its own
directory instead of the base app's. Its sed matched ${containers_dir}<type>,
which this sweep just replaced with $(appDir <type>) — so it would have
silently stopped redirecting, and an instance would have written to the
original's files (the adguard auth adapter case its own comment warns
about). Now matches both appDir forms, verified against bare, quoted,
unrelated-app, legacy and prose cases.
peer_shell/peer_pull streamed and extracted relative to the primary root.
Both now use the app's own root, and peer_shell keeps a single-root
fallback since it runs as a restricted SSH shell with no LibrePortal env.
Also fixes a pre-existing bug found on the way: webui_app_config.sh
tested "$containers_dir/frontend/data/last_update", one level short of the
real tree under the libreportal app dir, so the WebUI refresh trigger
after a config update has never once fired.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 versionshould 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 .envonup— you skipped step 2 above. Create.envand setAUTH_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 downkeeps it;docker compose down -vdeletes 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.