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
// 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',
},
});# Deploy to production
gkm deploy --stage productionBuild Providers
Server Provider
Generates a standalone Node.js server application using Hono.
gkm build --provider serverOutput: .gkm/server/
app.ts- Hono application entry pointdist/- Production bundle (when bundling enabled)
Production Options:
| Option | Default | Description |
|---|---|---|
bundle | true | Bundle server into single file |
minify | true | Minify bundled output |
healthCheck | '/health' | Health check endpoint path |
gracefulShutdown | true | Enable graceful shutdown handling |
external | [] | Packages to exclude from bundling |
openapi | false | Include OpenAPI spec in production |
// 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.
# API Gateway v2 (HTTP API)
gkm build --provider aws-apigatewayv2
# API Gateway v1 (REST API)
gkm build --provider aws-apigatewayv1Environment 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:
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:
# Set a development secret
gkm secrets:set STRIPE_KEY sk_test_xxx --stage development3. 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:
| Variable | Source | Description |
|---|---|---|
PORT | App config | From port in workspace config |
NODE_ENV | Auto | Always 'production' |
STAGE | CLI flag | Deployment stage name |
DATABASE_URL | Generated | Per-app credentials + Postgres service |
REDIS_URL | Generated | Redis service connection |
BETTER_AUTH_URL | Derived | https://{app-hostname} |
BETTER_AUTH_SECRET | Generated | Random secret, persisted in state |
BETTER_AUTH_TRUSTED_ORIGINS | Derived | All frontend URLs (comma-separated) |
Custom secrets are injected from the secrets store:
# Set production secrets
gkm secrets:set STRIPE_KEY sk_live_xxx --stage production
gkm secrets:set SENDGRID_API_KEY SG.xxx --stage productionEnvironment Sniffing
The CLI automatically detects required environment variables by analyzing your code.
Detection Strategy (Priority Order)
- Explicit
requiredEnv- Direct list in app config takes priority - Entry-based apps - Imports entry file to capture
envParser.parse()calls - Route-based apps - Calls
getEnvironment()on endpoint constructs - 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:
// 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:
| Variable | Source |
|---|---|
PORT | App config or default |
NODE_ENV | Always 'production' |
STAGE | Deployment stage name |
DATABASE_URL | Generated per-app credentials |
REDIS_URL | Provisioned Redis service |
BETTER_AUTH_URL | Derived from app hostname |
BETTER_AUTH_SECRET | Generated and persisted |
BETTER_AUTH_TRUSTED_ORIGINS | All frontend URLs |
Explicit Requirements
Override automatic detection:
// 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
// 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.
# 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 productionState Contents
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.
// 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
profileconfig option - IAM role (EC2, ECS, Lambda)
HostingerProvider
Hostinger DNS management.
// gkm.config.ts
export default defineWorkspace({
deploy: {
dns: {
provider: 'hostinger',
domain: 'myapp.com', // Required - root domain
ttl: 300, // Optional, default 300
},
},
});Setup:
- Get API token from Hostinger hPanel profile
- Store with
gkm login --provider hostinger
Manual DNS
For externally managed domains:
// 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:
- Waits for DNS propagation
- Verifies records resolve to correct IP
- Caches verification in state (skips on subsequent deploys)
- Triggers SSL certificate generation via Dokploy
Secrets Management
Setting Secrets
# 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.jsonSecret Types
Custom Secrets - User-provided key-value pairs:
gkm secrets:set --key API_KEY --value "secret"URL Secrets - Connection strings:
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 provisionedREDIS_PASSWORD- Generated when Redis provisioned
Viewing Secrets
# Show secrets (masked)
gkm secrets:show --stage production
# Show secrets (revealed)
gkm secrets:show --stage production --revealRotation
# Rotate service passwords
gkm secrets:rotate --stage production --service postgres
gkm secrets:rotate --stage production --service redisEncryption & Injection
During deployment:
- Secrets are filtered to only required variables per app
- Encrypted with an ephemeral master key
- Passed as Docker build args (
GKM_ENCRYPTED_CREDENTIALS,GKM_CREDENTIALS_IV) - Master key injected as
GKM_MASTER_KEYenvironment variable - Decrypted at runtime by the application
Dokploy Deployment
Dokploy is a self-hosted deployment platform.
Initial Setup
# Login to Dokploy instance
gkm login --provider dokploy
# The CLI will prompt for:
# - Dokploy endpoint URL
# - API tokenDeploy Command
# 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-buildWorkspace 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
apiapp uses thepublicschema (for shared migrations)- Other apps get their own schema with
search_pathset
-- 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
gkm docker --compose --services postgres,redisDockerfile Generation
The CLI generates optimized multi-stage Dockerfiles:
# 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
# Build image
gkm docker build --tag my-api:latest
# Push to registry
gkm docker push --tag my-api:latestCI/CD Integration
GitHub Actions
# .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 productionRequired Secrets
| Secret | Purpose |
|---|---|
DOKPLOY_TOKEN | Dokploy API authentication |
AWS_ROLE_ARN | For SSM state provider and Route53 DNS |
| Custom secrets | Application-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:
- Ensure all
get()calls happen before.parse() - Use
requiredEnvin config for dynamic variables - Check subprocess output with
--verboseflag
DNS Propagation Issues
# Check DNS resolution
dig api.myapp.com
# Force re-verification
gkm deploy --stage production --force-dnsState Sync Issues
# 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 --forceDatabase Connection Issues
Per-app credentials are stored in state. If connection fails:
- Check
gkm state:show --stage production - Verify credentials match Postgres users
- Re-run deployment to recreate users if needed