Workers are no longer curl|sh-only. Admins add and manage them from the
dashboard over SSH, with all runtime config (Kafka, Schema Registry,
Redis, AWS keys) stored encrypted via the existing KMS-envelope cipher
service.
Worker lifecycle:
1. Admin POSTs host/port/user. Backend generates an ed25519 keypair,
encrypts the private key under uuid.Nil (platform identity), and
stores the row in 'pending' state.
2. Admin pastes the returned public key into the VPS's authorized_keys.
3. Test connection — runs `true` over SSH, pins the host SHA256
fingerprint on first success (TOFU).
4. Install — backend scp's install-worker.sh + a per-worker env file
and runs it. State moves pending → provisioning → installed.
5. From then on: restart, update image, apply config, uninstall,
rotate keys, tail logs, live status, OS package update, reboot —
all dashboard buttons backed by SSH operations.
Credentials are reusable entities:
- aws_credentials: named keypair, secret encrypted at rest
- worker_profiles: bundles Kafka + Schema Registry + Redis + image +
release channel, references one AWS credentials row
- workers.profile_id links a worker to a profile; many workers can
share one profile
Saving a profile doesn't restart anything. The dashboard compares
profile.updated_at to each worker's config_applied_at and shows a
"stale config" badge; Apply rewrites /etc/warmbly/worker.env over SSH
and restarts the unit.
Auto-update on GitHub release:
- profile.release_channel ∈ {pinned, stable, dev}
- profile.auto_update toggles automatic rollout
- Trigger model is push, not poll: one check on backend boot, then
the /webhooks/github/releases endpoint (HMAC-validated with
RELEASES_WEBHOOK_SECRET) on every release event. Manual "Check now"
button as fallback.
- When a new tag resolves, the orchestrator SSHes into each assigned
worker, runs install-worker.sh --update --image <new>, which now
rewrites the systemd unit (not just `docker pull`) so the image
actually changes. workers.image_version captures the running tag
for the UI's "v1.2.3 → v1.2.4" diff.
Self-hostable: every release knob is env-driven —
RELEASES_GITHUB_REPO, RELEASES_WORKER_IMAGE_REPO,
RELEASES_WEBHOOK_SECRET, RELEASES_GITHUB_TOKEN, RELEASES_ENABLED. Set
RELEASES_ENABLED=false to disable the feature entirely.
OS-level updates and reboot are also exposed: detect apt / dnf / yum /
pacman / apk, run the right upgrade noninteractively, return the full
output and a reboot-required flag. Reboots are never automatic.
Migrations:
000028_worker_ssh — ssh fields, install_state enum, last_seen,
host fingerprint
000029_worker_credentials — aws_credentials + worker_profiles +
workers.profile_id + workers.config_applied_at
000030_worker_releases — release_channel enum, auto_update,
resolved_image_tag, workers.image_version
Endpoints added:
POST /admin/workers (create + keypair)
GET /admin/workers/managed
GET /admin/workers/:id/managed
POST /admin/workers/:id/{test,install,restart,upgrade,uninstall,rotate-keys,apply,system-update,reboot}
PUT /admin/workers/:id/profile
GET /admin/workers/:id/{live-status,logs}
DELETE /admin/workers/:id
GET /admin/aws-credentials CRUD
GET /admin/worker-profiles CRUD + /workers + /apply + /release
GET /admin/releases/state
POST /admin/releases/check
POST /webhooks/github/releases public, HMAC-validated
Admin UI:
/app/admin/workers list with status + version columns
/app/admin/workers/new add form with profile dropdown
/app/admin/workers/:id detail with all actions + logs + system update
/app/admin/credentials tabs: AWS credentials + worker profiles,
Releases panel, channel selector +
auto-update toggle in profile form
Warmbly Deployment Infrastructure
This directory contains all deployment configurations for Warmbly services.
Services
| Service | Language | Description |
|---|---|---|
| Backend | Go | API server |
| Consumer | Go | Kafka event consumer |
| Worker | Go | Distributed worker (1 per machine) |
| Tracking | Rust | Tracking pixel service |
| Realtime | Elixir | WebSocket server |
Directory Structure
deploy/
├── docker/ # Docker configurations
│ ├── backend.Dockerfile
│ ├── consumer.Dockerfile
│ ├── worker.Dockerfile
│ ├── realtime.Dockerfile
│ └── docker-compose.yml # Local development stack
│
├── kubernetes/
│ ├── base/ # Base Kustomize configs
│ │ ├── backend/
│ │ ├── consumer/
│ │ ├── worker/
│ │ ├── tracking/
│ │ ├── realtime/
│ │ ├── ingress/
│ │ └── config/
│ │
│ └── overlays/
│ ├── dev/ # Dev environment
│ └── prod/ # Production environment
│
└── config/
└── env.example # Environment variable reference
Local Development
Start the full development stack with Docker Compose:
cd deploy/docker
# Start infrastructure only
docker-compose up -d postgres redis kafka schema-registry
# Start all services
docker-compose up
Service URLs (Local)
- Backend API: http://localhost:8080
- Tracking: http://localhost:3000
- Realtime: http://localhost:4000
- Mailpit (email inbox): http://localhost:8025
- Kafka: localhost:9092
- Schema Registry: http://localhost:8081
- PostgreSQL: localhost:5432
- Redis: localhost:6379
Building Docker Images
# From repository root
docker build -f deploy/docker/backend.Dockerfile -t warmbly/backend .
docker build -f deploy/docker/consumer.Dockerfile -t warmbly/consumer .
docker build -f deploy/docker/worker.Dockerfile -t warmbly/worker .
docker build -f deploy/docker/realtime.Dockerfile -t warmbly/realtime .
docker build -f tracking/Dockerfile -t warmbly/tracking tracking/
Kubernetes Deployment
Prerequisites
-
External Secrets Operator - For AWS Secrets Manager integration
helm repo add external-secrets https://charts.external-secrets.io helm install external-secrets external-secrets/external-secrets -n external-secrets --create-namespace -
NGINX Ingress Controller
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm install ingress-nginx ingress-nginx/ingress-nginx -n ingress-nginx --create-namespace -
cert-manager - For TLS certificates
helm repo add jetstack https://charts.jetstack.io helm install cert-manager jetstack/cert-manager -n cert-manager --create-namespace --set installCRDs=true
Deploy to Dev
kubectl apply -k deploy/kubernetes/overlays/dev
Deploy to Production
kubectl apply -k deploy/kubernetes/overlays/prod
Verify Deployment
# Check pods
kubectl get pods -n warmbly
# Check services
kubectl get svc -n warmbly
# Check ingress
kubectl get ingress -n warmbly
Configuration
Environment Variables
See config/env.example for a complete list of environment variables.
Production Secrets
Production secrets are stored in AWS Secrets Manager and synced via External Secrets Operator:
| Secret Key | Description |
|---|---|
postgres/primary |
Primary database connection string |
redis/primary |
Redis connection string |
kafka/sasl/username |
Kafka SASL username |
kafka/sasl/password |
Kafka SASL password |
auth/secret |
Auth signing secret |
auth/jwt_secret |
JWT secret |
stripe/secret_key |
Stripe secret key |
stripe/webhook_secret |
Stripe webhook secret |
Non-Sensitive Config
Non-sensitive configuration is stored in the ConfigMap (config/configmap.yaml) and can be committed to git.
Worker DaemonSet
The Worker service runs as a DaemonSet, meaning one pod per node. Nodes must be labeled:
kubectl label node <node-name> warmbly.com/role=worker
Health Checks
All services expose health endpoints:
curl http://localhost:8080/health # Backend
curl http://localhost:3000/health # Tracking
curl http://localhost:4000/health # Realtime
Scaling
Development
Default replicas are set in base manifests.
Production
Production replica counts are configured in overlays/prod/patches/replicas.yaml:
- Backend: 3 replicas
- Consumer: 2 replicas
- Tracking: 3 replicas
- Realtime: 3 replicas
- Worker: 1 per labeled node (DaemonSet)