Laravel on k3s, Part 4: Supporting Services Without a Fragile Cluster

7 min readUpdated on

Laravel on k3s — Part 1 · Part 2 · Part 3 · Part 4

Supporting services are tempting because every one of them promises a missing piece of production maturity. Error tracking gives stack traces, object storage holds uploads, uptime checks provide an outside view, and a dashboard makes Kubernetes less opaque. But each service also owns a database or volume, credentials, an upgrade path, alerts, and an eventual restore. The goal is not to fit every useful tool on one VPS. It is to add the smallest service that answers a named operating question and has an owner.

Choose the boundary before the chart or container

Write the decision in one sentence. “We need an external check of the public checkout every five minutes” justifies an uptime monitor. “We might need monitoring one day” does not. For a small Laravel installation, managed error tracking, managed object storage, and provider-level uptime monitoring are usually safer alternatives than self-hosting three stateful systems. They move patching, storage replication, and some recovery responsibility to a provider.

Self-host only when data residency, cost, integration, or an existing operational capability outweighs that cost. Do not add a service merely because it has a Helm chart. A one-node cluster does not become resilient when it runs more Pods; it becomes a larger single failure domain.

Separate an optional tool from the application namespace. This makes ownership, RBAC and resource accounting visible, and it prevents a broad service selector from accidentally routing traffic to an administrative Pod.

yaml
apiVersion: v1
kind: Namespace
metadata:
  name: observability
  labels:
    owner: platform
---
apiVersion: v1
kind: ResourceQuota
metadata:
  name: observability-budget
  namespace: observability
spec:
  hard:
    requests.cpu: "500m"
    requests.memory: 1Gi
    limits.cpu: "1500m"
    limits.memory: 3Gi
    persistentvolumeclaims: "2"

A quota is a guardrail, not capacity planning. Measure the node's available CPU, memory and disk first. If an error tracker competes with MySQL or a queue worker for memory, the customer workload wins; moving observability to a managed product is often more useful than tuning eviction behaviour.

Put administration behind a private path

An internal tool should not become an unauthenticated public website just because an Ingress makes it reachable. Prefer a VPN, a private network, identity-aware proxy, or at minimum an allowlisted administrative network plus application authentication and TLS. IP allowlists alone are weak for roaming teams and do not replace account lifecycle management.

This Ingress example gives the monitor a distinct hostname. It deliberately does not claim to provide authentication: configure that at the selected proxy or service layer and test that anonymous requests are rejected.

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: uptime-admin
  namespace: observability
spec:
  ingressClassName: traefik
  rules:
    - host: status-admin.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: uptime-kuma
                port:
                  number: 3001

Never store a dashboard password, S3 key, or error-tracker DSN in a manifest committed to Git. Reference a protected Secret source, grant the service account only the permissions it needs, and rotate credentials with the same controlled restart procedure used for Laravel. Kubernetes base64 is encoding, not a vault.

A small, bounded uptime monitor

An uptime monitor's useful job is to test the public path from outside the application process. It should check a stable health endpoint and a critical customer journey separately. A /up request proves routing and process availability; it does not prove a checkout can write an order.

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: uptime-kuma
  namespace: observability
spec:
  replicas: 1
  selector:
    matchLabels: { app: uptime-kuma }
  template:
    metadata:
      labels: { app: uptime-kuma }
    spec:
      containers:
        - name: uptime-kuma
          image: louislam/uptime-kuma:1.23.16
          ports: [{ name: http, containerPort: 3001 }]
          resources:
            requests: { cpu: 50m, memory: 128Mi }
            limits: { cpu: 250m, memory: 512Mi }
          volumeMounts: [{ name: data, mountPath: /app/data }]
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: uptime-kuma-data
---
apiVersion: v1
kind: Service
metadata: { name: uptime-kuma, namespace: observability }
spec:
  selector: { app: uptime-kuma }
  ports: [{ name: http, port: 3001, targetPort: http }]

Pin a reviewed image version or digest; do not copy an unreviewed latest example. Create the PVC only after selecting a storage class and understanding whether its volume survives node loss. The monitor can alert on the application, but on a single VPS it cannot reliably alert when that same VPS, its network, or its disk is gone. Use an independent external monitor for that failure mode.

Object storage is an application contract

Laravel uploads should use a named disk and a private-by-default bucket policy. Whether the endpoint is MinIO, an S3-compatible provider, or a cloud service, the application needs one stable endpoint, credentials scoped to one bucket, and a recovery story for both objects and database records. Do not expose a MinIO console or API with broad public credentials simply to make local testing convenient.

php
// config/filesystems.php
'disks' => [
    'uploads' => [
        'driver' => 's3',
        'key' => env('UPLOADS_ACCESS_KEY_ID'),
        'secret' => env('UPLOADS_SECRET_ACCESS_KEY'),
        'region' => env('UPLOADS_REGION', 'us-east-1'),
        'bucket' => env('UPLOADS_BUCKET'),
        'endpoint' => env('UPLOADS_ENDPOINT'),
        'use_path_style_endpoint' => env('UPLOADS_PATH_STYLE', false),
        'visibility' => 'private',
    ],
],

Use signed temporary URLs or an authenticated download controller, rather than turning a private document bucket public. Back up objects to an independent location and test a restore with the matching database references. A bucket backup without metadata may leave the application unable to find objects; a database backup without objects leaves broken links. Lifecycle deletion is not a backup policy.

Error tracking needs data minimisation

GlitchTip or another Sentry-compatible service is valuable when it groups exceptions and links them to releases, but error events can contain PII, authorization headers, query parameters, or request bodies. Configure server and client SDK filtering before sending production traffic, set a retention window, and restrict who can browse events. Sample high-volume errors; do not turn an outage into a storage exhaustion event.

At the Laravel boundary, keep the DSN outside Git and send a release identifier that matches the deployed image. Verify with a deliberately harmless staging exception, then remove it. Do not test production monitoring by leaking a real customer's input into an exception.

sh
kubectl -n storefront set env deployment/web RELEASE_SHA=3f18d7c
kubectl -n storefront rollout status deployment/web --timeout=180s
kubectl -n storefront logs deployment/web --tail=100

Recovery, removal, and when not to add a service

Before calling any support service live, rehearse its restoration: recover its PVC or backup to an isolated namespace, log in with a newly issued admin account, and confirm it can read the restored data. Record who receives its alerts and who patches its image. An unowned alert is not observability; it is future noise.

Removal should be equally explicit. Export configuration and data, remove the Ingress first, verify no application secret or DNS record references the service, then delete its workload and volume according to the retention policy. Never delete a PVC merely to make kubectl get look tidy without confirming that a tested backup exists.

Do not run MinIO in this cluster when a managed object store offers the needed durability. Do not self-host error tracking when the team cannot patch and back up its database. Do not deploy a dashboard when kubectl plus a runbook is enough and nobody will own user accounts. The durable architecture remains boring: application Pods have probes and resource bounds, stateful systems have tested recovery, administrative endpoints are private, and every added service has a reason to exist.

GlitchTip, MinIO, Uptime Kuma and cluster dashboards can be useful, but each adds storage, upgrades, credentials and alert ownership. Add a service only when it answers a concrete operating need. Put it in its own namespace or clear boundary, set CPU/memory requests and limits, back up its data, and avoid exposing administrative UIs publicly.

The durable architecture is boring: application workloads have probes, stateful services have tested recovery, logs and alerts point to an owner, and every deployment can be rolled back to a known image.

Related articles

Existing system support

Need help with a live application?

I help companies improve live systems, clean up delivery workflows, and ship new features without adding avoidable complexity.

Comments (0)
Sign in to leave a comment

You need to be signed in to add a comment.

Login

Need someone to take responsibility for the next step?

Let’s talk about your project and define a scope that actually makes sense for your goals.