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.
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.
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.
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.
// 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.
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.