Deployment
This guide covers shipping a JCC Express MVC application to production: environment, builds, database, process management, Inertia SSR (when you use React/Vue with server-side rendering), and queues. Hosting details (Docker, PaaS, VPS) vary by provider; the steps below are what almost every deploy shares.
What you need on the server
- Bun (recommended runtime, same as local development).
- Node.js (optional but common) if you use npm for installs or run
nodefor SSR scripts. - A database reachable from the app (MySQL, PostgreSQL, SQLite for small setups, etc.).
- A reverse proxy (nginx, Caddy, Traefik, or your cloud load balancer) terminating HTTPS and forwarding to the app’s
PORT.
Production environment (.env)
Never commit real .env files. On the server, create .env from .env.example and set at least:
APP_ENV=productionAPP_DEBUG=falseAPP_URL=https://your-domain.example(full URL with scheme; used for links, cookies, OAuth redirects)PORT— Often set by the platform; otherwise choose a non-privileged port the proxy targets.- Database —
DB_*(or Mongo / Sequelize keys as your app uses). JWT_SECRET— Strong, unique value; runbun artisanNode key:generateon the server if you are not copying a known secret from a vault.- Mail, OAuth, Redis, queue — Fill only what you use; see
.env.example.
Restart the app process after any .env change.
Build steps
Order matters for typical Inertia + Vite apps.
1. Frontend assets (Inertia / Vite)
Produces public/build (and SSR bundles when configured):
(Typical vite-build script: vite build && vite build --ssr — check package.json in your app.)
2. TypeScript compile (optional but common)
Emits JavaScript under ./build per tsconfig.json:
Some teams skip tsc in production and run the HTTP entry directly; choose one approach and stay consistent.
3. Framework build command
bun artisanNode build copies public, filtered resources, and SSR bootstrap bits into build/ alongside your compiled output—use it when your deployment layout expects those files under build/. It does not replace tsc or vite-build.
Database
Run migrations on the server (or in CI before traffic):
Avoid migrate:fresh on production unless you intend to wipe data. Seed only when appropriate:
Inertia SSR in production (React / Vue)
Skip this section if ssr is off in app/Http/kernel.ts.
When Inertia is used with React or Vue and SSR is enabled (for example inertia({ rootView: "welcome", ssr: true })), the HTML for the first load is rendered by a separate SSR process. That process must be running and listening before you start the main HTTP server: the Inertia middleware calls it during requests, and starting the app first can cause failed or empty SSR until the SSR service comes up.
Complete Build steps and Database above, then use this start order:
bun artisanNode inertia:start-ssr— start first and keep it running.- Main HTTP app — start only after SSR is up. On apps scaffolded with
jcc-express-starter,package.jsonincludes astartscript for production; usebun run startornpm run start(notdev, which uses watch). If you have nostartscript, run your entry directly (for examplebun server.tsor the compiledbuild/server.js).
Manual example (two terminals or a script):
PM2 — register SSR before the app so it starts first (or use pm2 start order / ecosystem file with inertia-ssr listed before jcc-app):
On restarts, bring inertia-ssr up before jcc-app (or rely on PM2’s start order in an ecosystem config).
Optional: ssr:server script
Some setups use the npm script that runs the compiled SSR entry (for example node bootstrap/ssr/ssr.mjs):
If your stack expects this instead of or in addition to inertia:start-ssr, apply the same rule: start that SSR process before the main server. See docs/docs-for-dev.md for your version.
Ports and configuration
The SSR server listens on a port your middleware expects. Keep that port open between the SSR process and the main app (localhost, Docker network, or firewall). See the main developer guide for env vars and URLs.
Run the HTTP application
If you use Inertia SSR: start bun artisanNode inertia:start-ssr (or your documented SSR command) first, as in the previous section—then start the HTTP server below.
If you do not use SSR: you only need the main server.
Scaffolded apps (jcc-express-starter) — package.json defines a start script for production (no file watcher). From the project root:
Other / custom setups — run whatever your start script maps to, or invoke the entry yourself:
Compiled deploy (after tsc):
Do not use dev (or any --watch script) in production. Put each long-running process under systemd, PM2, Docker, or your platform’s supervisor so it restarts on failure.
Behind a reverse proxy, configure trusted proxies / X-Forwarded-* if your framework or Express app expects correct client IPs and https URLs (see docs/docs-for-dev.md for your version).
Queues and scheduled work
If you use database or Redis queues, run workers in production:
Use a process manager so workers stay up, for example:
Schedule bun artisanNode schedule:run from cron or a scheduler if your app uses the task scheduler.
Checklist before going live
APP_ENV=production,APP_DEBUG=false, correctAPP_URL(HTTPS).- Secrets (
JWT_SECRET, session, mail, OAuth) are production values, not placeholders. vite-build(andbuild/tscif you rely on emitted JS) completed successfully.bun artisanNode migrateapplied on the target database.- If using Inertia SSR:
bun artisanNode inertia:start-ssr(or your documented SSR script) is running before the main app (bun run start/npm run starton scaffolded projects, or your equivalent). - TLS certificates and proxy timeouts are configured.
- Backups and log aggregation are in place for your org.
