diff --git a/kubernetes/README.md b/kubernetes/README.md new file mode 100644 index 0000000..6b7487c --- /dev/null +++ b/kubernetes/README.md @@ -0,0 +1,80 @@ +# Rallly Kubernetes Manifests + +This directory contains base Kubernetes manifests to self-host Rallly. It separates configuration (ConfigMaps) from sensitive data (Secrets) and uses a StatefulSet for the PostgreSQL database. + +## Prerequisites + +- A Kubernetes cluster. +- `kubectl` configured to talk to your cluster. +- An Ingress Controller (e.g., NGINX) installed. + +## Configuration + +1. **Secrets (`secrets.yaml`):** + - **Important:** Do not commit the `secrets.yaml` file with real credentials to version control. Consider adding `secrets.yaml` to your `.gitignore` file to prevent accidental commits. + - Update `POSTGRES_PASSWORD` and `SECRET_PASSWORD` (use `openssl rand -hex 32` to generate). + - **Critical:** Ensure the password in `DATABASE_URL` matches `POSTGRES_PASSWORD`. Both must use the same value. + - **Format:** The `DATABASE_URL` format should look like this: `postgres://:@:5432/`. + +2. **Config (`rallly-config.yaml`):** + - Update `NEXT_PUBLIC_BASE_URL` to match your domain. + - Configure your SMTP settings for emails. + +3. **Ingress (`ingress.yaml`):** + - Change `host: rallly.example.com` to your actual domain. + - Ensure `ingressClassName` matches your cluster's controller (default is set to `nginx`). + - **TLS:** Create the TLS certificate Secret named `rallly-tls` or enable cert-manager (see comments in `ingress.yaml` for options). + +## Deployment Order + +Apply the manifests in the following order to ensure dependencies are met: + +```bash +# 1. Apply Secrets and Config first +kubectl apply -f secrets.yaml +kubectl apply -f rallly-config.yaml + +# 2. Apply Database (StatefulSet) +kubectl apply -f postgres.yaml + +# Wait for database to be ready +kubectl wait --for=condition=ready pod -l app=postgres --timeout=300s + +# 3. Apply Application (Deployment) +kubectl apply -f rallly.yaml + +# 4. Apply Ingress +kubectl apply -f ingress.yaml +``` + +**Note:** If you update `secrets.yaml` or `rallly-config.yaml` _after_ deployment, you must restart the Rallly pods for changes to take effect: + +```bash +kubectl rollout restart deployment rallly +``` + +This performs a **rolling restart**, so there will be no downtime. However, ensure the new configuration is valid; if pods fail to start, check the logs with `kubectl logs -f deployment/rallly`. + +## Verification + +Check that the pods are running: + +```bash +kubectl get pods +``` + +The Postgres pod should show `1/1 Running` and the Rallly pod should eventually show `1/1 Running` once the liveness probe passes. + +## Notes on Storage + +The PostgreSQL StatefulSet requests a 1Gi PersistentVolume. Ensure your cluster has a default StorageClass configured, or update the `volumeClaimTemplates` in `postgres.yaml` to specify a StorageClass. If no StorageClass is available, the PersistentVolumeClaim will remain pending and the postgres pod will not start. Check your cluster's available StorageClasses with `kubectl get storageclass`. + +## Notes on Backups + +For production deployments, implement regular PostgreSQL backups. Consider using: + +- Kubernetes-native backup tools (e.g., Velero) +- Scheduled pg_dump jobs within the cluster +- Cloud-provider managed backups (if using managed K8s) + +Refer to your cluster provider's backup documentation for recommendations. diff --git a/kubernetes/ingress.yaml b/kubernetes/ingress.yaml new file mode 100644 index 0000000..57202d7 --- /dev/null +++ b/kubernetes/ingress.yaml @@ -0,0 +1,34 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: rallly + namespace: default + annotations: + # Example for cert-manager (uncomment if using) + # cert-manager.io/cluster-issuer: letsencrypt-prod + + # Example for NGINX ingress controller size limit + # nginx.ingress.kubernetes.io/proxy-body-size: "10m" +spec: + # NOTE: Explicitly set to 'nginx'. Remove this line if using a different Ingress Controller + # or if you wish to use the cluster default. + ingressClassName: nginx + rules: + - host: rallly.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: rallly + port: + number: 80 + tls: + - hosts: + - rallly.example.com + secretName: rallly-tls + # Note: This Secret must be created separately. Options: + # 1. Use cert-manager (uncomment annotation above) to auto-provision + # 2. Manually create: kubectl create secret tls rallly-tls --cert=path/to/cert --key=path/to/key + # 3. Use an existing cluster-issued certificate secret diff --git a/kubernetes/postgres.yaml b/kubernetes/postgres.yaml new file mode 100644 index 0000000..f6bc0e7 --- /dev/null +++ b/kubernetes/postgres.yaml @@ -0,0 +1,97 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: default +spec: + ports: + - port: 5432 + selector: + app: postgres +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: postgres + namespace: default +spec: + selector: + matchLabels: + app: postgres + serviceName: "postgres" + replicas: 1 + template: + metadata: + labels: + app: postgres + spec: + securityContext: + # Run as standard Postgres user (UID 999) + fsGroup: 999 + runAsNonRoot: true + runAsUser: 999 + containers: + - name: postgres + # Switched to 14-alpine to align with official docker-compose + image: postgres:14-alpine + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: rallly-secrets + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: rallly-secrets + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + value: rallly + # Fix: Point PGDATA to a generic subpath to avoid mount errors (lost+found) + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + ports: + - containerPort: 5432 + name: postgres + # Health Probes + livenessProbe: + exec: + command: + - /bin/sh + - -c + # Uses env var and adds timeout to prevent hanging + - pg_isready -U $POSTGRES_USER -t 5 + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + exec: + command: + - /bin/sh + - -c + # Uses env var and adds timeout to prevent hanging + - pg_isready -U $POSTGRES_USER -t 5 + initialDelaySeconds: 10 + periodSeconds: 5 + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + resources: + limits: + cpu: "2" + memory: 2Gi + requests: + cpu: 500m + memory: 1Gi + volumeClaimTemplates: + - metadata: + name: postgres-data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi diff --git a/kubernetes/rallly-config.yaml b/kubernetes/rallly-config.yaml new file mode 100644 index 0000000..878f9d7 --- /dev/null +++ b/kubernetes/rallly-config.yaml @@ -0,0 +1,24 @@ +# kubernetes/rallly-config.yaml +# Stores all non-secret configuration variables. +apiVersion: v1 +kind: ConfigMap +metadata: + name: rallly-config + namespace: default +data: + # Base URL for the application (must match Ingress host) + NEXT_PUBLIC_BASE_URL: "https://rallly.example.com" + + # Email Settings + # Note: This is distinct from INITIAL_ADMIN_EMAIL (defined in secrets), which creates the admin account. + # SUPPORT_EMAIL is the user-facing support contact shown to users. + SUPPORT_EMAIL: "admin@example.com" + + EMAIL_LOGIN_ENABLED: "true" + # SECURITY: This allows ANY email to register. Restrict to "*@example.com" or specific emails for production. + ALLOWED_EMAILS: "*" + + # SMTP Settings (Credentials will be in the Secret file) + SMTP_HOST: "smtp.example.com" + SMTP_PORT: "587" + SMTP_SECURE: "false" diff --git a/kubernetes/rallly.yaml b/kubernetes/rallly.yaml new file mode 100644 index 0000000..abb4938 --- /dev/null +++ b/kubernetes/rallly.yaml @@ -0,0 +1,129 @@ +apiVersion: v1 +kind: Service +metadata: + name: rallly + namespace: default +spec: + selector: + app: rallly + ports: + - protocol: TCP + port: 80 + targetPort: 3000 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rallly + namespace: default + labels: + app: rallly +spec: + # Note: For production, use replicas: 2 or more with a PodDisruptionBudget for HA. + replicas: 1 + selector: + matchLabels: + app: rallly + strategy: + type: RollingUpdate + # Zero-downtime deployment strategy + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + template: + metadata: + labels: + app: rallly + spec: + securityContext: + fsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + containers: + - name: rallly + # Pinned version for stability and reproducibility + image: lukevella/rallly:v4.5.4 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 3000 + name: http + protocol: TCP + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + env: + # 1. Configuration (from ConfigMap) + - name: NEXT_PUBLIC_BASE_URL + valueFrom: + configMapKeyRef: + name: rallly-config + key: NEXT_PUBLIC_BASE_URL + - name: SUPPORT_EMAIL + valueFrom: + configMapKeyRef: + name: rallly-config + key: SUPPORT_EMAIL + - name: ALLOWED_EMAILS + valueFrom: + configMapKeyRef: + name: rallly-config + key: ALLOWED_EMAILS + - name: EMAIL_LOGIN_ENABLED + valueFrom: + configMapKeyRef: + name: rallly-config + key: EMAIL_LOGIN_ENABLED + - name: SMTP_HOST + valueFrom: + configMapKeyRef: + name: rallly-config + key: SMTP_HOST + - name: SMTP_PORT + valueFrom: + configMapKeyRef: + name: rallly-config + key: SMTP_PORT + - name: SMTP_SECURE + valueFrom: + configMapKeyRef: + name: rallly-config + key: SMTP_SECURE + # 2. Secrets (from Secret) + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: rallly-secrets + key: DATABASE_URL + - name: SECRET_PASSWORD + valueFrom: + secretKeyRef: + name: rallly-secrets + key: SECRET_PASSWORD + - name: INITIAL_ADMIN_EMAIL + valueFrom: + secretKeyRef: + name: rallly-secrets + key: INITIAL_ADMIN_EMAIL + resources: + limits: + cpu: "1" + memory: 1Gi + requests: + cpu: 200m + memory: 512Mi + + livenessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + port: 3000 + # Reduced delay so the pod becomes ready faster once running + initialDelaySeconds: 10 + periodSeconds: 5 diff --git a/kubernetes/secrets.yaml b/kubernetes/secrets.yaml new file mode 100644 index 0000000..193729f --- /dev/null +++ b/kubernetes/secrets.yaml @@ -0,0 +1,24 @@ +# kubernetes/secrets.yaml +# WARNING: This file uses 'stringData' for demonstration. +# For production, DO NOT commit this file to Git. +# Use SealedSecrets, ExternalSecrets, or manually create the secret on the cluster. +apiVersion: v1 +kind: Secret +metadata: + name: rallly-secrets + namespace: default +type: Opaque +stringData: + # Database Connection String (postgres://user:password@service:port/db_name) + # IMPORTANT: The username/password here MUST match POSTGRES_USER/POSTGRES_PASSWORD below. + DATABASE_URL: "postgres://rallly:CHANGE_ME_PASSWORD@postgres:5432/rallly" + + # Random string for session encryption (generate with 'openssl rand -hex 32') + SECRET_PASSWORD: "CHANGE_ME_TO_A_LONG_RANDOM_STRING" + + # The email of the first admin user + INITIAL_ADMIN_EMAIL: "admin@example.com" + + # Database Credentials (used by the postgres StatefulSet) + POSTGRES_USER: "rallly" + POSTGRES_PASSWORD: "CHANGE_ME_PASSWORD"