Skip to content

Deployment Strategies ​

Deploy your IGNIS application reliably, securely, and efficiently.

1. Building for Production ​

Compile the application to one executable before deploying:

bash
bun run compile

What this does: bun .configs/builders/compile.ts, which bundles src/index.ts with BunCompiler, registers the artifacts plugin, bakes in the build stamp and type-checks first (check: true): a type error stops the build. See Stamping a build.

Output: dist/bin, a standalone binary for bun-linux-x64. Set BUN_TARGET to build for another target, such as bun-linux-x64-musl for Alpine. bun run clean (devz clean) removes dist/.

2. Environment Configuration ​

Use environment variables for all configuration - never hard-code.

Production Environment Variables:

VariableValuePurpose
NODE_ENVproductionEnables performance optimizations. Error responses are fail-closed: an unset or unrecognized value is treated as production
APP_ENV_APPLICATION_SECRETStrong random stringApplication secret
APP_ENV_JWT_SECRETStrong random stringJWT signing key
APP_ENV_POSTGRES_HOST / _PORT / _USERNAME / _PASSWORD / _DATABASEProduction DB credentialsDatabase connection
APP_ENV_SERVER_HOST0.0.0.0Accept connections from any IP
APP_ENV_SERVER_PORT3000 or cloud-assignedServer port (PORT is also honoured)
APP_ENV_LOGGER_LEVELinfoLogger floor - debug | info | warn | error | emerg
APP_ENV_LOGGER_FOLDER_PATHWritable pathOpt-in rotating file output; unset means console only

WARNING

Leave APP_ENV_LOGGER_DO_REDACT unset in production. Setting it to false disables secret-key redaction in logs - a local-debugging switch only.

Where to store:

  • Docker: Use environment variables in docker-compose.yml or secrets
  • Kubernetes: ConfigMaps and Secrets
  • Cloud Platforms: AWS Secrets Manager, Azure Key Vault, Google Secret Manager

3. Deployment Methods ​

An app created by vulcan add --preset ignis --app <name> already has its Dockerfile, in .configs/builders/Dockerfile. Build it from the project root, and pass the commit:

bash
docker build -f services/api/.configs/builders/Dockerfile -t api \
  --build-arg APP_BUILD_COMMIT="$(git rev-parse HEAD)" \
  --build-arg APP_BUILD_BRANCH="$(git rev-parse --abbrev-ref HEAD)" .

docker run -p 3000:3000 -e NODE_ENV=production -e APP_ENV_APPLICATION_SECRET=xxx api

The commit is required: the root .dockerignore leaves .git out, so the build stamp cannot read it inside the image. A build without APP_BUILD_COMMIT stops with the command to run. The commit also lands in the image's org.opencontainers.image.revision label, so docker inspect shows it.

StageWhat it does
builderoven/bun:1.4.3-alpine: copies the whole project, bun install --frozen-lockfile --ignore-scripts, then bun run compile for bun-linux-<arch>-musl
productionalpine:3.21 with the binary, libstdc++ and libgcc, run as a non-root user on the app's port
Build argumentMeaning
APP_BUILD_COMMITRequired: a hex commit sha of 7 characters or more. The build stamp and the image label carry it. The literal HEAD that git rev-parse HEAD prints before a first commit is refused
APP_BUILD_BRANCH, APP_BUILD_VERSIONOptional. Read by the build stamp like the commit
BUN_CONFIG_REGISTRYOptional. A private npm mirror for bun install

WARNING

The build copies the whole project, so a .env in the context would put its secrets into the builder layer, and a CI layer cache can keep that layer. The root .dockerignore must exclude them:

**/.env
**/.env.*
!**/.env.example

vulcan init writes these lines from VULCAN 0.3.7 on, and vulcan doctor warns when they are missing. The Dockerfile also refuses a context that still holds a .env file and names it. Pass a private registry through BUN_CONFIG_REGISTRY; never copy a token file such as .npmrc into the image.

The build context is the whole project, trimmed by the root .dockerignore that vulcan init writes, because the preset cannot know which workspace packages the app imports. For better layer caching, copy only the package.json files and folders the app needs, then install.

Docker Compose (Full Stack) ​

For complete development and staging environments with database:

docker-compose.yml:

yaml
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - APP_ENV_APPLICATION_SECRET=${APP_SECRET}
      - APP_ENV_JWT_SECRET=${JWT_SECRET}
      - APP_ENV_POSTGRES_HOST=db
      - APP_ENV_POSTGRES_PORT=5432
      - APP_ENV_POSTGRES_USERNAME=ignis
      - APP_ENV_POSTGRES_PASSWORD=${DB_PASSWORD}
      - APP_ENV_POSTGRES_DATABASE=ignis_db
      - APP_ENV_REDIS_HOST=redis
      - APP_ENV_REDIS_PORT=6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=ignis
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=ignis_db
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ignis -d ignis_db"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:

Deploy with docker-compose:

bash
# Create .env file with secrets
echo "APP_SECRET=$(openssl rand -base64 32)" > .env
echo "JWT_SECRET=$(openssl rand -base64 32)" >> .env
echo "DB_PASSWORD=$(openssl rand -base64 24)" >> .env

# Start all services
docker-compose up -d

# View logs
docker-compose logs -f app

# Scale app (for load testing)
docker-compose up -d --scale app=3

Bun Single Executable (Without Docker) ​

bun run compile already produced a single standalone binary at dist/bin. Build for another platform with BUN_TARGET:

bash
BUN_TARGET=bun-linux-arm64 bun run compile

Pros: No dependencies needed on server, simple deployment Cons: Platform-specific, newer technology (test thoroughly)

IMPORTANT

A compiled binary MUST register a logger provider explicitly. Logger providers are sub-path exports and winston is an optional peer, so the default provider is reached through a runtime require that the bundler cannot see. Without this line the binary throws on its first log call.

typescript
// before the application is built
import { LoggerFactory } from '@venizia/ignis-helpers';
import { WinstonLogger } from '@venizia/ignis-helpers/winston';
// or: import { PinoLogger } from '@venizia/ignis-helpers/pino';

LoggerFactory.use({ provider: WinstonLogger });

The same rule applies to any optional peer: only a value import of the class carries it into the bundle. Naming a driver class in @datasource({ driver: NodePostgresDriver }) is what pulls pg in.

A peer a helper loads by name - ioredis for the Redis helpers, bullmq, socket.io-client, a mail transport's library - is not in the binary either, and the first client throws [ModuleUtility.loadSync] ioredis is required. Import it statically and pass it as the helper's module option, or register it once at the entrypoint. See Compiled binaries.

Deploy:

bash
# Copy executable and .env to server
scp dist/bin .env user@server:/app/

# Run
./bin

4. Production Best Practices ​

Use a process manager:

  • systemd - Linux service management (recommended for Bun)
  • Docker/Kubernetes - Built-in orchestration
  • PM2 - Alternative option

Example with systemd (Recommended for Bun):

ini
# /etc/systemd/system/my-app.service
[Unit]
Description=My IGNIS Application
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/app
ExecStart=/app/bin
Restart=on-failure
RestartSec=10
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target
bash
# Enable and start the service
sudo systemctl enable my-app
sudo systemctl start my-app
sudo systemctl status my-app

Example with PM2 (Alternative):

bash
pm2 start ./bin --name my-app
pm2 save
pm2 startup

Health checks: Add health check endpoint for load balancers:

typescript
// In application.ts
this.component(HealthCheckComponent);

Access at /health (default path, configurable) for liveness/readiness probes.

5. Build Debugging ​

When builds fail or behave unexpectedly, use TypeScript's extended diagnostics:

bash
# Show detailed build timing and statistics
tsc -p tsconfig.json --extendedDiagnostics

Output includes:

  • File I/O time
  • Parse time
  • Program time
  • Bind time
  • Check time
  • Emit time
  • Total time

Example output:

Files:              245
Lines:            89432
Nodes:           412856
Identifiers:     156234
Symbols:          98765
Types:            34567
Instantiations:  123456
Memory used:    245678K
I/O Read time:     0.23s
Parse time:        1.45s
Program time:      2.34s
Bind time:         0.56s
Check time:        4.23s
Emit time:         0.89s
Total time:        8.47s

Common issues:

  • High "Check time" → Type errors or complex type inference
  • High "I/O Read time" → Too many files, check include/exclude patterns
  • High "Instantiations" → Complex generic types, consider simplifying

6. Dependency Management ​

Force Update Strategy ​

Keep dependencies in sync with the NPM registry using the shared scripts/force-update.sh, exposed as the force-update script in every package:

bash
# Update to latest stable versions (from a package directory)
bun run force-update latest

# Update to pre-release versions (for testing new features)
bun run force-update next

What it does:

  1. Queries NPM registry for the specified tag (latest, next, or highest)
  2. Rewrites the package's @venizia/* dependencies in package.json to a caret range on the resolved version (^1.2.3), not an exact pin

When to use:

TagUse Case
latestProduction deployments, stable releases
nextTesting new features, pre-release validation
highestHighest released version by semver sort

Makefile shortcuts (repo root):

bash
make update           # Reinstall deps (postinstall runs force-update)
make update-core      # Update only @venizia/ignis
make update-helpers   # Update only @venizia/ignis-helpers

7. Test Environment Setup ​

Configure separate environment for testing:

Create .env.test ​

bash
# .env.test - Test environment variables
NODE_ENV=test
APP_ENV_APPLICATION_SECRET=test-secret-key
APP_ENV_JWT_SECRET=test-jwt-secret
APP_ENV_POSTGRES_HOST=localhost
APP_ENV_POSTGRES_PORT=5433
APP_ENV_POSTGRES_USERNAME=test_user
APP_ENV_POSTGRES_PASSWORD=test_password
APP_ENV_POSTGRES_DATABASE=test_db

Running Tests ​

bash
# Run all tests with test environment
NODE_ENV=test bun test --env-file=.env.test

# Run tests in watch mode
NODE_ENV=test bun test --watch --env-file=.env.test

# Run specific test file
NODE_ENV=test bun test --env-file=.env.test src/__tests__/user.test.ts

Test Database Setup ​

For integration tests, use a separate database:

bash
# Docker: Start test database
docker run -d \
  --name ignis-test-db \
  -e POSTGRES_USER=test_user \
  -e POSTGRES_PASSWORD=test_password \
  -e POSTGRES_DB=test_db \
  -p 5433:5432 \
  postgres:16

# Run migrations on test database
NODE_ENV=test bun run migrate --env-file=.env.test

8. CI/CD Security ​

Dependency Auditing ​

Integrate security audits into your CI/CD pipeline:

yaml
# GitHub Actions example
name: Security Audit

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 0 * * 1'  # Weekly on Monday

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest

      - name: Install dependencies
        run: bun install

      - name: Run security audit
        run: bun audit

      - name: Check for vulnerabilities
        run: |
          if bun audit --json | jq '.vulnerabilities | length' | grep -v '^0$'; then
            echo "Vulnerabilities found!"
            exit 1
          fi

Pre-deployment Checklist ​

yaml
# GitHub Actions - Full CI pipeline
jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2

      - name: Install
        run: bun install

      - name: Lint
        run: bun run lint

      - name: Typecheck
        run: bun run typecheck

      - name: Compile
        run: bun run compile

      - name: Test
        run: bun test --env-file=.env.test

      - name: Security Audit
        run: bun audit

      - name: Build Docker Image
        run: docker build -t my-app:${{ github.sha }} .

Critical Packages to Monitor ​

PackageWhyCheck
honoWeb framework, HTTP handlingSecurity advisories
drizzle-ormDatabase queries, SQL injectionVersion updates
joseJWT handling, cryptoVulnerability patches
@venizia/ignisFramework coreLatest stable release

9. Cloud Platform Deployments ​

Railway ​

Railway provides simple deployments with automatic builds:

railway.json:

json
{
  "$schema": "https://railway.app/railway.schema.json",
  "build": {
    "builder": "DOCKERFILE",
    "dockerfilePath": "Dockerfile"
  },
  "deploy": {
    "startCommand": "./bin",
    "healthcheckPath": "/health",
    "healthcheckTimeout": 30,
    "restartPolicyType": "ON_FAILURE",
    "restartPolicyMaxRetries": 3
  }
}
bash
# Deploy to Railway
railway login
railway init
railway up

Fly.io ​

fly.toml:

toml
app = "my-ignis-app"
primary_region = "sjc"

[build]
  dockerfile = "Dockerfile"

[http_service]
  internal_port = 3000
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 1

[[services]]
  protocol = "tcp"
  internal_port = 3000

  [[services.ports]]
    port = 80
    handlers = ["http"]

  [[services.ports]]
    port = 443
    handlers = ["tls", "http"]

  [[services.http_checks]]
    interval = 10000
    grace_period = "5s"
    method = "get"
    path = "/health"
    protocol = "http"
    timeout = 2000
bash
# Deploy to Fly.io
fly auth login
fly launch
fly deploy
fly secrets set APP_ENV_APPLICATION_SECRET=xxx

Render ​

render.yaml:

yaml
services:
  - type: web
    name: my-ignis-app
    runtime: docker
    dockerfilePath: ./Dockerfile
    envVars:
      - key: NODE_ENV
        value: production
      - key: APP_ENV_APPLICATION_SECRET
        generateValue: true
      - key: APP_ENV_JWT_SECRET
        generateValue: true
    healthCheckPath: /health
    autoDeploy: true

databases:
  - name: ignis-db
    databaseName: ignis
    user: ignis
    plan: starter

Kubernetes ​

k8s/deployment.yaml:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ignis-app
  labels:
    app: ignis
spec:
  replicas: 3
  selector:
    matchLabels:
      app: ignis
  template:
    metadata:
      labels:
        app: ignis
    spec:
      containers:
        - name: ignis
          image: my-ignis-app:latest
          ports:
            - containerPort: 3000
          env:
            - name: NODE_ENV
              value: "production"
            - name: APP_ENV_APPLICATION_SECRET
              valueFrom:
                secretKeyRef:
                  name: ignis-secrets
                  key: app-secret
            - name: APP_ENV_JWT_SECRET
              valueFrom:
                secretKeyRef:
                  name: ignis-secrets
                  key: jwt-secret
            - name: APP_ENV_POSTGRES_HOST
              valueFrom:
                configMapKeyRef:
                  name: ignis-config
                  key: db-host
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 10
            periodSeconds: 30
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 10
          resources:
            requests:
              memory: "256Mi"
              cpu: "200m"
            limits:
              memory: "512Mi"
              cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
  name: ignis-service
spec:
  selector:
    app: ignis
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
  type: LoadBalancer

Deploy to Kubernetes:

bash
# Create secrets
kubectl create secret generic ignis-secrets \
  --from-literal=app-secret=$(openssl rand -base64 32) \
  --from-literal=jwt-secret=$(openssl rand -base64 32)

# Create configmap
kubectl create configmap ignis-config \
  --from-literal=db-host=postgres-service

# Apply deployment
kubectl apply -f k8s/

# Check status
kubectl get pods -l app=ignis
kubectl logs -l app=ignis -f

10. Monitoring & Observability ​

Health Check Endpoints ​

IGNIS provides built-in health checks:

typescript
// In application.ts
import { HealthCheckComponent } from '@venizia/ignis';

this.component(HealthCheckComponent);

Endpoints (default path /health, configurable via component options):

  • GET /health - Basic liveness check
  • POST /health/ping - Echo/ping check (send { "message": "..." }, get a PONG back)

Logging in Production ​

Configure structured logging:

typescript
import type { ILogger } from '@venizia/ignis-helpers';
import { ApplicationLogger, LoggerFactory } from '@venizia/ignis-helpers';
import { PinoLogger } from '@venizia/ignis-helpers/pino';

// One line at the entrypoint selects the provider. Omit it and winston is loaded on
// first use - which only works when winston is installed and the app is not compiled.
LoggerFactory.use({ provider: PinoLogger });

const logger: ILogger = ApplicationLogger.get('MyService');
logger.info('Service started | port: %d | env: %s', 3000, 'production');

// Method-scoped child logger
logger.for('start').warn('Slow boot | took: %d ms', 1420);

Operational notes:

  • Five levels, each a direct method: debug, info, warn, error, emerg
  • debug() is compiled out at module load unless DEBUG is truthy and NODE_ENV is a development env - restart to change it
  • Log an Error with %s; %j swallows the stack
  • Secret-looking keys redact to [REDACTED] automatically
  • File output is opt-in: without APP_ENV_LOGGER_FOLDER_PATH nothing is written to disk

Metrics Collection ​

Add Prometheus metrics endpoint:

typescript
// src/controllers/metrics.controller.ts
@controller({ path: '/metrics' })
export class MetricsController extends BaseRestController {
  @get({ configs: { path: '/', responses: { 200: { description: 'Prometheus metrics' } } } })
  getMetrics(context: Context) {
    return context.text(`
# HELP http_requests_total Total HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",status="200"} ${requestCount}

# HELP process_uptime_seconds Process uptime
# TYPE process_uptime_seconds gauge
process_uptime_seconds ${process.uptime()}
    `);
  }
}

Summary ​

Deployment MethodBest ForComplexity
DockerGeneral productionLow
Docker ComposeFull stack with DBLow
Bun ExecutableSimple serversVery Low
Railway/RenderQuick deploymentsVery Low
Fly.ioEdge deploymentsLow
KubernetesEnterprise scaleHigh

Choose based on your scale and operational requirements.