Files
lunarfront-app/docs/deployment.md
Ryan Moon c2b1073fef feat: add CI/CD pipeline, production Dockerfile, and deployment architecture
- Add production Dockerfile with bun build --compile, multi-stage Alpine build
- Add .dockerignore
- Swap bcrypt -> bcryptjs (pure JS, no native addons)
- Add programmatic migrations on startup via drizzle migrator
- Add /v1/version endpoint with APP_VERSION baked in at build time
- Add .gitea/workflows/ci.yml (lint + test with postgres/valkey services)
- Add .gitea/workflows/build.yml (version bump, build, push to registry)
- Update CLAUDE.md and docs/architecture.md to remove multi-tenancy
- Add docs/deployment.md covering DOKS + ArgoCD architecture

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 19:50:37 -05:00

5.2 KiB

Infrastructure & Deployment Architecture

Overview

LunarFront runs on DigitalOcean. Each customer is a fully isolated deployment — their own Kubernetes namespace, their own database. There is no multi-tenancy in the application layer.

Stack: DOKS (Kubernetes) · ArgoCD · Helm · Gitea · Terraform · Ansible

The guiding principle is simplicity — Docker Compose on Droplets for Gitea, Kubernetes for customer app instances. No surprise bills.


Monthly Cost

Resource Spec Cost
DOKS cluster Control plane $12/mo
DOKS nodes (2x s-2vcpu-4gb) Runs all customer apps ~$48/mo
Managed Postgres (shared) All customer databases $15-25/mo
Managed Redis (shared) All customer queues/cache $15/mo
Gitea Droplet 1 vCPU, 1GB RAM $6/mo
Gitea Postgres Dedicated managed Postgres $15/mo
Spaces Registry, backups, files ~$21/mo
Fixed total ~$132-142/mo
Per customer New database in shared Postgres ~$0 marginal

Environments

Environment Hostname Purpose
Production (per customer) {customer}.app.lunarfront.tech Live customer instance on DOKS
Gitea git.lunarfront.tech Source control, CI/CD, container registry
Dev Local / feature namespace on DOKS Testing and staging

DNS

Managed through Cloudflare. Records defined in Terraform.

Record Type Points To
git.lunarfront.tech A Gitea Droplet
git-ssh.lunarfront.tech A Gitea Droplet (SSH port 2222)
registry.lunarfront.tech A Gitea Droplet (container registry)
{customer}.app.lunarfront.tech CNAME DOKS load balancer

Infrastructure (Terraform)

Terraform manages all DigitalOcean and Cloudflare resources. State stored in Spaces.

/terraform
  main.tf         providers, backend (Spaces state)
  variables.tf
  cluster.tf      DOKS cluster + node pools
  databases.tf    shared Postgres, shared Redis
  gitea.tf        Gitea Droplet
  spaces.tf       files, backups, tf-state buckets
  dns.tf          Cloudflare DNS records
  outputs.tf      cluster endpoint, DB URLs
  terraform.tfvars  secrets (gitignored)

State Backend

terraform {
  backend "s3" {
    endpoint                    = "https://nyc3.digitaloceanspaces.com"
    bucket                      = "lunarfront-terraform-state"
    key                         = "terraform.tfstate"
    region                      = "us-east-1"
    skip_credentials_validation = true
    skip_metadata_api_check     = true
    skip_region_validation      = true
    force_path_style            = true
  }
}

Gitea (Source Control + CI/CD)

Gitea runs on its own Droplet with a dedicated managed Postgres. It hosts:

  • Source code repositories
  • Gitea Actions CI/CD pipelines
  • Docker container registry (registry.lunarfront.tech)

Managed by Ansible. See lunarfront-infra/ansible/roles/gitea/.

CI Pipeline (Gitea Actions)

On push to main:

  1. Run lint + unit tests using the shared ci-runner image
  2. Build Docker image, push to registry.lunarfront.tech/ryan/lunarfront-app:{sha}
  3. Update the Helm chart values.yaml with the new image tag
  4. ArgoCD detects the change and syncs all customer deployments

Kubernetes (DOKS + ArgoCD)

All customer app instances run on a single DOKS cluster managed by ArgoCD (GitOps).

Repository Structure

/gitops
  apps/
    customer-acme/
      values.yaml
    customer-foo/
      values.yaml
  chart/
    Chart.yaml
    templates/
      deployment.yaml
      service.yaml
      ingress.yaml
      job-migrate.yaml   # runs drizzle-kit migrate on deploy
      secret.yaml

Per-Customer values.yaml

customer: acme
subdomain: acme

image:
  repository: registry.lunarfront.tech/ryan/lunarfront-app
  tag: "abc123"

database:
  url: "postgresql://lunarfront:pass@db-host:25060/customer_acme?sslmode=require"

redis:
  url: "rediss://..."

env:
  JWT_SECRET: "..."
  NODE_ENV: production

Adding a New Customer

  1. CREATE DATABASE customer_x; on the shared managed Postgres
  2. Add gitops/apps/customer-x/values.yaml
  3. Push — ArgoCD syncs, migration Job runs, instance is live
  4. Add DNS CNAME in Terraform

Database Strategy

One managed Postgres cluster shared across all customers. Each customer gets their own isolated database (CREATE DATABASE customer_x). No cross-customer queries are possible at the database level.

  • New customer = new database, no new infrastructure cost
  • Managed DO Postgres handles backups, failover, and SSL
  • Resize the cluster as total load grows

Day-to-Day Workflow

# Provision infrastructure from scratch
terraform init && terraform apply

# Configure Gitea server
ansible-playbook ansible/gitea.yml -i ansible/inventory.ini

# Normal deploy flow — push to main
git push origin main
# → Gitea Actions: lint + test + build image
# → Image pushed to registry
# → Helm chart values updated
# → ArgoCD syncs all customer deployments automatically

# Add a new customer
# 1. Create database
psql $DATABASE_URL -c "CREATE DATABASE customer_x;"
# 2. Add values file, push
git add gitops/apps/customer-x/values.yaml && git push
# Done — ArgoCD handles the rest