Part A — Round 3 regressions: - A.1: verify-cost-reconciliation.mjs now loads .env.local, wraps in try/catch, exits 1 on any failure - A.2: Vehicle Costs restored Santam — back to R 14,996.31 (matches row-by-row sum) - A.3: incl<excl / VAT math / VAT>20% now FATAL — seed exits 2 with full report (4 anomalies on Charl-CCS + Buildsmart) - A.4: Reconciliation target R 694,962.93 with both deltas itemised (Personnel +R19,800, Santam -R5,322.44, unexplained R 0.00) - A.5: 'Lines' view uses actual lines (recharts LineChart), no fills; fills only in 'Cost stack' Part B — Mobile standard: - B.1: header wraps to 2 rows on mobile; YTD badge cards stack; page-wide overflow eliminated on /income, /charts, /overheads, /running, /expenses - B.2: IncomeView card row layout stacked — name gets its own line on mobile, never truncated to single char - B.3: Plant type filter is a <select> dropdown below lg (was 18 pills wrapping to 13 rows); same for sort on /income and /running - B.4: Plant table first column sticky; -webkit-overflow-scrolling: touch; tables scroll inside their own container - B.5: All buttons/links use min-h-[44px] tap targets; text below 12px is bumped where it matters - B.6: Charts legend wraps below title; chart min-height h-44 → h-52 on mobile - B.7: paddingTop env(safe-area-inset-top) on sticky header for notch/home indicator clearance AGENTS.md created with the standing mobile checklist rule.
3.6 KiB
Deploying Mega Plant to Coolify
The one thing that catches everyone
NEXT_PUBLIC_PB_URL must be set as a Build Argument, not only as an
environment variable.
Next.js compiles NEXT_PUBLIC_* values into the JavaScript the browser
downloads. That happens at npm run build, inside the Docker build. Setting the
value only as a runtime env var means the server sees the right URL while the
browser bundle still contains whatever was baked in at build time.
The Dockerfile fails the build with an explicit message if the arg is missing, so you'll find out immediately rather than after a confusing deploy.
Setup
1. Create the application
Coolify → Project → New Resource → Application → point at this repository. Build Pack: Dockerfile. Port: 3000.
2. Build Arguments (Build → Build Arguments)
| Name | Value |
|---|---|
NEXT_PUBLIC_PB_URL |
https://pb.mega.plant.example |
3. Environment Variables (runtime — Environment Variables tab)
| Name | Value | Notes |
|---|---|---|
NEXT_PUBLIC_PB_URL |
same as above | server-side reads use it too |
PB_ADMIN |
the PocketBase superuser email | server-only, never exposed |
PB_PASS |
the superuser password | server-only, never exposed |
PORT |
3000 |
optional, defaults to 3000 |
4. Health check
Coolify → Health Checks → path /api/health, port 3000. The Dockerfile also
declares its own HEALTHCHECK against the same endpoint.
The probe deliberately does not touch PocketBase — it answers "is the web server up?". If it also checked the database, a brief PocketBase outage would make Coolify restart a perfectly healthy container.
5. Domain and TLS
Set the FQDN in Coolify and let Traefik issue the certificate. HTTPS is required — the PWA will not install over plain HTTP, and the service worker won't register.
PocketBase must be on HTTPS too
Server-side rendering reaches PocketBase from inside the container, so an
http:// URL works there. But the moment any browser-side code calls
PocketBase from an HTTPS page, the request is blocked as mixed content.
Put PocketBase behind TLS. If it's another Coolify resource, give it a domain and let Traefik handle the certificate, the same as this app.
Before the first deploy
- Rotate the PocketBase superuser password. The old one
(
plant@megainfra.co.za/1234567890) was committed to this repository in an early version ofscripts/seed.mjs. Treat it as compromised. - Confirm
.env.localis not committed (it's in.gitignore). - Run
npm run typecheckandnpm run buildlocally first — a Docker build failure costs several minutes to discover.
Test the image locally
docker build --build-arg NEXT_PUBLIC_PB_URL=https://pb.mega.plant.example -t mega-plant .
docker run --rm -p 3000:3000 -e PB_ADMIN=you@example.com -e PB_PASS=secret mega-plant
Then check the health endpoint:
curl -s localhost:3000/api/health
Notes
--webpackis not optional.@ducanh2912/next-pwais Webpack-only and generatespublic/sw.js.package.jsonalready passes the flag inbuild; don't remove it or the PWA silently stops working.data/source/is excluded from the image (~2MB of workbook and extracted JSON). It's only needed bynpm run seed, which is a one-off you run locally against PocketBase — not something the container does.- Seeding is not part of deployment. The container never writes to
PocketBase on boot. Run
npm run seeddeliberately, from your machine. - The image runs as a non-root
nextjsuser on Node 20 Alpine.