System Architecture¶
Contributor documentation
This section explains how the Manager, Nodes, and cluster work internally and how to contribute to the opensource cluster itself. If you are a developer who just wants to run your own code on the cluster, see the Users documentation instead.
Overview¶
graph TB
subgraph "Management Layer"
WebUI[Web UI<br/>EJS Templates]
API[Node.js API Server]
DB[(PostgreSQL)]
LDAP[LDAP Gateway<br/>NodeJS]
end
subgraph "Network Services"
DNS[DNSMasq<br/>DHCP + DNS]
NGINX[NGINX<br/>Reverse Proxy]
end
subgraph "External Services"
PushNotif[Push Notification Service<br/>2FA Authentication]
end
subgraph PVE[Proxmox Cluster]
LXC[LXC Container]
end
WebUI --> API
API --> DB
API --> PVE
API --> DNS
API --> NGINX
API --> PushNotif
LDAP --> DB
LDAP --> PushNotif
LXC -.-> LDAP
DNS --> LXC
NGINX --> LXC
classDef management fill:#e1f5ff,stroke:#01579b
classDef network fill:#f3e5f5,stroke:#4a148c
classDef compute fill:#e8f5e9,stroke:#1b5e20
classDef external fill:#fff3e0,stroke:#e65100
class WebUI,API,DB,LDAP management
class DNS,NGINX network
class LXC compute
class PushNotif external
Components¶
| Component | Role |
|---|---|
| Proxmox VE 13+ | Hypervisor — manages LXC containers via REST API. Nodes are registered Proxmox servers. |
| DNSMasq | DHCP + DNS. Auto-assigns IPs to containers, provides internal name resolution (container.cluster.internal). |
| NGINX | Reverse proxy — L7 (HTTP/HTTPS with auto TLS via ACME) and L4 (TCP port mapping). Config auto-generated from container services. |
| LDAP Gateway | Node.js LDAP server (source). Reads users/groups from the DB; containers authenticate via PAM/SSSD. |
| Push Notification Service | 2FA via push notifications (source). Configured in Settings. Used by LDAP gateway when AUTH_BACKENDS includes notification. |
| Database | PostgreSQL via Sequelize ORM. Stores users, groups, sites, nodes, containers, and service config. |
Data Flow¶
Container Creation¶
sequenceDiagram
participant User
participant API
participant Proxmox
participant LXC
participant DNSMasq
participant DB
participant NGINX
User->>API: Create Container Request
API->>Proxmox: Create LXC via API
Proxmox->>LXC: Create
LXC->>DNSMasq: Retrieve IP via DHCP
DNSMasq-->>LXC: IP assigned
API->>DNSMasq: Retrieve IP for new LXC
DNSMasq-->>API: LXC IP address
API->>DB: Store LXC + IP information
API-->>User: Container ready
NGINX->>API: Retrieve updated config
API-->>NGINX: Updated routing config
User Authentication¶
sequenceDiagram
participant User
participant Container
participant LDAP
participant DB
participant PushNotif as Push Notification<br/>Service
User->>Container: SSH login attempt
Container->>LDAP: LDAP bind request
LDAP->>DB: Verify credentials
DB-->>LDAP: Password validated
alt 2FA Enabled
LDAP->>PushNotif: Send notification (username)
PushNotif->>User: Push notification to device
User->>PushNotif: Approve/Reject
PushNotif-->>LDAP: User decision
alt Approved
LDAP-->>Container: Authentication success
Container-->>User: Login granted
else Rejected or Timeout
LDAP-->>Container: Authentication failed
Container-->>User: Access denied
end
else 2FA Disabled
LDAP-->>Container: Authentication success
Container-->>User: Login granted
end
HTTP Service Exposure¶
sequenceDiagram
participant Client
participant NGINX
participant ACME
participant DNSMasq
participant Container
Note over NGINX: Initial setup
NGINX->>ACME: Request certificate
ACME-->>NGINX: Certificate issued
Note over Client: User request
Client->>NGINX: HTTPS request (app.example.com)
NGINX->>DNSMasq: Resolve container IP
DNSMasq-->>NGINX: Container IP address
NGINX->>Container: Forward request
Container-->>NGINX: Response
NGINX-->>Client: HTTPS response
Authenticated HTTP Services¶
When authRequired is enabled on an HTTP service, NGINX uses the auth_request module to authenticate requests against an oauth2-proxy process before proxying. The domain's authServer is the address of that process, e.g. http://127.0.0.1:4180 (see External Domains). The manager itself no longer provides forward-auth — administrators run and configure oauth2-proxy.
NGINX proxies the whole /oauth2/* subtree (and the auth_request check at /oauth2/auth) to authServer, passing the app's own Host through. oauth2-proxy terminates those requests itself and builds redirect URIs and cookies against the correct app hostname. If the admin wants oauth2-proxy behind the same load-balancer IP, they expose its port via an L4 (stream {}) passthrough.
sequenceDiagram
participant Client
participant NGINX
participant OAuth2Proxy as oauth2-proxy
participant Container
Client->>NGINX: GET app.example.com/page
NGINX->>OAuth2Proxy: Subrequest: GET /oauth2/auth (Host: app.example.com)
alt 202 (authenticated)
OAuth2Proxy-->>NGINX: 202 + X-Auth-Request-* headers
NGINX->>Container: Proxied request + identity headers
Container-->>NGINX: Response
NGINX-->>Client: Response
else 401 (unauthenticated)
OAuth2Proxy-->>NGINX: 401
NGINX-->>Client: 302 → app.example.com/oauth2/sign_in?rd=https://app.example.com/page
end
NGINX captures identity headers from the oauth2-proxy subrequest (X-Auth-Request-User, X-Auth-Request-Preferred-Username, X-Auth-Request-Email, X-Auth-Request-Groups, and the access token) and forwards them to the backend container via proxy_set_header under a stable contract (X-User, X-Preferred-Username, X-Email, X-Groups, X-Access-Token). This requires oauth2-proxy to run with --set-xauthrequest (and --pass-access-token for the token). See Adding Authentication for how applications consume these.
If authRequired is enabled but no authServer is configured on the domain, NGINX serves a 503 error page. (A malformed authServer cannot reach the template — it is rejected by the API on write.)