Skip to content

Deployment Guide

This guide covers deploying @geekmidas workspace applications to various targets with the CLI's sophisticated deployment system.

Overview

The CLI provides a complete deployment pipeline for monorepo workspaces:

  • Environment Sniffing - Automatic detection of required environment variables
  • State Management - Track deployments across local and remote storage
  • DNS Automation - Automatic DNS configuration with multiple providers
  • Secrets Management - Encrypted secrets injection during builds
  • Multi-App Orchestration - Coordinated deployment of workspace apps

Quick Start

typescript
// gkm.config.ts
import { defineWorkspace } from '@geekmidas/cli/config';

export default defineWorkspace({
  name: 'my-saas',

  apps: {
    api: {
      path: 'apps/api',
      type: 'backend',
      port: 3000,
      routes: './src/endpoints/**/*.ts',
      envParser: './src/config/env',
      logger: './src/config/logger',
    },
    auth: {
      type: 'auth',
      path: 'apps/auth',
      port: 3001,
      provider: 'better-auth',
      entry: './src/index.ts',
      requiredEnv: ['DATABASE_URL', 'BETTER_AUTH_SECRET'],
    },
    web: {
      type: 'frontend',
      path: 'apps/web',
      port: 3002,
      framework: 'nextjs',
      dependencies: ['api', 'auth'],
    },
  },

  services: {
    db: true,
    cache: true,
  },

  deploy: {
    default: 'dokploy',
    dokploy: {
      endpoint: 'https://dokploy.myserver.com',
      projectId: 'proj_abc123',
      registry: 'ghcr.io/myorg',
      domains: {
        production: 'myapp.com',
        staging: 'staging.myapp.com',
      },
    },
    dns: {
      provider: 'route53',
      domain: 'myapp.com',
    },
  },

  state: {
    provider: 'ssm',
    region: 'us-east-1',
  },
});
bash
# Deploy to production
gkm deploy --stage production

Build Providers

Server Provider

Generates a standalone Node.js server application using Hono.

bash
gkm build --provider server

Output: .gkm/server/

  • app.ts - Hono application entry point
  • dist/ - Production bundle (when bundling enabled)

Production Options:

OptionDefaultDescription
bundletrueBundle server into single file
minifytrueMinify bundled output
healthCheck'/health'Health check endpoint path
gracefulShutdowntrueEnable graceful shutdown handling
external[]Packages to exclude from bundling
openapifalseInclude OpenAPI spec in production
typescript
// gkm.config.ts
import { defineWorkspace } from '@geekmidas/cli/config';

export default defineWorkspace({
  apps: {
    api: {
      path: 'apps/api',
      type: 'backend',
      port: 3000,
      routes: './src/endpoints/**/*.ts',
      envParser: './src/config/env',
      logger: './src/config/logger',
      providers: {
        server: {
          enableOpenApi: true,
          production: {
            bundle: true,
            minify: true,
            healthCheck: '/health',
            external: ['@prisma/client'],
          },
        },
      },
    },
  },
});

AWS Lambda Provider

Generates handlers compatible with AWS API Gateway.

bash
# API Gateway v2 (HTTP API)
gkm build --provider aws-apigatewayv2

# API Gateway v1 (REST API)
gkm build --provider aws-apigatewayv1

Environment Variables

The CLI handles environment variables differently in development and production.

Development (gkm dev)

In development, environment variables come from multiple sources:

1. Docker Compose Services

When services are configured, connection URLs are auto-generated:

typescript
services: {
  db: true,    // → DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
  cache: true, // → REDIS_URL=redis://localhost:6379
  mail: true,  // → SMTP_HOST=localhost, SMTP_PORT=1025
}

2. Secrets Store

Secrets from .gkm/secrets/development.json are injected:

bash
# Set a development secret
gkm secrets:set STRIPE_KEY sk_test_xxx --stage development

3. Per-App Mapping (Workspaces)

For multi-app workspaces, app-prefixed secrets are mapped:

API_DATABASE_URL=...   → DATABASE_URL (for api app)
AUTH_DATABASE_URL=...  → DATABASE_URL (for auth app)

4. .env Files

Standard .env and .env.local files are loaded.

Production (gkm deploy)

In production, the CLI auto-injects these variables:

VariableSourceDescription
PORTApp configFrom port in workspace config
NODE_ENVAutoAlways 'production'
STAGECLI flagDeployment stage name
DATABASE_URLGeneratedPer-app credentials + Postgres service
REDIS_URLGeneratedRedis service connection
BETTER_AUTH_URLDerivedhttps://{app-hostname}
BETTER_AUTH_SECRETGeneratedRandom secret, persisted in state
BETTER_AUTH_TRUSTED_ORIGINSDerivedAll frontend URLs (comma-separated)

Custom secrets are injected from the secrets store:

bash
# Set production secrets
gkm secrets:set STRIPE_KEY sk_live_xxx --stage production
gkm secrets:set SENDGRID_API_KEY SG.xxx --stage production

Environment Sniffing

The CLI automatically detects required environment variables by analyzing your code.

Detection Strategy (Priority Order)

  1. Explicit requiredEnv - Direct list in app config takes priority
  2. Entry-based apps - Imports entry file to capture envParser.parse() calls
  3. Route-based apps - Calls getEnvironment() on endpoint constructs
  4. Frontend apps - Returns empty array (no server secrets)

How It Works

The sniffer runs your code in an isolated subprocess with a patched EnvironmentParser that records all accessed variables:

typescript
// Your code
const config = new EnvironmentParser(process.env)
  .create((get) => ({
    database: get('DATABASE_URL').string(),  // Recorded!
    port: get('PORT').number(),              // Recorded!
  }))
  .parse();

// Sniffer detects: ['DATABASE_URL', 'PORT']

Auto-Supported Variables

These variables are automatically resolved without manual configuration:

VariableSource
PORTApp config or default
NODE_ENVAlways 'production'
STAGEDeployment stage name
DATABASE_URLGenerated per-app credentials
REDIS_URLProvisioned Redis service
BETTER_AUTH_URLDerived from app hostname
BETTER_AUTH_SECRETGenerated and persisted
BETTER_AUTH_TRUSTED_ORIGINSAll frontend URLs

Explicit Requirements

Override automatic detection:

typescript
// gkm.config.ts
import { defineWorkspace } from '@geekmidas/cli/config';

export default defineWorkspace({
  apps: {
    api: {
      path: 'apps/api',
      type: 'backend',
      port: 3000,
      requiredEnv: ['DATABASE_URL', 'STRIPE_SECRET_KEY', 'SENDGRID_API_KEY'],
    },
  },
});

State Providers

State providers track deployment resources (application IDs, service IDs, credentials) across deployments.

LocalStateProvider (Default)

Stores state in the local filesystem.

  • Location: .gkm/deploy-{stage}.json
  • Use case: Single developer, local development

SSMStateProvider

Stores state in AWS Systems Manager Parameter Store.

  • Location: /gkm/{workspaceName}/{stage}/state
  • Encryption: AWS-managed KMS key
  • Use case: Teams, CI/CD pipelines
typescript
// gkm.config.ts
import { defineWorkspace } from '@geekmidas/cli/config';

export default defineWorkspace({
  name: 'my-app',  // Required for SSM provider
  apps: { /* ... */ },
  state: {
    provider: 'ssm',
    region: 'us-east-1',
  },
});

CachedStateProvider

Wraps remote storage with local caching for faster reads.

bash
# Sync remote state to local
gkm state:pull --stage production

# Push local changes to remote
gkm state:push --stage production

# Compare local vs remote
gkm state:diff --stage production

State Contents

typescript
interface DokployStageState {
  provider: 'dokploy';
  stage: string;
  environmentId: string;
  applications: Record<string, string>;     // appName -> applicationId
  services: {
    postgresId?: string;
    redisId?: string;
  };
  appCredentials?: Record<string, {
    dbUser: string;
    dbPassword: string;
  }>;
  generatedSecrets?: Record<string, Record<string, string>>;
  dnsVerified?: Record<string, {
    serverIp: string;
    verifiedAt: string;
  }>;
  lastDeployedAt: string;
}

DNS Providers

Automatically configure DNS records for your deployed applications.

Route53Provider

AWS Route 53 DNS management.

typescript
// gkm.config.ts
export default defineWorkspace({
  deploy: {
    dns: {
      provider: 'route53',
      domain: 'myapp.com',        // Required - root domain
      region: 'us-east-1',        // Optional, uses AWS_REGION env var
      profile: 'production',      // Optional, AWS profile from ~/.aws/credentials
      hostedZoneId: 'Z123...',    // Optional, auto-detected from domain
      ttl: 300,                   // Optional, default 300
    },
  },
});

Features:

  • Auto-detects hosted zone from domain name
  • Batch processes records (up to 1000 per request)
  • Idempotent - skips existing records with same value
  • Supports: A, AAAA, CNAME, MX, TXT, SRV, CAA

Authentication: Uses AWS default credential chain (no login command required):

  • Environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
  • Shared credentials file (~/.aws/credentials)
  • AWS profile via profile config option
  • IAM role (EC2, ECS, Lambda)

HostingerProvider

Hostinger DNS management.

typescript
// gkm.config.ts
export default defineWorkspace({
  deploy: {
    dns: {
      provider: 'hostinger',
      domain: 'myapp.com',        // Required - root domain
      ttl: 300,                   // Optional, default 300
    },
  },
});

Setup:

  1. Get API token from Hostinger hPanel profile
  2. Store with gkm login --provider hostinger

Manual DNS

For externally managed domains:

typescript
// gkm.config.ts
export default defineWorkspace({
  deploy: {
    dns: {
      provider: 'manual',
      domain: 'myapp.com',        // Required - root domain
    },
  },
});

The CLI will display required DNS records for manual configuration.

DNS Verification

After creating records, the CLI:

  1. Waits for DNS propagation
  2. Verifies records resolve to correct IP
  3. Caches verification in state (skips on subsequent deploys)
  4. Triggers SSL certificate generation via Dokploy

Secrets Management

Setting Secrets

bash
# Initialize secrets for a stage
gkm secrets:init --stage production

# Set individual secrets
gkm secrets:set --stage production --key STRIPE_SECRET_KEY --value "sk_live_..."
gkm secrets:set --stage production --key SENDGRID_API_KEY --value "SG...."

# Import from JSON file
gkm secrets:import --stage production --file secrets.json

Secret Types

Custom Secrets - User-provided key-value pairs:

bash
gkm secrets:set --key API_KEY --value "secret"

URL Secrets - Connection strings:

bash
gkm secrets:set --key DATABASE_URL --value "postgres://..."
gkm secrets:set --key REDIS_URL --value "redis://..."

Service Secrets - Auto-managed credentials:

  • POSTGRES_PASSWORD - Generated when Postgres provisioned
  • REDIS_PASSWORD - Generated when Redis provisioned

Viewing Secrets

bash
# Show secrets (masked)
gkm secrets:show --stage production

# Show secrets (revealed)
gkm secrets:show --stage production --reveal

Rotation

bash
# Rotate service passwords
gkm secrets:rotate --stage production --service postgres
gkm secrets:rotate --stage production --service redis

Encryption & Injection

During deployment:

  1. Secrets are filtered to only required variables per app
  2. Encrypted with an ephemeral master key
  3. Passed as Docker build args (GKM_ENCRYPTED_CREDENTIALS, GKM_CREDENTIALS_IV)
  4. Master key injected as GKM_MASTER_KEY environment variable
  5. Decrypted at runtime by the application

Dokploy Deployment

Dokploy is a self-hosted deployment platform.

Initial Setup

bash
# Login to Dokploy instance
gkm login --provider dokploy

# The CLI will prompt for:
# - Dokploy endpoint URL
# - API token

Deploy Command

bash
# Deploy to production
gkm deploy --stage production

# Preview what would be deployed
gkm deploy --stage production --dry-run

# Skip building (use existing image)
gkm deploy --stage production --skip-build

Workspace Deployment Flow

For monorepos, the CLI orchestrates deployment in phases:

Phase 1: Infrastructure

  • Provision PostgreSQL (if configured)
  • Provision Redis (if configured)
  • Create per-app database users with schema isolation

Phase 2: Backend Apps

  • Build Docker images with encrypted secrets
  • Deploy in dependency order
  • Configure domains and SSL

Phase 3: Frontend Apps

  • Generate public URLs from deployed backends
  • Build with NEXT_PUBLIC_* environment variables
  • Deploy and configure domains

Phase 4: DNS & Verification

  • Create DNS records via configured provider
  • Verify propagation
  • Trigger SSL certificate generation

Per-App Database Isolation

When PostgreSQL is provisioned:

  • Each app gets its own database user
  • api app uses the public schema (for shared migrations)
  • Other apps get their own schema with search_path set
sql
-- API app
CREATE USER "api" WITH PASSWORD '...';
GRANT ALL ON SCHEMA public TO "api";

-- Other apps
CREATE USER "worker" WITH PASSWORD '...';
CREATE SCHEMA "worker" AUTHORIZATION "worker";
ALTER USER "worker" SET search_path TO "worker";

Docker Deployment

Generate Docker Files

bash
gkm docker --compose --services postgres,redis

Dockerfile Generation

The CLI generates optimized multi-stage Dockerfiles:

dockerfile
# Build stage
FROM node:22-alpine AS builder
WORKDIR /app
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build && pnpm gkm build --provider server

# Production stage
FROM node:22-alpine AS runner
WORKDIR /app
COPY --from=builder /app/.gkm/server/dist ./
EXPOSE 3000
CMD ["node", "app.js"]

Build and Push

bash
# Build image
gkm docker build --tag my-api:latest

# Push to registry
gkm docker push --tag my-api:latest

CI/CD Integration

GitHub Actions

yaml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

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

      - uses: pnpm/action-setup@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm

      - run: pnpm install --frozen-lockfile
      - run: pnpm build

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: us-east-1

      - name: Deploy
        env:
          DOKPLOY_TOKEN: ${{ secrets.DOKPLOY_TOKEN }}
        run: |
          pnpm gkm deploy --stage production

Required Secrets

SecretPurpose
DOKPLOY_TOKENDokploy API authentication
AWS_ROLE_ARNFor SSM state provider and Route53 DNS
Custom secretsApplication-specific (Stripe, SendGrid, etc.)

Production Checklist

Before deploying to production:

  • [ ] All tests passing (pnpm test:once)
  • [ ] Type checks passing (pnpm ts:check)
  • [ ] Linting passing (pnpm lint)
  • [ ] Secrets configured (gkm secrets:show --stage production)
  • [ ] DNS provider configured
  • [ ] State provider configured (SSM for teams)
  • [ ] Health check endpoint configured
  • [ ] Database migrations ready
  • [ ] Logging configured for production
  • [ ] Error tracking enabled (Sentry, etc.)

Troubleshooting

Environment Variables Not Detected

If the sniffer misses variables:

  1. Ensure all get() calls happen before .parse()
  2. Use requiredEnv in config for dynamic variables
  3. Check subprocess output with --verbose flag

DNS Propagation Issues

bash
# Check DNS resolution
dig api.myapp.com

# Force re-verification
gkm deploy --stage production --force-dns

State Sync Issues

bash
# Pull latest state from remote
gkm state:pull --stage production

# Compare local vs remote
gkm state:diff --stage production

# Force push local state
gkm state:push --stage production --force

Database Connection Issues

Per-app credentials are stored in state. If connection fails:

  1. Check gkm state:show --stage production
  2. Verify credentials match Postgres users
  3. Re-run deployment to recreate users if needed