Business panel for a 3D-printing / manufacturing operation: product, material and component catalog, production jobs with timers and printers, orders, invoices and quotations with PDF generation, POS with cash sessions, purchasing and supplier payments (AP), AR aging, stock takes, and InfluxDB-backed dashboards.
| Layer | Technology |
|---|---|
| Backend | AdonisJS v7 (Node ≥ 24), session auth + Bouncer authorization |
| Frontend | Inertia.js + React 19, Vite, Tailwind v4, shadcn (served by the same AdonisJS process) |
| Database | PostgreSQL 17 (Lucid ORM) |
| Cache / sessions | Redis 7 |
| File storage | MinIO (S3-compatible) — internal only, never exposed |
| Metrics | InfluxDB 2 — dashboard trend charts, fed by a daily snapshot job |
| Runtime | Docker Compose, single exposed port |
Only one endpoint is ever exposed: the AdonisJS app itself. Postgres, Redis, MinIO, and InfluxDB live on an internal Docker network with no published host ports. Every image, file, and PDF is streamed through the app with session authentication and per-resource permission checks — the browser never talks to MinIO, and no presigned URLs exist.
Internet / your network
|
| (TLS / domain / proxy — your choice,
| out of scope of this stack)
v
host port APP_BIND (default 127.0.0.1:3333)
|
==================== docker: single published port =====================
|
v
+------------------+
| AdonisJS app |
| (Inertia+React) |
| port 3333 |
+--+----+----+---+-+
| | | |
sessions/cache -> | | | | <- metrics (write: daily
+---------+ | | | | snapshot job, read: charts)
| Redis | <------+ | | +-------------+
| :6379 | | | |
+---------+ | | v
| | +------------+
app data -> | | | InfluxDB |
+------------+ | | | :8086 |
| Postgres | <--------+ | +------------+
| :5432 | |
+------------+ | <- S3 API (uploads + streaming
| downloads, auth-gated by the app)
+-------------+
| MinIO |
| :9000/:9001 |
+-------------+
------- internal `backend` network: NO published host ports -------
File serving flow (all routes require a session + the resource's *.view permission):
browser --GET /catalog/products/42/image?v=<key>--> app
app --bouncer.authorize('products.view')--------> allowed?
app --drive.getStream(imageKey)-----------------> MinIO (internal)
app <--object bytes------------------------------ MinIO
browser <--streamed response + Cache-Control-------- app
Images get private, max-age=1y, immutable (safe: the ?v= token changes whenever an image is replaced, because storage keys are unique per upload). PDFs and file downloads get private, no-store.
- Linux server with Docker Engine + the Compose v2 plugin
- Your user able to run docker (
sudo usermod -aG docker $USER, then re-login) git(for theupdatecommand)openssl(for./deploy.sh initsecret generation — preinstalled on virtually every distro)
deploy.sh checks all of this for you and fails with the exact remediation if something is missing.
git clone <repo-url> Business ERP-panel && cd Business ERP-panel
./deploy.sh init # generate .env: all secrets auto-created (openssl),
# prompts only for APP_URL and APP_BIND
./deploy.sh # build → pending migrations/seed → start → health checkinit auto-generates APP_KEY, the Postgres/Redis/MinIO passwords, and the InfluxDB token; it asks only for the two values it can't guess (APP_URL, APP_BIND) and writes .env with 600 permissions. It refuses to overwrite an existing .env. Running ./deploy.sh without a .env offers to run init for you. Prefer manual control? cp .env.production.example .env and fill it in yourself — init is optional.
Back up the generated
.env.APP_KEYandINFLUX_TOKENcannot be regenerated later without invalidating sessions/metrics.RESEND_API_KEY(outbound mail) is the one valueinitleaves empty.
Key values in .env:
| Variable | Meaning |
|---|---|
APP_KEY |
Session/encryption secret. ./deploy.sh init generates it (or node ace generate:key / openssl rand -base64 32); never rotate |
APP_URL |
Public URL users visit. No domain required — http://192.168.1.50:3333 is fine for a LAN deploy. The scheme matters: https:// turns on Secure cookies, HSTS, and upgrade-insecure-requests; http:// keeps them off so the app works over plain HTTP |
APP_BIND |
Host interface:port the app publishes. Default 127.0.0.1:3333 (loopback only). Set 0.0.0.0:3333 or SERVER_IP:3333 to expose more widely. This is the only exposed port in the stack. |
DB_HOST / REDIS_HOST |
Must stay postgres / redis (compose service names) — deploy.sh rejects 127.0.0.1 |
MINIO_ROOT_USER/PASSWORD, AWS_*, S3_BUCKET |
MinIO credentials; bucket is created automatically on first boot |
INFLUX_TOKEN |
Minted as the InfluxDB admin token on first boot only — keep it stable forever; changing it later will not re-key the existing volume |
INFLUX_INIT_USERNAME/PASSWORD |
First-boot InfluxDB admin credentials |
MIGRATE / SEED |
Keep false; deploy.sh runs controlled migrations and production-safe seeders |
S3_ENDPOINT and INFLUX_URL are intentionally not in .env — they are pinned inside docker-compose.prod.yml to the internal hostnames (http://minio:9000, http://influxdb:8086) so they can never accidentally point at a public host.
./deploy.sh init # generate .env (secrets auto-created; see above)
./deploy.sh # build + migrate pending files + seed + verify
./deploy.sh update # fast-forward Git, re-exec latest script, deploy
./deploy.sh backup # verified PostgreSQL custom dump + protected .env copy
./deploy.sh build # build the app image only
./deploy.sh up # start the stack only
./deploy.sh migrate # migrate pending files + safe seed + restart/verify
./deploy.sh migrate-only # pending migrations only; no backup or seeders
./deploy.sh logs # tail app logs
./deploy.sh ps # stack status
./deploy.sh pull # git fetch + fast-forward only (no rebuild)
./deploy.sh down # stop the stack (volumes/data kept)
./deploy.sh nuke # stop + DELETE all volumes (destroys DB!) — asks for confirmationdeploy.sh pull and deploy.sh update always fast-forward main. ENV_FILE=path/to/other.env ./deploy.sh … overrides the env file. Set BACKUP_DIR=/secure/path to store recovery archives outside the checkout and HEALTH_TIMEOUT_SECONDS=… to change the default 90-second health deadline.
Deployments are backup-free by default. To create and verify a database backup before a particular release, use WITH_BACKUP=true ./deploy.sh update. For schema changes only against the currently built image, use ./deploy.sh migrate-only; it applies only migrations still pending in adonis_schema, skips every seeder, restarts the app, and verifies health. Default deployments retain application-image rollback but cannot restore the database, so use backward-compatible migrations.
On a server that predates the safe pipeline, first run ./deploy.sh pull, then ./deploy.sh update. This ensures the newly pulled script—not the already-running old script—controls the first migration-safe release.
The stack deliberately stops at the published port. Point whatever you like at APP_BIND — a reverse proxy with TLS, a tunnel, or direct exposure via APP_BIND=0.0.0.0:3333. Nothing else (MinIO, Postgres, Redis, InfluxDB) is reachable from outside Docker, so there is nothing else to protect.
On Windows, the development helper runs the infrastructure in Docker Desktop and the application directly through the locally installed Node.js/npm:
.\dev.ps1 setup # first run: install, start infrastructure, migrate, seed, run app
.\dev.ps1 # later runs: start infrastructure and run app with HMR
.\dev.ps1 down # stop infrastructure without deleting development dataIf local PowerShell scripts are disabled, use
powershell -ExecutionPolicy Bypass -File .\dev.ps1 setup.
The cross-platform manual equivalent is:
docker compose -f docker-compose.dev.yml up -d # postgres/redis/minio/influx with dev creds
cp .env.dev.example .env # match the dev creds
npm install
node ace migration:run && node ace db:seed
npm run dev # HMR backend + Vite frontendTests: npm test (Japa: unit / functional / browser suites). Lint/typecheck: npm run lint, npm run typecheck.