Guide/guides

Deployment Guide

This guide summarizes the steps required to promote a freshly scaffolded Guren application to production. It assumes you generated your project with create-guren-app and that you have a PostgreSQL instance available.

Deployment Guide

This guide summarizes the steps required to promote a freshly scaffolded Guren application to production. It assumes you generated your project with create-guren-app and that you have a PostgreSQL instance available.

Production Checklist

  • Configure environment variables (DATABASE_URL, APP_URL, PORT, etc.).
  • Install dependencies with Bun in production mode.
  • Build frontend assets.
  • Run database migrations (and seed data if necessary).
  • Start the Bun server behind a process manager or container runtime.

1. Prepare Environment Variables

Create a production-specific .env (or inject variables through your hosting platform). At minimum configure:

APP_URL=https://example.com
PORT=3333
DATABASE_URL=postgres://user:password@db-host:5432/database
NODE_ENV=production

Avoid committing this file—use your platform’s secret manager instead.

rule

Treat every value in .env as sensitive. Prefer secret managers or environment variables provided by your platform so credentials never appear in git history, build logs, or container images.

2. Install Dependencies

On the deployment host:

bun install --production

This installs only the dependencies required at runtime. If your environment builds assets during deployment, you can omit --production to keep dev tooling available.

3. Build Frontend Assets

NODE_ENV=production bun run build

The scaffolded build script runs both bunx vite build and bunx vite build --ssr, producing the client manifest at public/assets/.vite/manifest.json and the SSR manifest at public/assets/.vite/ssr-manifest.json. At runtime src/main.ts calls autoConfigureInertiaAssets, which reads those files and wires the GUREN_INERTIA_* environment variables automatically.

4. Run Database Migrations (and Seeders)

NODE_ENV=production bun run db:migrate
# Optional
bun run db:seed

Run these commands on every deployment to keep the schema in sync. Seeders are optional and typically used for demo or staging data.

rule

Run migrations before the new code begins serving traffic. Rolling back partially applied migrations is messy—if a deploy fails after running them, redeploy the previous commit without re-running migrations.

5. Start the Server

You can start the Bun server directly:

NODE_ENV=production bun run bin/serve.ts

For reliability, wrap this command with a process manager (e.g. systemd, pm2, supervisord, or your hosting provider’s run command). Example systemd unit:

  • The startup banner only renders in non-production environments by default. If you want to show it (or disable it explicitly) set GUREN_DEV_BANNER=1 or GUREN_DEV_BANNER=0.
  • The framework skips launching the Vite dev server when NODE_ENV=production. If you’re running a custom dev workflow in production-like environments, toggle it with GUREN_DEV_VITE=1 (on) or GUREN_DEV_VITE=0 (off).
  • Outside production, a busy port makes the server try the next one so bun run dev keeps working. Set GUREN_STRICT_PORT=1 to bind the requested port or fail with EADDRINUSE instead. Use it anywhere a run has to know it reached the app it started — smoke scripts, E2E runners, CI — because walking to another port otherwise lets the run pass against whatever was already listening.
  • The HTTP server's own teardown is already wired: listen() closes the socket on SIGINT, SIGTERM, and process exit, which is what a process manager or container runtime sends to stop the service. Anything else your app runs still needs its own shutdown handler — a scheduler, a queue worker, or a store holding timers, as Scheduling, Queue, and Rate Limiting show. Call app.stop() when application code, rather than the process ending, decides when the server stops.
[Unit]
Description=Guren Application
After=network.target

[Service]
EnvironmentFile=/etc/guren/my-app.env
WorkingDirectory=/var/www/my-app
ExecStart=/usr/local/bin/bun run bin/serve.ts
Restart=always

[Install]
WantedBy=multi-user.target

Reload systemd, enable the service, and start it with sudo systemctl enable --now my-app.

Container Deployment Example

FROM oven/bun:1 AS base
WORKDIR /app

COPY bun.lock package.json ./
RUN bun install --production

COPY . .
RUN NODE_ENV=production bun run build

EXPOSE 3333
ENV NODE_ENV=production
CMD ["bun", "run", "bin/serve.ts"]

Build and run:

docker build -t my-app .
docker run --env-file .env.prod -p 3333:3333 my-app

The container image bakes in both the client and SSR bundles so the server can stream pre-rendered HTML immediately.

Mount your configuration or secrets as needed for your hosting environment.

AWS Lambda (Serverless)

Guren runs on AWS Lambda's Node.js runtime — ideal for variable traffic or minimal infrastructure management. The official plugin handles bundling and ships a CDK construct for the infrastructure:

bunx guren plugin @guren/plugin-lambda
bun add @guren/plugin-lambda

The CLI scaffolds src/lambda.ts (whose exports become your Lambda handlers) and registers a lambda:build command:

bunx guren lambda:build

The build assembles a .lambda/ directory: a self-contained function bundle, static assets staged for S3, and the environment the function expects. CloudFront serves those staged files ahead of the function, so the CDK construct puts a viewer-response function on the asset behaviors that gives the types a browser renders as a document — .html, .htm, .svg, .xhtml, .xmlContent-Disposition: attachment and X-Content-Type-Options: nosniff, matching what the framework does for public/ when it serves those files itself. The framework provides dedicated handlers for HTTP, SQS queues, EventBridge scheduling, and CLI commands. See the Serverless Deployment Guide for the full setup including the database, SSR, and CDK deployment.

Vercel (Serverless)

SSR apps can deploy to Vercel using the official plugin. The plugin assembles a Build Output API directory that runs on Vercel's Bun runtime.

bunx guren plugin @guren/plugin-vercel
bun add @guren/plugin-vercel

The CLI scaffolds src/vercel.ts, scripts/vercel-build.ts, and vercel.json for you. Build and deploy:

bun run vercel:build
vercel deploy --prebuilt

Before writing any output, vercel:build runs the deploy-runtime checks guren doctor reports and warns, without failing, on an in-memory session or OAuth store, a ScryptHasher, or filesystem provider discovery — each works locally and breaks across function invocations.

note

The plugin targets SSR apps only. It reads Vite manifests to inject the correct GUREN_INERTIA_* environment variables into the serverless function. API-only apps should use Docker or Lambda instead.

public/ is copied to .vercel/output/static, which the CDN serves ahead of the function. The generated config.json therefore carries a route that gives the types a browser renders as a document — .html, .htm, .svg, .xhtml, .xmlContent-Disposition: attachment and X-Content-Type-Options: nosniff, matching what the framework does for public/ when it serves those files itself. It sits after handle: "hit", so it applies only to files the CDN answered and never to a path your function serves, such as a dynamic /sitemap.xml.

Cloudflare Workers

Apps can run on Cloudflare Workers with D1 as the database, via the official plugin.

bunx guren plugin @guren/plugin-cloudflare
bun add @guren/plugin-cloudflare
bunx guren cloudflare:build
bunx wrangler deploy

Workers has no filesystem and shares no memory between requests, so sessions and OAuth state must be database-backed, and migrations are applied out of band. See the Cloudflare Workers Deployment Guide for the full setup including D1, secrets, free-plan limits, and local development.

Post-Deployment Tasks

  • Follow the Production Operations Runbook for SLO, incident response, and backup/restore drill policy.
  • Set up HTTPS (e.g. via a reverse proxy such as Nginx, Caddy, or your cloud platform).
  • Configure logging and monitoring—Bun prints to stdout/stderr, so ship logs to your chosen aggregator.
  • Schedule automated backups for the PostgreSQL database.
  • Implement health checks (e.g. expose /health from a registerHealthRoutes(router) registrar via router.get('/health', (ctx) => ctx.json({ ok: true }))) and wire them into your load balancer.

Following this checklist ensures each release is reproducible, migrates the database safely, and keeps your application responsive in production.