Guide/guides

Deploy to Production

This guide covers the essential steps to take a Guren application from local development to a production environment. It focuses on Docker-based deployment with Bun.

Deploy to Production

This guide covers the essential steps to take a Guren application from local development to a production environment. It focuses on Docker-based deployment with Bun.

Note

For serverless deployment on AWS Lambda, see the Serverless Guide. For the full deployment reference, see Deployment.

Pre-Deploy Checklist

Run these commands before every deployment to catch issues early:

# Build frontend and backend
bun run build

# Type check the entire project
bun run typecheck

# Run the full test suite
bun run test

# Validate route, controller, and page consistency
bunx guren doctor

Fix any errors before proceeding. The doctor command catches mismatches between routes, controllers, and pages that could cause runtime failures.

1. Configure Environment Variables

Your production environment needs these variables at minimum:

Variable Example Purpose
APP_URL https://example.com Public-facing URL
APP_ENV production Enables production optimizations
PORT 3333 Server listen port
DATABASE_URL postgres://user:pass@host:5432/db Postgres connection string
SESSION_SECRET (random 64-char string) Signs session cookies

Warning

Never commit secrets to git. Use your platform's secret manager or inject variables at deploy time.

Generate a session secret with:

openssl rand -hex 32

2. Write a Dockerfile

Create a Dockerfile in your project root:

FROM oven/bun:1 AS base
WORKDIR /app

# Install dependencies
FROM base AS deps
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production

# Build the application
FROM base AS build
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build

# Production image
FROM base AS production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/public ./public
COPY --from=build /app/package.json ./

ENV APP_ENV=production
ENV PORT=3333
EXPOSE 3333

CMD ["bun", "run", "start"]

Build and test locally:

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

3. Run Database Migrations

Run migrations as part of your deployment pipeline, not inside the Dockerfile. This avoids running migrations on every container start:

# In your CI/CD pipeline or deployment script
bunx guren db:migrate --force

The --force flag suppresses the confirmation prompt in production.

4. Set Up a Health Check

Add a health check endpoint to your routes so load balancers and container orchestrators can verify your app is running:

import { Router } from '@guren/core'

export function registerWebRoutes(router: Router): void {
  router.get('/health', (c) => {
    return c.json({ status: 'ok' })
  })

  // ... your other routes
}

For a more thorough check that also verifies the database connection, use the built-in health check system. See Health Checks for details.

5. Configure Docker Compose for Production

For single-server deployments, a docker-compose.production.yml keeps things manageable:

services:
  app:
    build: .
    ports:
      - "3333:3333"
    env_file: .env.production
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3333/health"]
      interval: 30s
      timeout: 5s
      retries: 3

  postgres:
    image: postgres:17
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  pgdata:

Deploy with:

docker compose -f docker-compose.production.yml up -d

6. Post-Deploy Verification

After deployment, verify everything is working:

# Check the health endpoint
curl https://example.com/health
# Expected: {"status":"ok"}

# Verify a page loads
curl -I https://example.com
# Expected: HTTP/2 200

# Check logs for errors
docker compose -f docker-compose.production.yml logs app --tail 50

Production Hardening

Once your app is deployed and running, consider these additional steps:

  • Reverse proxy — place Nginx or Caddy in front of Bun for TLS termination and static asset serving
  • HSTSStrict-Transport-Security: max-age=31536000 is sent automatically when NODE_ENV=production. Add includeSubDomains/preload via securityHeaders: { hsts: { ... } } in createApp, or disable with hsts: false if you serve plain HTTP internally
  • Process monitoring — use restart: unless-stopped in Docker or a process manager like systemd
  • Logging — configure structured logging and ship logs to a centralized service
  • Backups — schedule regular Postgres backups with pg_dump or a managed database service

Next Steps