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

194 lines
5.2 KiB
Markdown

# 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
```hcl
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
```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
```bash
# 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
```