Skip to content

Kubernetes

HeliosLogs runs well on Kubernetes as a single-node StatefulSet. A single node owns its data and control plane on a persistent volume, so this is a replicas: 1 workload — see Scaling & HA before you try to scale it.

The manifest below is self-contained: a namespace, an admin-bootstrap Secret, a Service, and a StatefulSet with two PVCs (data + secrets). It works on any conformant cluster using the default StorageClass.

The manifest

Save as helioslogs.yaml and kubectl apply -f helioslogs.yaml.

yaml
apiVersion: v1
kind: Namespace
metadata:
  name: helioslogs
---
apiVersion: v1
kind: Secret
metadata:
  name: helioslogs-admin
  namespace: helioslogs
type: Opaque
---
apiVersion: v1
kind: Service
metadata:
  name: helioslogs
  namespace: helioslogs
  labels:
    app: helioslogs
spec:
  selector:
    app: helioslogs
  ports:
    - name: http
      port: 7300
      targetPort: http
    - name: syslog-udp
      port: 5514
      targetPort: syslog-udp
      protocol: UDP
    - name: syslog-tcp
      port: 5514
      targetPort: syslog-tcp
      protocol: TCP
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: helioslogs
  namespace: helioslogs
  labels:
    app: helioslogs
spec:
  serviceName: helioslogs
  replicas: 1                       # single-node control plane — see Scaling & HA
  selector:
    matchLabels:
      app: helioslogs
  template:
    metadata:
      labels:
        app: helioslogs
    spec:
      securityContext:
        # The image runs as the nonroot user 65532. fsGroup makes the mounted PVCs
        # group-owned and group-writable so that user can write to them.
        runAsNonRoot: true
        runAsUser: 65532
        runAsGroup: 65532
        fsGroup: 65532
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: helioslogs
          image: helioslogs/helioslogs:0.2.2     # pin a version (':latest' also exists)
          args: ["serve", "--host", "0.0.0.0", "--port", "7300", "--data-dir", "/app/data"]
          ports:
            - name: http
              containerPort: 7300
            - name: syslog-udp
              containerPort: 5514
              protocol: UDP
            - name: syslog-tcp
              containerPort: 5514
              protocol: TCP
          envFrom:
            - secretRef:
                name: helioslogs-admin
          volumeMounts:
            - name: data
              mountPath: /app/data
            - name: secret
              mountPath: /app/secret
          readinessProbe:
            httpGet:
              path: /api/health
              port: http
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /api/health
              port: http
            initialDelaySeconds: 15
            periodSeconds: 20
          resources:
            requests:
              cpu: 250m
              memory: 512Mi
            limits:
              cpu: "2"
              memory: 2Gi
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop: ["ALL"]
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 20Gi          # size for your retention — tune this
        # storageClassName: fast   # omit to use the cluster default
    - metadata:
        name: secret
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 64Mi          # just the encryption + JWT keys

Apply and access

bash
kubectl apply -f helioslogs.yaml
kubectl -n helioslogs rollout status statefulset/helioslogs

# Reach the UI without an Ingress:
kubectl -n helioslogs port-forward svc/helioslogs 7300:7300

Open http://localhost:7300 and log in with the credentials from the Secret (or complete the setup wizard if you removed it). Next: First steps.

Change the admin password

The Secret above ships a placeholder password. Set a real one before exposing the instance, and consider managing the Secret with a sealed-secrets / external-secrets operator rather than committing it to git.

Expose with an Ingress

Point an Ingress at the helioslogs Service on port 7300. Adjust ingressClassName, the host, and TLS to match your cluster:

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: helioslogs
  namespace: helioslogs
  # annotations:
  #   cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts: ["logs.example.com"]
      secretName: helioslogs-tls
  rules:
    - host: logs.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: helioslogs
                port:
                  number: 7300

Terminate TLS at the Ingress and read Security hardening before exposing HeliosLogs publicly.

Receiving syslog

HeliosLogs also listens for raw syslog (UDP + TCP, RFC 5424 / 3164) on port 5514. The manifest publishes that port on the container and Service, but the listener is off by default — enable it under Admin → Data Ingestion → Syslog (or set HELIOS_SYSLOG_PORT). It has no token auth, so restrict it to trusted senders. See Syslog.

In-cluster senders can target helioslogs.helioslogs.svc:5514. Devices outside the cluster need a LoadBalancer or NodePort Service — an Ingress can't carry raw syslog, only HTTP:

yaml
apiVersion: v1
kind: Service
metadata:
  name: helioslogs-syslog
  namespace: helioslogs
spec:
  type: LoadBalancer
  selector:
    app: helioslogs
  ports:
    - name: syslog-udp
      port: 514                 # standard syslog port → container's 5514
      targetPort: syslog-udp
      protocol: UDP
    - name: syslog-tcp
      port: 514
      targetPort: syslog-tcp
      protocol: TCP

Mixed UDP + TCP on a LoadBalancer

Exposing UDP and TCP on a single LoadBalancer needs Kubernetes ≥ 1.26 (or the MixedProtocolLBService feature gate). If your platform's load balancer can't mix protocols, split them into two Services.

Hardening (optional)

For a read-only root filesystem, mount an emptyDir at /tmp and set the flag — all durable writes already go to the two PVCs:

yaml
          volumeMounts:
            - name: tmp
              mountPath: /tmp
            # ... data and secret mounts ...
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
      volumes:
        - name: tmp
          emptyDir: {}

Scaling & HA

replicas: 1 is deliberate. A single node keeps its control plane and a block cache on its own ReadWriteOnce volume; bumping replicas would give each pod a separate PVC and a separate control plane — not a cluster.

For high availability across nodes, HeliosLogs uses a shared S3-compatible object store (--shared-store) instead of per-pod local state, with the same secret keys mounted on every pod (use a Kubernetes Secret for /app/secret, not a per-pod PVC). See Multi-node & shared store and Secrets & encryption.

Upgrade

Bump the image tag and let the StatefulSet roll the pod (it terminates the old pod before starting the new one, so the ReadWriteOnce volumes hand off cleanly):

bash
kubectl -n helioslogs set image statefulset/helioslogs \
  helioslogs=helioslogs/helioslogs:0.2.3
kubectl -n helioslogs rollout status statefulset/helioslogs

See Upgrades & backups for version policy and backing up the data + secret volumes.