HIP-14: Application Deployment Standard. Status Draft. Hanzo's own standard — read this before implementing against it.
This proposal defines the application deployment standard for the Hanzo ecosystem. The Hanzo Platform is a self-hosted PaaS (Platform as a Service) built as a fork of Dokploy, an open-source application deployment platform. It provides a standardized pipeline for building, deploying, scaling, and monitoring all Hanzo services -- from the LLM Gateway to the Chat frontend to internal tooling.
Every Hanzo service MUST be deployable through Platform. The deployment flow is: git push to repository, Platform detects the change, builds a container image, pushes it to the registry, deploys it to the target environment, runs health checks, and either promotes the deployment or rolls back. Developers interact with Platform through a web UI at platform.hanzo.ai or through the hanzo CLI. They never run kubectl apply or docker compose up on production infrastructure directly.
Repository: github.com/hanzoai/platform Production: https://platform.hanzo.ai Port: 3000 (Platform UI), 5173 (legacy admin) Docker: ghcr.io/hanzoai/platform:latest Cluster: the cluster (24.199.76.156)
Hanzo operates 30+ services across two Kubernetes clusters (the cluster, lux-k8s). Without a standardized deployment layer, each team deploys independently:
kubectl apply. Manifests drift between what is in Git and what is deployed. A typo in a resource limit brings down a service with no audit trail of who changed what.pnpm build. The Python SDK uses uv. IAM uses go build. Each project invents its own CI/CD workflow. A new engineer onboarding to any project must first reverse-engineer how it gets built and deployed..env files on developer machines. Without centralized injection from KMS (HIP-27), secrets leak into build logs, Git history, and container images.kubectl get pods on each cluster, parse image tags, and cross-reference with Git commits. Platform provides a dashboard showing every service, its current deployment, health status, and resource usage.A single PaaS layer eliminates all six problems. Teams define a hanzo.yaml manifest in their repository. Platform handles everything else: build, image push, deploy, health check, TLS certificate, domain routing, log aggregation, and rollback. The cognitive load drops from "understand Kubernetes, Docker, Traefik, and Let's Encrypt" to "write a 10-line YAML file and push."
This section explains the why behind every major architectural decision. PaaS choices are sticky -- once 30 services depend on a deployment platform, migrating away costs months. Understanding the rationale prevents future engineers from re-evaluating settled questions.
Dokploy was selected after evaluating all major open-source PaaS platforms:
| Factor | Dokploy | Coolify | CapRover | Vercel | |--------|---------|---------|----------|--------| | License | Apache 2.0 | BSL | Apache 2.0 | Proprietary | | Architecture | Docker + Traefik | Docker + Traefik | Docker Swarm | Proprietary | | Language | TypeScript/Node.js | PHP/TypeScript | JavaScript | N/A | | UI quality | Modern, clean | Modern, good | Dated | Excellent | | K8s support | Via Docker | Planned | No | No | | Active development | Yes (2024+) | Yes | Stagnant | Yes | | Self-hostable | Yes | Yes (with license) | Yes | No |
Dokploy's architecture is the simplest correct design: applications are Docker containers, routing is Traefik, builds are Docker builds, and state is SQL. There is no custom scheduler, no proprietary runtime, no magic. When something breaks, docker logs and docker inspect tell you everything you need to know.
Upstream Dokploy covers the common case well. Our fork adds capabilities specific to the Hanzo ecosystem that upstream cannot or will not support:
hanzo org sees IAM, Cloud, Console; the lux org sees validators, gateway, markets; the zoo org sees research services. A user's org membership (from IAM) determines what they see in Platform.We maintain our fork by periodically rebasing on upstream Dokploy releases, resolving conflicts in the IAM/KMS integration layer. The fork diverges only in authentication, secrets, multi-tenancy, and audit -- core application deployment logic remains aligned with upstream.
Every deployable service includes a hanzo.yaml at the repository root. This manifest declares the application's build, runtime, scaling, and routing configuration. Platform reads this file to determine how to build and deploy the service.
# hanzo.yaml -- full schema
name: my-service # Required. Unique within the org.
org: hanzo # Required. Organization that owns this service.
runtime: auto # auto | node-20 | node-22 | python-3.12 | go-1.22 | rust-1.78 | docker
version: "1.0.0" # Informational. Not used for deployment decisions.
build:
dockerfile: Dockerfile # Path to Dockerfile (default: Dockerfile). Ignored if runtime != docker.
context: . # Docker build context (default: repository root).
command: npm run build # Build command for non-Docker runtimes.
output: dist # Build output directory (for static sites).
args: # Build arguments (NOT for secrets -- use env.secret).
NODE_ENV: production
NEXT_TELEMETRY_DISABLED: "1"
deploy:
instances: 2 # Number of replicas (default: 1).
memory: 512Mi # Memory limit per instance (default: 256Mi).
cpu: "0.5" # CPU limit per instance (default: 0.25).
strategy: rolling # rolling | recreate (default: rolling).
max_surge: 1 # Extra instances during rolling update (default: 1).
max_unavailable: 0 # Instances that can be down during rolling update (default: 0).
startup_timeout: 120 # Seconds to wait for health check before marking failed (default: 120).
services:
- type: web # web | worker | cron
port: 3000 # Container port for web services.
protocol: http # http | grpc | tcp (default: http).
healthcheck:
path: /api/health # HTTP path for health check (default: /).
interval: 30s # Time between checks (default: 30s).
timeout: 5s # Per-check timeout (default: 5s).
retries: 3 # Failures before marking unhealthy (default: 3).
domains:
- cloud.hanzo.ai # Custom domains. TLS provisioned automatically.
- api.hanzo.ai/v2 # Path-based routing supported.
- type: worker # Background workers have no port or domain.
command: node worker.js
healthcheck:
exec: ["node", "healthcheck.js"]
- type: cron
command: node cleanup.js
schedule: "0 */6 * * *" # Standard cron syntax.
env:
- name: DATABASE_URL
secret: true # Resolved from KMS at deploy time.
kms_key: CLOUD_DATABASE_URL # KMS secret key name.
kms_project: hanzo-cloud # KMS project (default: matches app name).
kms_env: prod # KMS environment (default: matches deploy target).
- name: NODE_ENV
value: production # Plaintext env var. Stored in Platform DB.
- name: LOG_LEVEL
value: info
resources:
postgres: # Managed SQL (optional).
version: "16"
storage: 10Gi
redis: # Managed KV (optional).
version: "7"
maxmemory: 256mb
When runtime: auto is specified (or the field is omitted), Platform detects the runtime by inspecting the repository:
| Detection Signal | Runtime | |-----------------|---------| | Dockerfile exists | docker | | package.json with engines.node | node (matching version) | | requirements.txt or pyproject.toml | python | | go.mod | go | | Cargo.toml | rust | | None of the above | Error: cannot detect runtime |
If a Dockerfile is present, it always takes precedence over language-based detection. This ensures that teams with custom build requirements can always escape to full Docker control.
The build pipeline executes in isolated Docker containers. No build shares state with any other build.
1. git clone (shallow, single branch)
│
2. Runtime detection (if runtime: auto)
│
3. Build
├─ Docker runtime: docker build --file <dockerfile> --build-arg ... <context>
├─ Node runtime: Install deps (npm/yarn/pnpm) → run build command → copy output
├─ Python runtime: uv sync → run build command → package with uvicorn
├─ Go runtime: go build -o app . → copy binary into scratch/distroless
└─ Rust runtime: cargo build --release → copy binary into scratch/distroless
│
4. docker tag ghcr.io/hanzoai/<org>-<app>:<git-sha-short>
│
5. docker push ghcr.io/hanzoai/<org>-<app>:<git-sha-short>
│
6. Update deployment record in SQL
│
7. Deploy (see Deployment section)
Build logs are streamed to the Platform UI in real-time via WebSocket. Logs are retained for 30 days.
Build caching: Platform mounts a persistent Docker build cache per application. Subsequent builds reuse layers from previous builds, reducing build times by 50-80% for typical Node.js and Go applications.
Build timeout: Builds that exceed 15 minutes are killed. This prevents runaway builds from blocking the build queue. The timeout is configurable per application.
Platform supports two deployment targets:
For development, staging, and low-traffic services. Platform generates a compose.yml from the application manifest, runs docker compose up -d, and monitors the container health.
Platform DB → Generate compose.yml → docker compose up -d → Health check → Route traffic
This is the default target for new applications. It requires no Kubernetes cluster.
For production services that require horizontal scaling, rolling updates, and resource isolation. Platform generates Kubernetes Deployment, Service, and Ingress manifests from the application manifest and applies them via the Kubernetes API.
Platform DB → Generate K8s manifests → kubectl apply → Wait for rollout → Health check → Route traffic
The Kubernetes target requires a kubeconfig with appropriate RBAC permissions. Platform creates one namespace per organization and one Deployment per application.
All production deployments use rolling updates by default. The sequence:
startup_timeout seconds.failed. Old pods continue serving traffic.The max_surge and max_unavailable fields control the rollout speed. The default (max_surge: 1, max_unavailable: 0) means at most one extra pod is created during the update, and zero pods are taken down until the new pod is healthy. This is the safest configuration -- it trades speed for zero-downtime guarantees.
Rollback is instant because it is an image tag revert, not a rebuild:
Current: ghcr.io/hanzoai/hanzo-cloud:abc123f (deployed 10 minutes ago, broken)
Previous: ghcr.io/hanzoai/hanzo-cloud:def456a (deployed yesterday, known good)
Rollback: Update deployment image tag to def456a → K8s pulls existing image from cache → Pods start in <10s
Platform retains the last 25 deployment records per application. Each record includes the image tag, Git commit SHA, deploy timestamp, deploying user, and deployment status. Rolling back to any of these 25 versions is a one-click operation in the UI or a single CLI command:
hanzo rollback # Roll back to the immediately previous deployment
hanzo rollback --to def456a # Roll back to a specific image tag
hanzo rollback --to 3 # Roll back to the 3rd most recent deployment
When a domain is added to a service's domains list (either in hanzo.yaml or via the UI), Platform:
_hanzo-verify.example.com → <verification-token>).Wildcard certificates are supported for organizations that manage many subdomains (e.g., *.hanzo.ai). These use the DNS-01 challenge and require API credentials for the DNS provider (stored in KMS).
Environment variables marked with secret: true in the application manifest are resolved from KMS (HIP-27) at deploy time:
kms_key, kms_project, and kms_env.When a secret is rotated in KMS, the next deployment of the affected service automatically picks up the new value. For immediate rotation without redeployment, Platform supports a "restart" action that restarts containers with fresh environment variables from KMS.
Platform aggregates logs from all containers and exposes them through:
hanzo logs --follow for tail-like streaming.GET /api/v1/apps/{app}/logs?since=1h&level=error for programmatic access.Logs are retained for 30 days in Platform's database. For long-term retention, logs can be forwarded to an external sink (e.g., Loki, Elasticsearch) via a configurable log drain.
Platform also exposes basic metrics per application:
| Metric | Source | Description | |--------|--------|-------------| | CPU usage | cAdvisor / Docker stats | Per-container CPU utilization | | Memory usage | cAdvisor / Docker stats | Per-container RSS | | Request count | Traefik access logs | HTTP requests per second | | Response time | Traefik access logs | p50, p95, p99 latency | | Error rate | Traefik access logs | 4xx and 5xx responses per second | | Container restarts | Docker / K8s events | Crash loop detection |
Platform enforces organization boundaries at every layer:
hanzo and lux sees services from both orgs. A user with membership in only zoo sees only Zoo services.hanzo namespace cannot reach a database in the lux namespace unless explicitly allowed.// Core deployment types
interface Application {
id: string;
name: string; // e.g., "cloud"
org: string; // e.g., "hanzo"
repo: string; // e.g., "github.com/hanzoai/cloud"
branch: string; // e.g., "main"
runtime: "auto" | "node" | "python" | "go" | "rust" | "docker";
status: "running" | "deploying" | "failed" | "stopped";
currentDeployment: Deployment | null;
domains: string[];
createdAt: string;
updatedAt: string;
}
interface Deployment {
id: string;
appId: string;
imageTag: string; // e.g., "ghcr.io/hanzoai/hanzo-cloud:abc123f"
gitCommit: string; // Full SHA
gitMessage: string; // First line of commit message
status: "building" | "pushing" | "deploying" | "running" | "failed" | "rolled_back";
deployedBy: string; // IAM user ID
startedAt: string;
completedAt: string | null;
buildLogs: string; // URL to build log stream
url: string; // Public URL of the deployed service
}
interface DeploymentEvent {
id: string;
deploymentId: string;
type: "build_started" | "build_completed" | "build_failed"
| "deploy_started" | "deploy_completed" | "deploy_failed"
| "health_check_passed" | "health_check_failed"
| "rollback_initiated" | "rollback_completed";
message: string;
timestamp: string;
}
# Authentication
hanzo login # Opens browser for IAM OAuth login
# Application management
hanzo apps # List applications in your orgs
hanzo apps create --name my-app # Create a new application
hanzo apps delete my-app # Delete an application (requires confirmation)
# Deployment
hanzo deploy # Deploy current branch from hanzo.yaml
hanzo deploy --branch feature/x # Deploy a specific branch
hanzo deploy --image ghcr.io/... # Deploy a pre-built image
# Monitoring
hanzo status # Show status of all applications
hanzo status my-app # Show detailed status of one application
hanzo logs my-app # Stream logs
hanzo logs my-app --since 1h # Logs from the last hour
# Scaling
hanzo scale my-app 5 # Scale to 5 instances
hanzo scale my-app 0 # Scale to zero (stop)
# Rollback
hanzo rollback my-app # Rollback to previous deployment
hanzo rollback my-app --to <tag> # Rollback to specific version
# Environment variables
hanzo env my-app # List env vars (secrets are masked)
hanzo env my-app set KEY=value # Set a plaintext env var
hanzo env my-app set KEY --secret # Set a secret env var (prompts for value)
hanzo env my-app unset KEY # Remove an env var
# Domains
hanzo domains my-app # List domains
hanzo domains my-app add example.com # Add a custom domain
hanzo domains my-app remove ex.com # Remove a domain
Internet
│
┌─────────┴─────────┐
│ Traefik │
│ (TLS termination) │
│ :80 → :443 │
└─────────┬─────────┘
│
┌───────────────┼───────────────┐
│ │ │
platform.hanzo.ai cloud.hanzo.ai *.hanzo.ai ...
│ │ │
│ ┌────────┴────────┐ │
│ │ Deployed Apps │ │
│ │ (containers) │ │
│ └─────────────────┘ │
│ │
┌─────────┴─────────┐ │
│ Hanzo Platform │─────────────────────┘
│ (Node.js/Next.js) │ (manages routing)
│ :3000 │
└────┬─────────┬────┘
│ │
┌────────┴──┐ ┌───┴────────┐ ┌─────────────┐
│ SQL │ │ KV │ │ Hanzo IAM │
│ :5432 │ │ :6379 │ │ hanzo.id │
│ platform │ │ (queues) │ │ (OAuth) │
└────────────┘ └────────────┘ └─────────────┘
┌─────────────┐
│ Hanzo KMS │
│kms.hanzo.ai │
│ (secrets) │
└─────────────┘
┌─────────────┐
│ GHCR │
│ (registry) │
└─────────────┘
Platform authenticates users via Hanzo IAM (HIP-26) using the OAuth 2.0 Authorization Code Grant with PKCE. The integration uses Better Auth, the authentication library used by Dokploy, with a custom hanzo provider:
// platform/pkg/platform/src/lib/auth.ts (simplified)
import { betterAuth } from "better-auth";
const iamUrl = process.env.HANZO_IAM_URL
|| process.env.HANZO_IAM_ENDPOINT
|| process.env.HANZO_IAM_SERVER_URL
|| process.env.IAM_ENDPOINT
|| "https://hanzo.id";
export const auth = betterAuth({
socialProviders: {
hanzo: {
clientId: process.env.HANZO_IAM_CLIENT_ID || process.env.HANZO_CLIENT_ID,
clientSecret: process.env.HANZO_IAM_CLIENT_SECRET || process.env.HANZO_CLIENT_SECRET,
issuer: iamUrl,
authorization: `${iamUrl}/oauth/authorize`,
token: `${iamUrl}/oauth/token`,
userinfo: `${iamUrl}/api/userinfo`,
},
},
});
When a user clicks "Sign in with Hanzo" on the Platform login page:
hanzo.id/oauth/authorize with PKCE challenge.platform.hanzo.ai/callback with authorization code.For users who previously logged in via GitHub (legacy Dokploy flow), Platform matches the IAM email against existing user records. This prevents duplicate identity silos.
Platform uses SQL (HIP-29) to store:
The database is platform on localhost in the Kubernetes cluster .
Built images are pushed to GitHub Container Registry (GHCR) at ghcr.io/hanzoai/. The image naming convention is:
ghcr.io/hanzoai/<org>-<app>:<git-sha-short>
ghcr.io/hanzoai/<org>-<app>:latest
Examples:
ghcr.io/hanzoai/hanzo-cloud:abc123fghcr.io/hanzoai/hanzo-iam:latestghcr.io/hanzoai/lux-gateway:def456aGHCR was chosen over self-hosted registries (Harbor, MinIO-backed) because it requires zero operational overhead and integrates natively with GitHub Actions for CI-triggered builds.
Traefik handles all ingress routing, TLS termination, and load balancing. Platform dynamically updates Traefik's configuration when domains are added, removed, or when deployments change:
Platform supports two deployment triggers:
hanzo deploy in the CLI. This triggers the same build pipeline but with explicit human intent.Platform does NOT replace CI for testing. The expected flow is:
Developer pushes code
│
├─ GitHub Actions: lint, test, type-check
│ │
│ └─ Tests pass → merge to main
│
└─ Platform webhook: build → push → deploy → health check
Tests run in CI (GitHub Actions). Deployment runs in Platform. These are separate concerns.
Every build runs in a fresh Docker container with:
build.network: none in the manifest.docker commands. Building Docker images uses BuildKit in rootless mode.Secrets follow a strict lifecycle:
--env or Kubernetes envFrom).[REDACTED].docker history of built images is inspected to ensure no ENV or ARG instructions contain secret values. If found, the build is rejected with a clear error message.In Kubernetes deployments, each organization's namespace has a default-deny NetworkPolicy:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: hanzo
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress: [] # Deny all ingress by default
egress:
- to: # Allow DNS resolution
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- port: 53
protocol: UDP
Applications must explicitly declare their network dependencies in the manifest. Platform generates NetworkPolicy rules that allow only declared communication paths. For example, if cloud declares resources.postgres, Platform creates a NetworkPolicy allowing egress from the cloud pods to the SQL service on port 5432.
Every action in Platform is logged to the audit_log table:
| Field | Type | Description | |-------|------|-------------| | id | UUID | Event ID | | timestamp | timestamptz | When the event occurred | | user_id | string | IAM user who performed the action | | user_email | string | Email for human-readable logs | | org | string | Organization context | | app | string | Application name (if applicable) | | action | string | deploy, rollback, scale, env.set, env.delete, domain.add, domain.remove, app.create, app.delete | | details | jsonb | Action-specific metadata (image tag, scale count, env var name, etc.) | | ip_address | inet | Client IP | | user_agent | string | Client user agent |
Audit logs are retained for 1 year. They are queryable via the Platform API and UI. Global admins can export audit logs in CSV or JSON format for compliance reporting.
Platform defines three roles per organization:
| Role | Permissions | |------|------------| | Viewer | View applications, deployments, logs. Cannot modify anything. | | Developer | Everything Viewer can do, plus: deploy, rollback, set env vars, manage domains. | | Admin | Everything Developer can do, plus: create/delete applications, manage team members, view audit logs. |
Role assignments are derived from IAM group memberships. The mapping is configurable per organization:
# Platform role mapping (stored in Platform DB)
org: hanzo
roles:
admin: ["hanzo-platform-admins"] # IAM group name
developer: ["hanzo-engineers"]
viewer: ["hanzo-all"]
Copyright and related rights waived via CC0.