Kubernetes
CrashCart as a Deployment behind an Ingress, with Postgres either managed or as a small in-cluster StatefulSet. The manifests below were applied as written on a fresh cluster.
Storage: depends on the Postgres you point it at
The in-cluster postgres.yaml below runs TimescaleDB: compressed storage, free retention. A managed Postgres (Neon, RDS, Cloud SQL, …) runs in plain mode — simpler to operate, but no compression, comfortable to a few million events a month. See three ways your data is stored.
You need
- A cluster with an ingress controller (the manifests assume
ingress-nginx; adjustingressClassNameand the body-size annotation for others) - A domain, e.g.
crashcart.example.com, pointing at the ingress - Optionally cert-manager for TLS — or terminate TLS at your load balancer
1. Save the manifests
crashcart.yaml — namespace, secret, config, deployment, service, ingress:
apiVersion: v1
kind: Namespace
metadata:
name: crashcart
---
apiVersion: v1
kind: Secret
metadata:
name: crashcart
namespace: crashcart
type: Opaque
stringData:
# Replace every value. Generate keys with: openssl rand -hex 32
POSTGRES_PASSWORD: change-me
DATABASE_URL: postgres://crashcart:change-me@postgres:5432/crashcart?sslmode=disable
API_KEYS: change-me
VIEWER_PASSWORD: change-me
---
apiVersion: v1
kind: ConfigMap
metadata:
name: crashcart
namespace: crashcart
data:
PUBLIC_URL: https://crashcart.example.com
RETENTION_DAYS: "30"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: crashcart
namespace: crashcart
spec:
replicas: 1
selector:
matchLabels:
app: crashcart
template:
metadata:
labels:
app: crashcart
spec:
containers:
- name: crashcart
image: ghcr.io/crashcartapp/crashcart:latest
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: crashcart
- secretRef:
name: crashcart
readinessProbe:
httpGet:
path: /health
port: 8080
periodSeconds: 5
livenessProbe:
httpGet:
path: /health
port: 8080
periodSeconds: 30
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
name: crashcart
namespace: crashcart
spec:
selector:
app: crashcart
ports:
- port: 80
targetPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: crashcart
namespace: crashcart
annotations:
# Crash reports can be large.
nginx.ingress.kubernetes.io/proxy-body-size: 25m
# Uncomment with cert-manager installed:
# cert-manager.io/cluster-issuer: letsencrypt
spec:
ingressClassName: nginx
rules:
- host: crashcart.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: crashcart
port:
number: 80
# tls:
# - hosts: [crashcart.example.com]
# secretName: crashcart-tlsIf you're using a managed Postgres (Neon, RDS, Cloud SQL, …), set DATABASE_URL in the Secret to its connection string and skip the next file.
postgres.yaml — an in-cluster TimescaleDB for small installs:
# In-cluster TimescaleDB. Give it a StorageClass you trust; or use a
# managed Postgres (plain mode, no compression) and skip this file.
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: crashcart
spec:
selector:
app: postgres
ports:
- port: 5432
clusterIP: None
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: crashcart
spec:
serviceName: postgres
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: timescale/timescaledb:latest-pg16
env:
- name: POSTGRES_USER
value: crashcart
- name: POSTGRES_DB
value: crashcart
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: crashcart
key: POSTGRES_PASSWORD
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
ports:
- containerPort: 5432
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
readinessProbe:
exec:
command: ["pg_isready", "-U", "crashcart", "-d", "crashcart"]
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 20Gi2. Edit the values
In crashcart.yaml:
- Secret: replace every
change-me. KeepPOSTGRES_PASSWORDand the password insideDATABASE_URLidentical. Generate keys withopenssl rand -hex 32. - ConfigMap:
PUBLIC_URLis the HTTPS address your apps will use. - Ingress: your host name; uncomment the
tlsblock and the cert-manager annotation if you use it.
3. Apply
kubectl apply -f crashcart.yaml -f postgres.yaml
kubectl -n crashcart rollout status deployment/crashcartCrashCart may restart once or twice while Postgres is still starting — that's Kubernetes doing its job. Once rollout status reports success:
kubectl -n crashcart get pods
# crashcart-… 1/1 Running
# postgres-0 1/1 Running4. Check
curl https://crashcart.example.com/health
# {"status":"ok"}Before DNS or TLS are in place, test through a port-forward:
kubectl -n crashcart port-forward svc/crashcart 8080:80
curl http://localhost:8080/health5. Create a project
kubectl -n crashcart exec deploy/crashcart -- /crashcart project shop-ios "Shop app (iOS)" ios
# project shop-ios (id 1)
# DSN: https://<key>@crashcart.example.com/1Open https://crashcart.example.com (any username, your VIEWER_PASSWORD), paste the DSN into the SDK — Connect an SDK — and go through Before going live once.
Upgrading
Pin the image to a release and change it to upgrade:
kubectl -n crashcart set image deployment/crashcart crashcart=ghcr.io/crashcartapp/crashcart:0.2.0
kubectl -n crashcart rollout status deployment/crashcartMigrations run when the new pod starts. With :latest, kubectl -n crashcart rollout restart deployment/crashcart pulls the newest release.
Scaling
CrashCart keeps no local state; raise replicas freely. All pods share the one database, including background work.
Backups
kubectl -n crashcart exec deploy/crashcart -- /crashcart export > backup-$(date +%F).ndjsonRestore with kubectl -n crashcart exec -i deploy/crashcart -- /crashcart import < backup.ndjson. For the in-cluster Postgres, also snapshot the data-postgres-0 PersistentVolumeClaim. See Operations.
iOS crashes
Run the container/symbolicate image from the repository as its own Deployment + Service in the namespace and set SYMBOLICATE_URL: http://symbolicate:8080 in the ConfigMap. Android and JavaScript need nothing extra.
If something doesn't work
| Symptom | Check |
|---|---|
crashcart pod in CrashLoopBackOff for more than a minute | kubectl -n crashcart logs deploy/crashcart — usually DATABASE_URL is wrong or Postgres isn't reachable |
/health returns 503 | Postgres is down: kubectl -n crashcart logs postgres-0 |
| The DSN shows the wrong host | Fix PUBLIC_URL in the ConfigMap and rollout restart |
Large crash reports fail with 413 | Raise the ingress body-size annotation |
postgres-0 stuck Pending | No default StorageClass; set storageClassName in the volume claim template |