Overview
AppCluster is a lightweight, .NET-native orchestration platform for self-hosted multi-server environments. It deploys .NET applications as isolated OS processes — no containers, no image registries, no Kubernetes.
Applications are packaged as dotnet publish output in a .zip file, uploaded to the cluster,
and executed via dotnet {entry.dll} under a dedicated OS user with full resource isolation.
Key design principles:
- Single binary — every node runs the same
AppCluster.Nodeprocess - No external database — state replicated leader-to-follower, persisted per node in SQLite
- Reconciliation-based — desired state vs. actual state, continuously converged
- Plugin architecture — all functionality in hot-reloadable plugins
Architecture
Every server runs the same AppCluster.Node binary, which combines the REST API, leader/follower
state replicator, plugin host, and local reconciler in a single ASP.NET Core application.
A custom leader-follower replicator elects a single leader for cluster-wide writes. State changes stream to every follower over SSE, and each node persists a single-row SQLite snapshot of the full cluster state. No separate control plane, no external database — HA requires 3 or 5 nodes.
Note: earlier releases used Raft (DotNext.Net.Cluster). As of April 2026 this was replaced with the simpler custom replicator described above.
How Deployment Works
- User calls REST API on any node — writes are forwarded to the current leader
- Leader applies the command, persists a new snapshot, and emits a state-change event
- Every follower receives the change over SSE and updates its own snapshot
- Each node's
LocalReconcilercompares desired vs. actual state - Reconciler starts, stops, or restarts apps as needed using plugins
Node Components
Each node runs these components in a single process:
- REST API — Minimal API endpoints for all cluster operations
- Cluster Replicator — Leader election, state replication, SQLite snapshot store
- Plugin Host — Loads and manages hot-reloadable plugins via AssemblyLoadContext
- Module Host — Runs optional standalone modules (reconcilers, LB, DNS, alerting, etc.) and multiplexes their UIs
- LocalReconciler — Continuous reconciliation loop
- YARP Reverse Proxy — Routes external traffic to running apps
- Health Checker — HTTP/TCP probes with crash restart
Quick Start
Prerequisites: .NET 10 SDK
# Build the entire solution
dotnet build
# Run a single node
dotnet run --project src/AppCluster.Node
# The dashboard is available at http://localhost:5100The node starts with four ports:
| Port | Purpose |
|---|---|
| 5100 | REST API + Web GUI |
| 5101 | Leader / follower state replication |
| 5102 | Internal cluster communication |
| 5103 | HTTPS (with SNI certificate selection) |
Cluster Setup
For high availability, run 3 or 5 nodes. Start the first node normally — it becomes the leader:
# Start the first node
dotnet run --project src/AppCluster.NodeGet the join key from the first node:
curl http://node1:5100/api/cluster/join-keyJoin additional nodes to the cluster:
curl -X POST http://node2:5100/api/cluster/join \
-H "Content-Type: application/json" \
-d '{"leaderAddress": "http://node1:5100", "joinKey": "<key>"}'Or run with CLI arguments:
dotnet run --project src/AppCluster.Node -- --data ./node2 --join <key>Deploying Applications
Upload an Artifact
Package your application as a dotnet publish output compressed into a .zip file,
then upload it to the cluster:
curl -X POST http://localhost:5100/api/artifacts/upload \
-F "file=@myapp.zip"The cluster verifies the artifact's SHA256 hash and distributes it to all nodes.
Deploy
curl -X POST http://localhost:5100/api/deploy \
-H "Content-Type: application/json" \
-d '{"appName": "myapp", "artifactId": "<id>", "instances": 2}'Deployment Strategies
- Replace — Stop old instances, start new ones
- Rolling — Gradually replace instances one at a time
- Blue-Green — Run both versions, switch traffic after health checks pass
All strategies support automatic rollback if health checks fail.
Declarative Configuration
Define your desired state in YAML or JSON and apply it with the CLI:
dotnet run --project src/AppCluster.Cli -- apply cluster.yamlExample cluster.yaml:
applications:
- name: hello-app
artifact: hello-world-1.0.0-20260405182710
entryAssembly: HelloApp.dll
replicas: 2
resources:
cpu: "0.5"
memory: "256Mi"
maxProcesses: 64
env:
ASPNETCORE_URLS: "http://0.0.0.0:8080"
secrets:
- name: DATABASE_URL
appId: hello-appCLI Reference
| Command | Description |
|---|---|
appcluster validate <file> | Validate config file syntax |
appcluster diff <file> | Show what would change |
appcluster apply <file> | Apply desired state to the cluster |
appcluster export -o <file> | Export current cluster state |
REST API Reference
All endpoints are under /api. Authentication is via the X-Api-Key header.
Write requests on any node are automatically forwarded to the current leader.
Nodes & Status
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/nodes | List cluster nodes |
| GET | /api/apps | List deployed applications |
| GET | /api/deployments | List deployments |
Deployment
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/deploy | Deploy an application |
| POST | /api/apps/{id}/stop | Stop an application |
| DELETE | /api/apps/{id} | Remove an application |
| POST | /api/apps/{id}/rollback | Rollback to previous version |
| GET | /api/apps/{id}/history | Get deployment history |
Artifacts
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/artifacts/upload | Upload a .zip artifact |
| GET | /api/artifacts | List artifacts |
| GET | /api/artifacts/{id} | Get artifact details |
Secrets
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/secrets | Create a secret |
| GET | /api/secrets | List secrets (names only) |
| DELETE | /api/secrets/{id} | Delete a secret |
Routes
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/routes | Create a route |
| GET | /api/routes | List routes |
| DELETE | /api/routes/{id} | Delete a route |
Cluster Management
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/cluster/join | Join a node to the cluster |
| GET | /api/cluster/status | Get cluster status |
| GET | /api/cluster/join-key | Get the join key |
| POST | /api/cluster/rotate-key | Rotate the join key |
Authentication
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/auth/keys | Create an API key |
| GET | /api/auth/keys | List API keys |
| DELETE | /api/auth/keys/{id} | Revoke an API key |
Audit & Webhooks
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/audit | Query audit log. Filters: userId (actor username), appId, action, ns, since, category, limit |
| GET | /api/audit/stream | SSE stream of new audit entries (replays a small backlog first) |
| POST | /api/webhooks | Create a webhook |
| GET | /api/webhooks | List webhooks |
| DELETE | /api/webhooks/{id} | Delete a webhook |
Users, Sessions & Layered RBAC v1.3.0+
The cluster has two layers of access control. Layer 1 is the existing
namespace role system (viewer / operator / admin / super-admin per namespace).
Layer 2 adds per-resource grants so you can give a user a specific capability
(e.g. instance:debug on qa-*) without making them admin
cluster-wide. The two layers compose with OR semantics — layer 2 only grants,
never restricts.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/auth/permissions?resource=&action=&namespace=&name= |
Decision API. Returns { allowed, reason, effectiveRole, ... } combining layer 1 + layer 2 |
| GET | /api/users/{userId}/permissions |
List grants for a user. Admin-or-self |
| POST | /api/users/{userId}/permissions |
Create a grant. Body: { resourceType, action, namespace?, resourceName?, description? } |
| DELETE | /api/users/{userId}/permissions/{permissionId} |
Revoke a grant |
| GET | /api/permissions/resource-types |
Aggregator across every running module's /module-ui — powers the GUI's grant editor dropdowns |
| POST | /api/users/{userId}/lock |
Block login pending review. Body: { reason? } |
| POST | /api/users/{userId}/unlock |
Restore login access |
| POST | /api/users/{userId}/force-logout |
Bump TokenInvalidationCutoffAt — rejects all tokens issued before now. Admin-or-self |
| POST | /api/users/{userId}/2fa/admin-disable |
Admin recovery path when a user has lost their authenticator app |
| GET | /api/users/{userId}/sessions |
List active sessions with IP / user-agent / last-seen / isCurrent flag. Admin-or-self |
| DELETE | /api/users/{userId}/sessions/{sessionId} |
Revoke one session — marks it Revoked and deletes its token |
| POST | /api/auth/logout |
Mark the current session revoked + delete its token (frontend logout calls this best-effort before clearing localStorage) |
Databases v1.2.1+
DatabaseManager + Postgres / MySQL / MSSQL providers broker per-app and
per-developer credentials against EXISTING external database servers — the
cluster does not host the engines. Apps declare their database needs in
DesiredDeployment.DatabaseRefs; the reconciler auto-provisions a SQL
user, grant, and connection-string env var before pod start.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/databases/servers | Register a database server (Postgres / MySQL / MSSQL) |
| GET | /api/databases/servers | List registered servers |
| DELETE | /api/databases/servers/{id} | Deregister + cascading cleanup of secrets and grants |
| POST | /api/databases/servers/{id}/test | Probe connectivity via the manager module |
| GET | /api/databases/servers/{id}/databases | List managed databases on the server |
| POST | /api/databases/servers/{id}/grants/developers | Create a per-developer grant; returns plaintext password + connection string once |
| GET | /api/databases/grants/apps · /grants/developers | List app or developer grants |
| GET / POST | /api/databases/servers/{id}/drift | Drift report (orphan grants / orphan users / expired-but-live) · /scan forces a fresh scan |
| POST | /api/databases/servers/{id}/query | Ad-hoc query for the in-browser workbench. Body: { database, sql, maxRows? } |
Live Debugger v1.0.0+
The Debugger module attaches netcoredbg / vsdbg / msvsmon to a running
pod and returns ready-to-paste IDE configs. Sessions are time-boxed (default 30 min,
hard cap 240) and admin-gated — layer-2 RBAC permits scoping
(instance, debug) grants per namespace.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/instances/{id}/debug-attach | Spawn a debugger bound to the pod's PID; returns VS Code launch.json + VS connection string + Rider profile |
| GET | /api/debug-sessions[?appId=] | List active sessions across the cluster (fan-out) |
| DELETE | /api/debug-sessions/{id} | End a session; the IDE is disconnected |
Platform Isolation
AppCluster provides OS-level isolation for each deployed application:
| Feature | Linux | Windows |
|---|---|---|
| User isolation | Dedicated OS user (0700) | Local user + NTFS ACLs |
| Filesystem | Home directory per app | Per-app directory |
| Network | Network namespaces + veth pairs | Windows Firewall rules |
| Process visibility | /proc with hidepid=2 | Job Objects |
| Resource limits | cgroups v2 | Job Objects |
| Security profiles | AppArmor | N/A |
Port Mapping
- Linux: Network namespaces allow apps to use hardcoded ports internally. External access is mapped through veth pairs.
- Windows: Applications must read their port from the
ASPNETCORE_URLSenvironment variable.
Secrets Management
Secrets are encrypted with AES-256-GCM and stored in the replicated cluster state.
They are injected into application processes as environment variables at startup — never written to disk in plain text.
Secrets are namespace-scoped and can be referenced by modules using the secret:namespace/key syntax.
# Create a secret
curl -X POST http://localhost:5100/api/secrets \
-H "Content-Type: application/json" \
-d '{"name": "DATABASE_URL", "value": "postgres://...", "appId": "myapp"}'Routing & Reverse Proxy
AppCluster includes a built-in YARP reverse proxy that routes external traffic to your applications. Routes are configured dynamically via the API — no restart required.
# Create a route
curl -X POST http://localhost:5100/api/routes \
-H "Content-Type: application/json" \
-d '{"host": "myapp.example.com", "appName": "myapp", "port": 8080}'Health Checks
Configure HTTP or TCP health probes for each application. Failed health checks trigger automatic restart with exponential backoff.
- HTTP probes — Send GET requests to a configurable path, expect 2xx response
- TCP probes — Attempt TCP connection to the application port
- Crash restart — Processes that exit unexpectedly are restarted automatically
- Exponential backoff — Prevents restart loops for persistently failing apps
Authentication & RBAC
AppCluster supports multiple authentication methods for both interactive users and programmatic clients:
- Username + password with BCrypt hashing
- TOTP 2FA (Google Authenticator, Authy) with recovery codes
- WebAuthn / Passkeys — Windows Hello, Touch ID, FIDO2 security keys
- LDAP / Active Directory via the
ActiveDirectorymodule with multi-provider support and periodic user/group sync - API tokens — per-user bearer tokens with optional role downscoping
- Legacy API key — the
X-Api-Keyheader is still supported for backward compatibility
RBAC is namespace-scoped: each user has a role per namespace. Roles, from least to most privileged:
- Viewer — read-only access
- Operator — deploy, stop, and manage applications
- Admin — full access inside a namespace, including secrets, routes, and users
- Super-Admin — cluster-wide administration (join keys, nodes, module install)
A default super-admin (admin / admin) is created on first run — change it immediately.
Plugins
All core functionality lives in 5 hot-reloadable plugins loaded via collectible AssemblyLoadContext.
Plugins reference only the AppCluster.Contracts assembly and can be updated without restarting the node.
| Plugin | Purpose |
|---|---|
| ArtifactStore | Artifact upload, storage, and SHA256 verification |
| ProcessManager | Process start, stop, and lifecycle monitoring |
| IsolationManager | OS user isolation + AppArmor (Linux) / NTFS ACLs (Windows) |
| ResourceManager | cgroups v2 (Linux) / Job Objects (Windows) resource limits |
| HealthReporter | Health checks and /proc metrics collection |
See the Plugins page for detailed descriptions and downloads.
Modules
Optional modules are standalone ASP.NET Core apps deployed to the system namespace
that talk to the node over named-pipe IPC. Each module exposes /health, /ready,
and /module-ui endpoints, and its UI is multiplexed into the main dashboard.
Provider modules (DNS, SSL, load balancer, notification channels) follow a standard
/api/provider/* pattern with capability discovery so the orchestrating module can plug
in new backends without code changes.
| Module | Purpose |
|---|---|
| Reconciler.Dotnet | .NET app reconciler — deploy, health, exec. Auto-installed on node startup. Multi-replica per node with dynamic port allocation. |
| Reconciler.Static | Static site reconciler using per-app Caddy processes |
| Reconciler.Docker | Docker container reconciler — pulls images from the Registry module |
| Registry | Container registry integration — reads registry:2 catalogs, manages credentials, resolves digests |
| LoadBalancers | Central load balancer manager; delegates to provider modules (LbNginx, LbHaproxy) |
| LbNginx | Nginx provider — SSH-based config sync, server provisioning, ACME challenges |
| LbHaproxy | HAProxy provider — Data Plane API with SSH-fallback sync |
| CronJobs | Master-coordinated cron scheduling via Cronos; SSH execution; SQLite history |
| Jobs | Interval-based HTTP job scheduling |
| Webhooks | Webhook delivery for cluster events |
| DnsManager | DNS orchestration — discovers and routes to provider modules |
| DnsProviderCloudflare | Cloudflare DNS provider (zones, records, Origin CA) |
| SslProviderLetsencrypt | Let's Encrypt certificates (HTTP-01 and DNS-01 challenges) |
| SslProviderInternalCa | Internal certificate authority with PEM download |
| BackupRestore | Encrypted backup / restore ZIP snapshots with config, artifacts, and secrets |
| Alerting | Alert rules on metrics and logs with acknowledge flow |
| SmtpProvider | SMTP email notification channel (MailKit) |
| MetricsCollector | Node and app metric collection; dashboard UI with sparklines and gauges |
| ActiveDirectory | LDAP / AD authentication and periodic user/group sync |
Configuration
Ports
| Port | Purpose | Configurable |
|---|---|---|
| 5100 | REST API + Web GUI | Yes |
| 5101 | Leader / follower state replication | Yes |
| 5102 | Internal cluster communication | Yes |
| 5103 | HTTPS (with SNI) | Yes |
Data Directory
By default, node data is stored in a data/ directory relative to the working directory.
Override with the --data CLI argument:
dotnet run --project src/AppCluster.Node -- --data /var/lib/appclusterTechnology Stack
- .NET 10, ASP.NET Core 10 (Minimal APIs)
- Custom leader/follower state replicator — SSE stream + SQLite-persisted snapshots
- YARP — Reverse proxy
- Microsoft.Data.Sqlite — state, logs, deployment history, alerting, backups, AD cache
- Serilog — Structured logging
- YamlDotNet — Config parsing
- SSH.NET — Load balancer and cron remote execution
- Certes — ACME / Let's Encrypt
- MailKit — SMTP email
- Cronos — Cron expression parsing
- BCrypt.Net-Next, Fido2, Otp.NET — Password, WebAuthn, and TOTP auth
- System.DirectoryServices.Protocols — LDAP / Active Directory