Skip to content

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: 5

TIP

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: 8080

Ingress

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: 80

Scaling 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_CONNS per replica so the total across all replicas does not exceed your database's max_connections.