Day 2 Operations Guide
Introduction
So your helmfile apply completed without errors. The pods are running, the ingress is serving traffic, and your university's users are logging in through DFN-AAI via Keycloak. Congratulations — Day 1 is done.
Day 2 operations is where the real work begins. This guide covers the operational lifecycle of openDesk Edu after initial deployment: monitoring, backup verification, certificate renewal, updates, scaling, and the inevitable troubleshooting. It is written for university IT staff who manage Kubernetes clusters and need concrete command examples, not abstract advice.
Every environment is different — your cluster topology, storage class, ingress controller, and monitoring stack will differ from the reference deployment. Treat the commands below as starting points and adapt them to your site.
Monitoring the Platform
Health Endpoints
openDesk Edu exposes a centralized health endpoint at https://opendesk-edu.org/api/health (replace with your actual domain). It returns a JSON summary of component status:
curl -s https://opendesk-edu.org/api/health | jq .
A healthy response contains an "ok": true status and per-service entries. If any service reports unhealthy, the endpoint returns HTTP 503.
Individual services also expose their own probes:
| Service | Health Check Path | Type |
|---|---|---|
| Keycloak | /realms/master/.well-known/uma2-configuration |
HTTP 200 check |
| Nextcloud | /status.php |
HTTP 200 check |
| OpenCloud | /health |
HTTP 200 check |
| ILIAS | /Services/HealthCheck/health_check.php |
HTTP 200 |
| Moodle | /admin/tool/health/index.php |
HTTP 200 |
| Grommunio | /grommunio-api/health |
HTTP 200 check |
You can script a quick smoke test:
for svc in keycloak nextcloud opencloud ilias moodle grommunio; do
status=$(curl -s -o /dev/null -w "%{http_code}" "https://opendesk-edu.org/$svc/health")
echo "$svc: $status"
done
Prometheus / Grafana Integration
The openDesk Edu stack ships optional Prometheus and Grafana configurations. If enabled, you gain:
- Kubernetes node metrics (CPU, memory, disk pressure)
- Pod resource usage per namespace
- Keycloak metrics (active sessions, login rate, token issuance)
- PVC usage as a percentage of capacity
To verify Prometheus is collecting targets:
kubectl -n opendesk-monitoring port-forward svc/prometheus-operated 9090:9090 &
curl -s http://localhost:9090/api/v1/targets | jq '.data.activeTargets | length'
If Grafana is deployed, import the openDesk Edu dashboard from the project's GitHub repository. The dashboard ID is published in the release notes.
Without Prometheus: kubectl-based Monitoring
If you run the platform without Prometheus, these commands give you the essential pulse:
Pod restarts:
kubectl -n opendesk-edu get pods --field-selector=status.phase!=Running
kubectl -n opendesk-edu get pods -o wide | grep -v Running
PVC usage:
kubectl -n opendesk-edu get pvc
for pvc in $(kubectl -n opendesk-edu get pvc -o name); do
echo "--- $pvc ---"
kubectl -n opendesk-edu exec deploy/some-pod -- df -h | grep /data
done
Certificate expiry:
kubectl -n opendesk-edu get certificate -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.notAfter}{"\n"}{end}'
Keycloak session count (requires Keycloak admin CLI):
kubectl -n opendesk-edu exec deploy/keycloak -- \
/opt/keycloak/bin/kcadm.sh get sessions -r master --no-config \
--server http://localhost:8080 --realm master \
--user "$KC_ADMIN" --password "$KC_PASSWORD"
Notifications
For proactive monitoring, configure alerts in Prometheus Alertmanager. The openDesk Edu chart includes alert rules for:
- Pod crashlooping
- PVC usage > 80%
- Certificate expiry < 30 days
- Keycloak down
Alertmanager can route to:
- Matrix webhook: Configure a Matrix bot room and set
alertmanager.config.receivers[].webhook_configsto your Matrix webhook URL. - Email: Use the built-in email receiver with your university's SMTP relay.
If you run without Alertmanager, write a simple cronjob that checks the health endpoint and sends a Matrix message via curl:
#!/bin/bash
HEALTH=$(curl -s -o /dev/null -w "%{http_code}" https://opendesk-edu.org/api/health)
if [ "$HEALTH" != "200" ]; then
curl -X POST -H "Content-Type: application/json" \
-d '{"msgtype":"m.text","body":"openDesk Edu health check FAILED"}' \
"https://matrix.your-uni.de/_matrix/client/v3/rooms/!roomid:server/send/m.room.message?access_token=$TOKEN"
fi
Backup Verification and Restores
How k8up / restic Backups Work
Backups are handled by the k8up operator, which schedules restic backups to an S3-compatible storage backend. The default schedule is defined in the helmfile values:
k8up:
backup:
schedule: "0 2 * * *" # daily at 02:00
prometheus: # backup metrics exposed
backend: s3
s3:
endpoint: s3.your-uni.de
bucket: opendesk-backup
Each service's PersistentVolumeClaim (PVC) is backed up via a Schedule custom resource. The operator creates a Job that mounts the PVC and runs restic backup on it.
Verifying Backup Integrity
Never trust a backup you haven't verified. Add this to your weekly routine:
List snapshots:
kubectl -n opendesk-edu exec deploy/k8up -- \
restic -r s3:s3.your-uni.de/opendesk-backup snapshots \
--password-file /etc/restic/password
Check repository integrity:
kubectl -n opendesk-edu exec deploy/k8up -- \
restic -r s3:s3.your-uni.de/opendesk-backup check \
--password-file /etc/restic/password \
--read-data-subset=5% # checks 5% of data; swap to --read-data quarterly
Verify a specific snapshot:
kubectl -n opendesk-edu exec deploy/k8up -- \
restic -r s3:s3.your-uni.de/opendesk-backup \
verify --password-file /etc/restic/password <snapshot-id>
Step-by-Step Restore Procedures
Before any restore, shut down the affected service to prevent data corruption. Scale the deployment to zero replicas:
kubectl -n opendesk-edu scale deploy/<service> --replicas=0
Keycloak Database (PostgreSQL)
-
Identify the latest snapshot containing Keycloak data:
kubectl -n opendesk-edu exec deploy/k8up -- \ restic -r s3:... snapshots --path /data/keycloak/postgresql \ --password-file /etc/restic/password --latest 1 -
Restore to a temporary PVC:
kubectl -n opendesk-edu apply -f - <<EOF apiVersion: v1 kind: PersistentVolumeClaim metadata: name: keycloak-db-restore spec: storageClassName: your-storage-class accessModes: [ReadWriteOnce] resources: requests: storage: 10Gi EOF -
Launch a restore job:
kubectl -n opendesk-edu apply -f - <<EOF apiVersion: v1 kind: Pod metadata: name: keycloak-db-restore spec: containers: - name: restic image: restic/restic:latest command: ["restic", "restore", "latest", "--target", "/restore", "--path", "/data/keycloak/postgresql"] env: - name: RESTIC_REPOSITORY value: s3:... - name: RESTIC_PASSWORD valueFrom: secretKeyRef: name: k8up-restic-secret key: password volumeMounts: - mountPath: /restore name: restore-target volumes: - name: restore-target persistentVolumeClaim: claimName: keycloak-db-restore restartPolicy: Never EOF -
Stop the existing Keycloak PostgreSQL pod, delete the original PVC, and re-create it from the restored data:
kubectl -n opendesk-edu scale statefulset keycloak-postgresql --replicas=0 kubectl -n opendesk-edu delete pvc data-keycloak-postgresql-0 # Create new PVC with same name and StorageClass # Copy data from restore-pvc using a temporary Pod kubectl -n opendesk-edu scale statefulset keycloak-postgresql --replicas=1 -
Verify Keycloak starts and accepts logins.
Nextcloud / OpenCloud Files
The procedure is similar — restore the Nextcloud PVC from the latest snapshot. After restoration, run the Nextcloud maintenance repair:
kubectl -n opendesk-edu exec deploy/nextcloud -- \
php occ maintenance:repair
For OpenCloud, run the integrity check:
kubectl -n opendesk-edu exec deploy/opencloud -- \
opencloud files:check
ILIAS / Moodle LMS Data
ILIAS stores data in multiple PVCs (files, ilias-data). Restore each in order:
- Database (MySQL/MariaDB) — same PostgreSQL procedure adapted for MySQL
- Filesystem data —
restic restoreto the ILIAS data PVCs
After restore, flush the ILIAS cache:
kubectl -n opendesk-edu exec deploy/ilias -- \
php ilias.php resetPassword
kubectl -n opendesk-edu exec deploy/ilias -- \
php setup/setup.php --update=keep,no-goto
For Moodle:
kubectl -n opendesk-edu exec deploy/moodle -- \
php admin/cli/purge_caches.php
Grommunio Mailboxes
Grommunio stores mail data in its own PVC. Restore the PVC and then run:
kubectl -n opendesk-edu exec deploy/grommunio -- \
grommunio-admin-mailbox list
To restore individual mailboxes, use the Grommunio export/import tools after the PVC restore:
kubectl -n opendesk-edu exec deploy/grommunio -- \
grommunio-admin-mailbox-export -u user@example.com -f /tmp/backup.mbox
kubectl -n opendesk-edu exec deploy/grommunio -- \
grommunio-admin-mailbox-import -u user@example.com -f /tmp/backup.mbox
Testing a Restore in a Staging Environment
Critical: At least quarterly, perform a full restore drill in a staging cluster. The staging environment should mirror production topology (storage class, namespace structure, ingress).
Process:
- Deploy a fresh openDesk Edu stack in the staging namespace with
helmfile -e staging sync. - Scale all services to zero replicas.
- Restore all PVCs from the most recent production snapshots.
- Scale services back up.
- Verify login flows, file access, mail delivery, and LMS course content.
- Document any discrepancies and update this guide.
Certificate Management
How Certificate Renewal Works
openDesk Edu can provision TLS certificates either through openDesk Certificates (Bundesdruckerei, the default for German universities) or Let's Encrypt. Both methods use Cert-manager with the appropriate issuer.
The certificate issuer is configured in the helmfile values:
global:
certificates:
issuer: letsencrypt-prod # or opendesk-ca
Cert-manager automatically renews certificates before expiry. By default, renewal attempts start 30 days before expiry.
Checking Certificate Expiry Dates
# List all certificates and their expiry
kubectl -n opendesk-edu get certificate -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.notAfter}{"\n"}{end}'
# Expiry in days (requires jq)
kubectl -n opendesk-edu get certificate -o json | \
jq -r '.items[] | "\(.metadata.name): \(.status.notAfter)"'
You can also check the actual TLS certificate served by the ingress:
openssl s_client -connect opendesk-edu.org:443 -servername opendesk-edu.org \
</dev/null 2>/dev/null | openssl x509 -noout -dates
Troubleshooting Failed Renewals
Cert-manager certificate status:
kubectl -n opendesk-edu describe certificate <name>
kubectl -n opendesk-edu describe certificaterequest
kubectl -n opendesk-edu get order
kubectl -n opendesk-edu get challenge
Common failure causes:
| Symptom | Likely Cause | Fix |
|---|---|---|
Order failed: DNS01 error |
DNS propagation delay (new domain) | Wait 5–10 minutes, delete the Order resource to retry |
rateLimited |
Let's Encrypt rate limit hit | Check cert-manager logs; wait 1 hour before retrying |
Challenge not ready |
Ingress not yet serving the HTTP-01 challenge | Verify ingress controller is running and NetworkPolicy allows HTTP-01 |
Certificate not issued, issuer not ready |
Let's Encrypt account not registered | Check the Issuer/ClusterIssuer status |
For openDesk Certificates: Contact the Bundesdruckerei support portal for issuer-side issues. Provide the certificate name and the error from kubectl describe certificate.
Manual Certificate Renewal
If auto-renewal fails and you need a certificate urgently:
# Delete the old certificate to force re-issuance
kubectl -n opendesk-edu delete certificate <name>
# Re-apply the Certificate manifest (Cert-manager will create it again)
kubectl -n opendesk-edu apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: opendesk-edu-tls
namespace: opendesk-edu
spec:
secretName: opendesk-edu-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- opendesk-edu.org
- "*.opendesk-edu.org"
EOF
Updating Components
Safe Update Procedure
The canonical update path is:
# 1. Review changes
helmfile diff
# 2. Create a backup (k8up on-demand)
kubectl -n opendesk-edu apply -f - <<EOF
apiVersion: k8up.io/v1
kind: Backup
metadata:
name: pre-update-backup
namespace: opendesk-edu
spec:
failedJobsHistoryLimit: 1
successfulJobsHistoryLimit: 1
EOF
# 3. Apply the update
helmfile apply
# 4. Monitor rollout
kubectl -n opendesk-edu rollout status deploy/<service>
Updating Individual Charts vs. Full Stack
For a single-component update (e.g., a Keycloak patch release):
helmfile -l app=keycloak diff
helmfile -l app=keycloak apply
For a full stack update (e.g., monthly release):
helmfile diff
helmfile apply
Checking Upstream Releases for Breaking Changes
Before updating any component:
- Check the openDesk Edu release notes on GitHub for each new version.
- Review upstream chart upgrade notes — especially for Keycloak (migration scripts required for major versions), PostgreSQL (minor version upgrades may require re-initialization of replication slots), and Grommunio (database schema migrations).
- Test in staging first — deploy the new version to a staging namespace and verify all service interactions.
Key version compatibility constraints:
| Component | Breaking Change Trigger | Mitigation |
|---|---|---|
| Keycloak | Major version bump (e.g., 24 → 25) | Run kcadm.sh update migration, verify SAML/OpenID config |
| PostgreSQL | Major version (15 → 16) | Requires pg_upgrade or dump/restore |
| Grommunio | Database schema change | Check release notes for migration commands |
| Nextcloud | Major version (28 → 29) | Run occ upgrade after deploy |
Rollback Strategy
Helm rollback (single chart):
helm -n opendesk-edu rollback keycloak 1
Helmfile rollback (full stack): Helmfile does not have a built-in rollback command. The recommended approach is:
# Revert to the previous helmfile state with git
git revert HEAD
git push
helmfile apply
Alternatively, track which Helm chart versions you ran previously:
helm -n opendesk-edu list --all
helm -n opendesk-edu rollback <release> <revision>
Common Failure Modes
Keycloak Database Connection Lost
Symptoms: Users cannot log in; Keycloak returns 500 errors; log shows connection refused or pq: role "keycloak" does not exist.
Debugging:
kubectl -n opendesk-edu logs deploy/keycloak --tail=50
kubectl -n opendesk-edu exec deploy/keycloak-postgresql -- pg_isready
kubectl -n opendesk-edu get pvc data-keycloak-postgresql-0
Resolution:
- If PostgreSQL is down: check PVC and pod status.
- If PostgreSQL is up but Keycloak can't connect: verify the
KC_DB_URLenvironment variable in the Keycloak deployment matches the PostgreSQL service name. - If the database is corrupted: restore from the latest k8up backup.
Pod Stuck in CrashLoopBackOff
# Get the crash reason
kubectl -n opendesk-edu logs <pod-name> --previous
# Check events
kubectl -n opendesk-edu get events --field-selector involvedObject.name=<pod-name>
# Common causes:
# - OOM: Increase memory limits or add resources.requests.memory
# - Config error: Invalid ConfigMap/YAML, check with kubectl describe
# - PVC not found: Verify PVC exists and is bound
# - Init container failure: Check init container logs separately
PVC Filling Up
Monitor usage:
kubectl -n opendesk-edu get pvc
kubectl -n opendesk-edu top pod --containers
For a more detailed view, exec into a pod and check:
kubectl -n opendesk-edu exec deploy/nextcloud -- df -h /var/www/html
Resolution:
- Identify which service is growing: Nextcloud files, Grommunio mail, ILIAS uploads, and PostgreSQL WAL are common culprits.
- Clean up: archive old data, enable retention policies (e.g., Nextcloud trashbin auto-expiry).
- Expand PVC: edit the PVC to increase storage (requires StorageClass with
allowVolumeExpansion: true).
Network Policy Blocking Inter-Service Communication
Symptoms: Service A can't reach Service B (e.g., Keycloak can't reach LDAP, or ILIAS can't connect to the database). Connections hang or timeout.
Debugging:
# Check if NetworkPolicies exist in the namespace
kubectl -n opendesk-edu get networkpolicies
# Test connectivity from a pod
kubectl -n opendesk-edu exec deploy/keycloak -- \
curl -s -o /dev/null -w "%{http_code}" http://keycloak-postgresql:5432
# Check policy logs if using Calico or Cilium with logging
Resolution: Review and adjust NetworkPolicy selectors. The openDesk Edu default policies allow intra-namespace traffic, but custom policies may be too restrictive. Temporarily lift the policy to confirm the root cause:
kubectl -n opendesk-edu delete networkpolicy restrictive-policy
Then re-apply a corrected version.
Node Failure / Pod Eviction
When a Kubernetes node fails, pods are rescheduled according to the PodDisruptionBudget (PDB). openDesk Edu defines PDBs for critical services:
kubectl -n opendesk-edu get pdb
What to do:
- Cordon the failing node:
kubectl cordon <node> - Drain it gracefully:
kubectl drain <node> --ignore-daemonsets --delete-emptydir-data - After repair, uncordon:
kubectl uncordon <node> - Verify pods are running on remaining nodes:
kubectl -n opendesk-edu get pods -o wide
OOMKilled Pods
Symptom: Pod restarts with reason OOMKilled.
Debugging:
kubectl -n opendesk-edu describe pod <pod-name> | grep -A5 "Last State"
kubectl -n opendesk-edu top pod <pod-name> --containers
Resolution: Increase the memory limit in the Helm values for the affected service:
keycloak:
resources:
requests:
memory: "2Gi"
limits:
memory: "4Gi"
For Java-based services (Keycloak, Grommunio, Collabora), also adjust JVM heap settings:
keycloak:
extraEnv:
- name: JAVA_OPTS
value: "-Xms1g -Xmx2g"
Scaling Services
When to Scale
Monitor these thresholds to decide when scaling is needed:
| Symptom | Threshold | Action |
|---|---|---|
| CPU throttling | > 80% sustained | Increase replica count or CPU limit |
| Memory pressure | > 85% of limit | Increase memory limit or replica count |
| Request latency | > 2s p99 latency | Scale horizontally |
| PVC usage | > 80% capacity | Expand PVC or add cleanup policy |
| Keycloak sessions | > 5,000 active | Add Keycloak replicas (requires HA configuration) |
Horizontal Pod Autoscaling (HPA)
For stateless services (NGINX, Collabora, Element, Etherpad), enable HPA:
kubectl -n opendesk-edu autoscale deployment nextcloud --cpu-percent=70 --min=1 --max=5
Or add to your helmfile values:
nextcloud:
autoscaling:
enabled: true
minReplicas: 1
maxReplicas: 5
targetCPUUtilizationPercentage: 70
Note: Keycloak can be scaled horizontally only if its session cache is configured for distributed mode (Infinispan/JGroups). This requires setting KEYCLOAK_CACHE_TYPE=distributed and enabling pod discovery. Without this, scaling Keycloak beyond one replica may cause session inconsistency.
Vertical Scaling for Stateful Services
Stateful services (databases, ILIAS, Moodle, Grommunio) typically scale vertically:
# Edit the StatefulSet's resource limits
kubectl -n opendesk-edu edit statefulset ilias-postgresql
# Change resources.requests.memory and resources.limits.memory
After changing limits, the pods restart with the new values.
For ILIAS specifically, PHP-FPM workers are the bottleneck. Increase in values:
ilias:
php:
fpm:
maxChildren: 50
memoryLimit: "512M"
Cluster Node Scaling
For the Kubernetes worker pool itself:
- Cloud/on-prem with cluster autoscaler: Configure min/max nodes per node group. The openDesk Edu reference deployment requests 32 GB RAM per node as a baseline.
- Manual scaling: Add or remove worker nodes via your infrastructure tooling (Terraform, Ansible, or bare-metal provisioning).
Node sizing guidance per service tier:
| Tier | Suggested Node Size | Services |
|---|---|---|
| Small (≤ 500 users) | 3 × 32 GB RAM, 8 vCPU | All services on shared nodes |
| Medium (500–2,000) | 5 × 64 GB RAM, 16 vCPU | Dedicated DB nodes for Keycloak, LMS |
| Large (2,000–5,000) | 7 × 128 GB RAM, 32 vCPU | Separate node pools for LMS, Mail, Files |
Log Collection
Centralized Logging Options
The openDesk Edu stack supports integration with several logging backends:
| Backend | Deployment Method | Retention Default | Recommended For |
|---|---|---|---|
| Loki + Grafana | Included in monitoring chart | 7 days | Small to medium deployments |
| Elasticsearch + Kibana | Manual deployment via ECK | 30 days | Large deployments, audit compliance |
| journalctl (no centralization) | No additional software | Systemd default | Dev/test only |
To ship logs to Loki if not already configured:
global:
logging:
backend: loki
loki:
url: http://loki:3100/loki/api/v1/push
Default Log Locations Per Service
Without centralized logging, logs remain in each pod:
| Service | Log Path (inside container) | Relevant Events |
|---|---|---|
| Keycloak | /opt/keycloak/data/log/server.log |
Login failures, token errors, SAML assertion issues |
| Nextcloud | /var/www/html/data/nextcloud.log |
File sync errors, share failures |
| ILIAS | /var/www/ilias/data/log/ |
LMS access issues, plugin errors |
| Grommunio | /var/log/grommunio/ |
SMTP errors, mailbox quota warnings |
| PostgreSQL | /var/log/postgresql/ |
Connection errors, replication lag |
| NGINX ingress | /var/log/nginx/ |
Access and error logs per service |
Log Retention Policies
For centralized logging, configure retention based on your storage budget:
- Loki: 7–30 days (default 7)
- Elasticsearch: 30–90 days via ILM (Index Lifecycle Management)
- Pod logs (
kubectl logs): Kubernetes reaps them when the pod is deleted — set--max-log-agein kubelet config for longer retention on disk.
Using kubectl Logs Effectively
# Stream all service logs in real time
kubetail -n opendesk-edu -l app.kubernetes.io/component=opendesk-edu
# Follow a specific deployment
kubectl -n opendesk-edu logs -f deploy/keycloak
# Last 100 lines with timestamps
kubectl -n opendesk-edu logs --tail=100 --timestamps deploy/nextcloud
# Search for errors across all pods
kubectl -n opendesk-edu logs --selector=app.kubernetes.io/instance=opendesk-edu \
--tail=1000 | grep -i error | head -50
# Multi-container pod: specify container
kubectl -n opendesk-edu logs deploy/ilias -c ilias-fpm
Periodic Maintenance Tasks
Daily
- Quick health check:
curl https://opendesk-edu.org/api/health— should return 200. - Certificate expiry scan:
kubectl -n opendesk-edu get certificate -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.notAfter}{"\n"}{end}'— flag anything under 30 days. - Review failed backup jobs:
kubectl -n opendesk-edu get jobs -l app=k8up | grep -E "0/1|Error".
Weekly
- Backup verification: Run
restic checkon the backup repository. List recent snapshots and verify a random sample. - Pod review:
kubectl -n opendesk-edu get pods— investigate any CrashLoopBackOff, OOMKilled, or pending pods. - Review Keycloak active sessions: Verify session count is within expected range for your user base.
Monthly
- Update check: Review openDesk Edu GitHub releases for new versions. Read release notes for breaking changes.
- Security audit: Check for CVEs affecting the component versions you run. Subscribe to the openDesk Edu security mailing list.
- Resource trend analysis: Compare Prometheus/Grafana resource usage graphs to the previous month. Identify services growing faster than expected.
- User feedback: Check support tickets for patterns that indicate operational issues.
Quarterly
- Full restore drill: Execute a complete restore in a staging environment. Include Keycloak, Nextcloud/OpenCloud, ILIAS/Moodle, and Grommunio. Time the process and document any improvements.
- Keycloak session cleanup: Review and purge stale sessions:
kubectl -n opendesk-edu exec deploy/keycloak -- \ /opt/keycloak/bin/kcadm.sh logout --all --realm your-realm - Helm chart updates: Run
helmfile depsandhelmfile diffto verify compatibility with the next planned release. - Capacity planning review: Analyze PVC usage growth, node resource utilization, and user growth trends. Plan any resource expansions for the next quarter.
Conclusion
Operating openDesk Edu is an ongoing process, not a one-time setup. The key to a stable platform is consistency: regular health checks, verified backups, disciplined update procedures, and proactive capacity management.
Build runbooks for your specific environment, document deviations from the reference deployment, and share operational knowledge across your team. When something breaks — and it will — the time you invested in preparation will be repaid many times over.
For support, refer to the openDesk Edu community forum or contact the project maintainers via the GitHub repository.
