Overview

KubePanel is a Kubernetes-native web hosting control panel built on a strict separation of concerns. Three components handle three distinct responsibilities:

Design principle: Django handles business logic, the Domain CR is the contract between Django and the operator, and the Kopf operator owns all infrastructure. Secrets never touch the Django database — they are generated and stored only in Kubernetes.

Django Dashboard
Business logic, user management, quota enforcement, UI

Handles authentication, multi-tenancy, package quotas, domain ownership, email accounts, DNS, and audit logging. Talks to Kubernetes only to create/update/delete Domain CRs and read their status.

Creates / Updates / Deletes
Domain Custom Resource
kubepanel.io/v1alpha1  Kind: Domain

The contract between Django and the operator. Specifies domain name, workload configuration, resource limits, email/DNS/backup settings, and receives status updates (SFTP port, DB credentials, phase, conditions).

Watches & Reconciles
Kopf Operator
Infrastructure controller — owns all Kubernetes resources

Watches Domain CRs for changes and reconciles the full infrastructure: namespace, PVC, secrets (SFTP, DB, DKIM), ConfigMaps (nginx, app config), Deployment, Services, Ingress, and the central DKIM configuration. Self-heals drift every 5 minutes.

Creates & Manages
Kubernetes Resources
Per-domain namespace  •  Namespace: dom-example-com
PVC
Persistent storage for site files (Linstor)
Secrets
SFTP, database, DKIM credentials
ConfigMaps
nginx config, app config
Deployment
App + nginx + SFTP containers
Services
ClusterIP (web), NodePort (SFTP)
Ingress
TLS termination via cert-manager

The Three Layers

Django Dashboard — Business Logic

The Django application handles everything that requires business rules: who owns which domain, what resource limits apply, billing tiers, multi-tenancy, and audit trails. It communicates with Kubernetes exclusively through the Domain CR API.

ResponsibilityWhy Here
User accounts & authenticationSessions, 2FA, login rate limiting
Domain ownershipMulti-tenancy, authorization checks
Packages & quotasBusiness rules, limit enforcement
WorkloadType & WorkloadVersionAdmin-configurable runtime options
Mail users & aliasesBusiness entities with FK relationships
Cloudflare API tokensUser-specific encrypted credentials
DNS zones & recordsUser-managed via UI, synced to Cloudflare
Audit loggingBusiness audit trail (every operation logged)

Domain CR — The Contract

The Domain Custom Resource is the single source of truth for a domain's desired infrastructure state. Django writes to it; the operator reads from it. Status flows back the other way: the operator writes SFTP port, database credentials reference, phase, and conditions; Django reads them to display in the UI.

apiVersion: kubepanel.io/v1alpha1
kind: Domain
metadata:
  name: example-com
spec:
  domainName: example.com
  workload:
    type: php
    version: "8.2"
    image: docker.io/kubepanel/php82:v1.0
    port: 9001
    proxyMode: fastcgi
  resources:
    storage: 10Gi
    limits:
      cpu: "500m"
      memory: 512Mi
  email:
    enabled: true
    dkimSecretRef: dkim-example-com
status:
  phase: Ready
  sftp:
    port: 30042
    username: example-com
  database:
    host: mariadb.kubepanel.svc.cluster.local
    name: example_com

Kopf Operator — Infrastructure Manager

The operator is a Python application using the Kopf framework. It watches all Domain CRs cluster-wide and reconciles the full infrastructure stack for each domain. Key behaviors:

  • On create: provisions the full stack — namespace, PVC, secrets, ConfigMaps, Deployment, Services, Ingress, MariaDB database, DKIM keys
  • On update: patches only what changed (e.g. workload image, resource limits, nginx config)
  • On delete: removes all Kubernetes resources, drops the MariaDB database, removes DKIM keys from central OpenDKIM
  • On resume: operator restart — skips full reconciliation if the domain is already Ready
  • Timer (every 5 min): reconciliation loop that corrects any drift from desired state

Request Flow

A typical HTTP request from a visitor to a KubePanel-hosted site:

Visitor
Browser request to example.com
nginx-ingress + ModSecurity WAF
TLS termination • WAF rules • Real IP extraction

Handles TLS, sets X-Forwarded-For, applies GlobalWAF rules, routes to the domain's ClusterIP Service.

nginx sidecar (domain pod)
DomainWAF • FastCGI / HTTP proxy • Cache

Applies per-domain WAF rules, serves static files, proxies dynamic requests to the app container.

App container
PHP-FPM / Gunicorn / Node.js — the hosted application

Kubernetes Resources Per Domain

Every domain gets its own namespace (dom-example-com) containing the following resources:

ResourceTypePurpose
dataPersistentVolumeClaimWebsite files storage (Linstor)
sftp-credentialsSecretSSH keypair, SFTP password, shadow hash
db-credentialsSecretMariaDB database name, user, password
nginx-configConfigMapAuto-generated nginx.conf with proxy mode
app-configConfigMapphp.ini / gunicorn.conf / app settings
{domain}DeploymentApp + nginx + SFTP containers
{domain}-svcService (ClusterIP)Web traffic from ingress
{domain}-sftpService (NodePort)External SFTP access
{domain}IngressTLS termination, routing from nginx-ingress
backupPVCBackup storage (VolumeSnapshots + DB dumps)

Storage — Linstor / DRBD

KubePanel uses Linstor (Piraeus Operator) with DRBD for replicated block storage. Each domain's data PVC is replicated across nodes in real time. If a node fails, the PVC is immediately available on a healthy node.

ResourceStorageClassDetails
Domain data PVClinstor-scReplicated, RWO, size configurable per domain
MariaDB PVClinstor-sc5Gi, shared database instance
SMTP PVClinstor-sc5Gi, Dovecot Maildir storage
VolumeSnapshotspiraeus-snapshotsUsed for backup/restore

Networking & Ingress

External HTTP/HTTPS traffic flows through nginx-ingress with ModSecurity WAF enabled. SFTP is exposed via NodePort services on the cluster's public node IPs.

nginx-ingress is configured with real_ip_header X-Forwarded-For and set_real_ip_from 10.0.0.0/8 to ensure real client IPs reach application logs and WAF rules.

Traffic TypeEntry PointProtocol
HTTPnginx-ingress NodePort :80HTTP → redirected to HTTPS
HTTPSnginx-ingress NodePort :443TLS terminated, proxied to ClusterIP
SFTPPer-domain NodePort (30000–32767)SSH/SFTP direct to pod
SMTP Submissionnginx-ingress TCP :587STARTTLS to Postfix
IMAPSnginx-ingress TCP :993TLS to Dovecot
POP3Snginx-ingress TCP :995TLS to Dovecot

Email Stack

All email services run in the kubepanel namespace and serve all hosted domains from a single shared stack.

ComponentRolePort
PostfixMTA — SMTP submission + outbound delivery587, 465
DovecotIMAP/POP3 — mailbox access993, 995
OpenDKIMDKIM signing milter (all domains)8891 (milter)
RspamdSpam / malware filtering milter11332 (milter)

DKIM private keys for all domains are stored in the dkim-keys Secret. The operator updates this secret and restarts OpenDKIM whenever a domain is added, updated, or deleted. See Email Hosting for the full DKIM flow.

Database

A single shared MariaDB instance runs in the kubepanel namespace. Each hosted domain gets its own database and user with auto-generated credentials stored only in a Kubernetes Secret — never in Django's database.

Database credentials are generated by the operator on domain creation and stored exclusively in the db-credentials Secret in the domain's namespace. Django never sees the raw password.

Isolation Model

Each domain is fully isolated from every other domain at multiple layers:

LayerMechanismWhat It Prevents
FilesystemSeparate PVC per domainCross-domain file access
ProcessSeparate container per domainProcess-level interference
NetworkKubernetes namespace + network policiesLateral network movement
ResourcesCPU/memory limits per containerNoisy neighbor resource starvation
DatabaseSeparate DB + user per domainCross-domain data access
SecretsSecrets scoped to domain namespaceCredential leakage between domains

Secrets Management

KubePanel never stores sensitive credentials in Django's database. All secrets are generated by the operator and stored in Kubernetes Secrets within the domain's namespace.

Secret NameNamespaceContents
sftp-credentialsdom-{domain}SSH public/private key, SFTP password, shadow hash
db-credentialsdom-{domain}Database name, username, password
dkim-{domain}kubepanelDKIM private key, public key, DNS TXT record
dkim-keyskubepanelAll domain private keys (used by OpenDKIM)
mariadb-authkubepanelMariaDB root credentials (operator reads only)

Namespaces

NamespaceContains
kubepanelDashboard, Operator, MariaDB, Postfix/Dovecot, OpenDKIM, Rspamd, shared secrets
dom-{domain}Per-domain: PVC, Secrets, ConfigMaps, Deployment, Services, Ingress
ingress-nginxnginx-ingress controller, ModSecurity WAF
cert-managercert-manager, ClusterIssuer (letsencrypt-prod)
piraeus-systemLinstor / Piraeus storage operator

Custom Resource Definitions

KindGroupScopePurpose
Domainkubepanel.io/v1alpha1ClusterPer-domain infrastructure spec
Backupkubepanel.io/v1alpha1NamespacedBackup job trigger and status
Restorekubepanel.io/v1alpha1NamespacedRestore job trigger and status
DNSZonekubepanel.io/v1alpha1ClusterCloudflare DNS zone and records
GlobalWAFkubepanel.io/v1alpha1ClusterGlobal ModSecurity rules
DomainWAFkubepanel.io/v1alpha1NamespacedPer-domain WAF rules
GlobalL3Firewallkubepanel.io/v1alpha1ClusterCalico network policies
SMTPFirewallkubepanel.io/v1alpha1ClusterSMTP-level blocking rules
Licensekubepanel.io/v1alpha1ClusterLicense key and tier enforcement

Port Reference

PortProtocolServiceNotes
80TCPnginx-ingressHTTP — redirects to HTTPS
443TCPnginx-ingressHTTPS — all hosted domains
587TCPPostfixSMTP Submission (STARTTLS)
465TCPPostfixSMTPS
993TCPDovecotIMAPS
995TCPDovecotPOP3S
30000–32767TCPSFTPNodePort per domain
3306TCPMariaDBInternal cluster only
8891TCPOpenDKIMMilter — internal only
11332TCPRspamdMilter — internal only

Want to explore a specific feature in depth? See the Features section or jump to Security, Email, or Backup detail pages.