Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions deploy/kubernetes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Deploying Rackpad on Kubernetes

Plain, dependency-free manifests for running Rackpad on any Kubernetes cluster.
They mirror [`docker-compose.yml`](../../docker-compose.yml): a single replica,
a non-root container, a read-only root filesystem, and the SQLite database on a
PersistentVolume.

## Quick start

```bash
# Apply everything (namespace, PVC, deployment, service):
kubectl apply -k deploy/kubernetes/

# ...or apply the files individually:
kubectl apply -f deploy/kubernetes/namespace.yaml
kubectl apply -f deploy/kubernetes/pvc.yaml
kubectl apply -f deploy/kubernetes/deployment.yaml
kubectl apply -f deploy/kubernetes/service.yaml
```

Reach the UI with a port-forward:

```bash
kubectl -n rackpad port-forward svc/rackpad 3000:3000
# open http://localhost:3000
```

To expose it permanently, edit and apply [`ingress.yaml`](./ingress.yaml)
(set the host, `ingressClassName`, and TLS), or use a LoadBalancer service.

## What's in here

| File | Purpose |
| --- | --- |
| `namespace.yaml` | `rackpad` namespace |
| `pvc.yaml` | 2Gi RWO volume for the SQLite DB at `/data` |
| `deployment.yaml` | Single-replica, non-root, read-only-rootfs deployment |
| `service.yaml` | ClusterIP service on port 3000 |
| `ingress.yaml` | Optional ingress example (commented defaults) |
| `kustomization.yaml` | Bundles the core resources for `kubectl apply -k` |

## Notes

- **Image tag** — the manifests use `:latest`. Pin to a
[released tag](https://github.com/Kobii-git/rackpad/releases) for production.
- **Storage** — the PVC uses the cluster's default StorageClass. SQLite relies
on file locking, so prefer block storage (iSCSI, local-path, Ceph RBD) over
NFS. Never run more than one replica (`strategy: Recreate`, `replicas: 1`).
- **Configuration** — every environment variable from the Compose files is
supported. The deployment sets the common ones; uncomment `APP_URL` /
`TRUST_PROXY` when running behind an ingress, and add the `OIDC_*` variables
to enable SSO (see [`docs/OIDC.md`](../../docs/OIDC.md)).

## Network discovery on Kubernetes

Discovery and SNMP have networking requirements that the minimal-privilege
deployment above deliberately does **not** grant. See
[`docs/DISCOVERY_DEPLOYMENT.md`](../../docs/DISCOVERY_DEPLOYMENT.md) for the full
background; the short version for Kubernetes:

- **Layer-3 scans (ICMP / TCP / HTTP reachability) and SNMP discovery** work
from a normal pod — they only need routing to your device subnets plus a raw
socket. Grant `NET_RAW` (and `NET_BIND_SERVICE` for the SNMP trap listener).
Because Kubernetes can't give ambient capabilities to a non-root uid, run the
container as root with everything else dropped:

```yaml
# deployment.yaml — pod securityContext:
securityContext:
runAsUser: 0
runAsGroup: 0
fsGroup: 0
# ...and the container securityContext:
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
add: ["NET_RAW", "NET_BIND_SERVICE"]
drop: ["ALL"]
```

- **Layer-2 scans (ARP / MAC capture)** require the pod to share a broadcast
domain with the targets — a normal pod on a routed CNI cannot see client MACs
and will return empty results. To capture MACs on the node's own subnet, also
set `hostNetwork: true` and add `NET_ADMIN` (the equivalent of
[`docker-compose.host-discovery.yml`](../../docker-compose.host-discovery.yml)'s
`network_mode: host`). For devices on other VLANs, prefer SNMP against your
switches instead.
98 changes: 98 additions & 0 deletions deploy/kubernetes/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: rackpad
namespace: rackpad
labels:
app.kubernetes.io/name: rackpad
spec:
replicas: 1
# SQLite is single-writer; never scale this beyond one replica.
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: rackpad
template:
metadata:
labels:
app.kubernetes.io/name: rackpad
spec:
# Image runs as the non-root "rackpad" user (uid/gid 10001).
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: rackpad
# Pin to a released tag for production: https://github.com/Kobii-git/rackpad/releases
image: ghcr.io/kobii-git/rackpad:latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 3000
env:
- name: NODE_ENV
value: production
- name: HOST
value: 0.0.0.0
- name: PORT
value: "3000"
- name: DATABASE_PATH
value: /data/rackpad.db
# Periodically re-checks existing device monitors (up/down/latency).
# Set to 0 to disable. This is NOT subnet discovery.
- name: MONITOR_INTERVAL_MS
value: "300000"
# Set this to your external URL when behind an ingress/reverse proxy,
# e.g. https://rackpad.example.com — needed for OIDC and CSRF origins.
# - name: APP_URL
# value: ""
# - name: TRUST_PROXY
# value: "1"
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: data
mountPath: /data
- name: tmp
mountPath: /tmp
livenessProbe:
httpGet:
path: /api/health
port: http
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /api/health
port: http
periodSeconds: 10
timeoutSeconds: 5
startupProbe:
httpGet:
path: /api/health
port: http
periodSeconds: 5
failureThreshold: 30
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 512Mi
volumes:
- name: data
persistentVolumeClaim:
claimName: rackpad-data
# rootfs is read-only; the app needs a writable /tmp.
- name: tmp
emptyDir: {}
26 changes: 26 additions & 0 deletions deploy/kubernetes/ingress.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Optional. Exposes rackpad over HTTP(S). Adjust the host, ingressClassName, and
# TLS to match your cluster. If you use cert-manager, add the issuer annotation.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rackpad
namespace: rackpad
# annotations:
# cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
# ingressClassName: nginx
rules:
- host: rackpad.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: rackpad
port:
number: 3000
# tls:
# - hosts:
# - rackpad.example.com
# secretName: rackpad-tls
9 changes: 9 additions & 0 deletions deploy/kubernetes/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- pvc.yaml
- deployment.yaml
- service.yaml
# Optional — review the host/class/TLS first, then uncomment:
# - ingress.yaml
4 changes: 4 additions & 0 deletions deploy/kubernetes/namespace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: rackpad
17 changes: 17 additions & 0 deletions deploy/kubernetes/pvc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: rackpad-data
namespace: rackpad
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
# Holds the SQLite database (DATABASE_PATH=/data/rackpad.db). A few GiB
# is plenty for a homelab inventory.
storage: 2Gi
# Uses the cluster default StorageClass. Uncomment and set this if you want a
# specific one. SQLite relies on file locking, so prefer block storage
# (e.g. iSCSI / local-path / Ceph RBD) over NFS, where lock semantics vary.
# storageClassName: ""
15 changes: 15 additions & 0 deletions deploy/kubernetes/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: rackpad
namespace: rackpad
labels:
app.kubernetes.io/name: rackpad
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: rackpad
ports:
- name: http
port: 3000
targetPort: http