Appearance
Running in Kubernetes
Coequal runs well in Kubernetes. The distroless Docker image is small, starts fast, and has no shell to exploit.
The image lives in a private Amazon ECR repository — see Docker › Authenticating to the registry for how to obtain pull credentials.
Image pull secret
Create an imagePullSecrets entry from the AWS access key + secret you received. ECR auth tokens expire every 12 hours, so for clusters outside AWS use a tool that refreshes the secret on a schedule (for example, ecr-token-refresh or a CronJob that runs aws ecr get-login-password and re-creates the secret).
bash
PASSWORD=$(AWS_ACCESS_KEY_ID=AKIA... AWS_SECRET_ACCESS_KEY=... \
aws ecr get-login-password --region us-east-2)
kubectl create secret docker-registry coequal-ecr \
--namespace coequal \
--docker-server=845238243589.dkr.ecr.us-east-2.amazonaws.com \
--docker-username=AWS \
--docker-password="$PASSWORD"On EKS, prefer IRSA with the coequal-ecr-customer-pull policy attached to the service account — no static credentials, no rotation cron.
Secret
Store sensitive configuration in a Kubernetes Secret:
yaml
apiVersion: v1
kind: Secret
metadata:
name: coequal
namespace: coequal
type: Opaque
stringData:
DATABASE_URL: "postgres://user:pass@postgres:5432/coequal?sslmode=require"
KINDE_DOMAIN: "https://your-tenant.kinde.com"
KINDE_CLIENT_ID: "your-client-id"
KINDE_CLIENT_SECRET: "your-client-secret"
KINDE_REDIRECT_URI: "https://app.example.com"
KINDE_LOGOUT_URI: "https://app.example.com"
ADMIN_API_TOKEN: "your-admin-token-at-least-50-characters-long-for-security"
SMTP_PASSWORD: "your-smtp-password"ConfigMap
Non-sensitive configuration goes in a ConfigMap:
yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: coequal
namespace: coequal
data:
PORT: "8080"
LOG_LEVEL: "info"
PROMETHEUS_ENABLED: "true"
MAILER_TRANSPORT: "smtp"
MAILER_FROM_NAME: "Coequal"
MAILER_FROM_EMAIL: "noreply@example.com"
SMTP_HOST: "smtp.example.com"
SMTP_PORT: "587"
SMTP_USERNAME: "noreply@example.com"Deployment
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: coequal
namespace: coequal
spec:
replicas: 2
selector:
matchLabels:
app: coequal
template:
metadata:
labels:
app: coequal
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
imagePullSecrets:
- name: coequal-ecr
containers:
- name: coequal
image: 845238243589.dkr.ecr.us-east-2.amazonaws.com/coequal:latest
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: coequal
- secretRef:
name: coequal
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
livenessProbe:
httpGet:
path: /metrics
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /metrics
port: 8080
initialDelaySeconds: 5
periodSeconds: 5TIP
The /metrics endpoint is used for probes here since Coequal does not currently expose a dedicated health endpoint. It returns 200 when the server is up and accepting connections.
Service
yaml
apiVersion: v1
kind: Service
metadata:
name: coequal
namespace: coequal
spec:
selector:
app: coequal
ports:
- port: 80
targetPort: 8080Ingress
Example using an nginx ingress controller with TLS:
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: coequal
namespace: coequal
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: coequal-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: coequal
port:
number: 80Scaling considerations
- Replicas: Coequal is stateless and can be scaled horizontally. The grading and calibration workers poll the database, so multiple replicas can share a backlog safely.
- Database: All replicas share the same PostgreSQL database. Tune
DB_MAX_OPEN_CONNSper replica so the total across all replicas does not exceed your database'smax_connections.