Skip to content

Instantly share code, notes, and snippets.

@Jordan-Hall
Last active October 14, 2025 23:19
Show Gist options
  • Save Jordan-Hall/62f8c5f1f21f3165325aaca47e241d5d to your computer and use it in GitHub Desktop.
Save Jordan-Hall/62f8c5f1f21f3165325aaca47e241d5d to your computer and use it in GitHub Desktop.
#!/bin/bash
set -e
# -------------------------
# DEFAULT CONFIGURATION
# -------------------------
HOST_IP_DEFAULT=$(hostname -I | awk '{print $1}')
DOMAIN_DEFAULT="$HOST_IP_DEFAULT.sslip.io"
EMAIL_DEFAULT="[email protected]"
ADMIN_USER_DEFAULT="admin"
ADMIN_PASS_DEFAULT="admin123"
SURREALDB_REPLICAS_DEFAULT=3
COOLIFY_REPLICAS_DEFAULT=1
KUBERO_REPLICAS_DEFAULT=1
PD_REPLICAS_DEFAULT=3
TIKV_REPLICAS_DEFAULT=3
STORAGE_PATH_DEFAULT="/mnt/tikv-data"
K3S_MASTER_TOKEN_FILE="/root/k3s_token.txt"
K3S_MASTER_IP_FILE="/root/k3s_master_ip.txt"
# -------------------------
# USER PARAMETERS
# -------------------------
HOST_IP="${HOST_IP:-$HOST_IP_DEFAULT}"
DOMAIN="${DOMAIN:-$DOMAIN_DEFAULT}"
EMAIL="${EMAIL:-$EMAIL_DEFAULT}"
ADMIN_USER="${ADMIN_USER:-$ADMIN_USER_DEFAULT}"
ADMIN_PASS="${ADMIN_PASS:-$ADMIN_PASS_DEFAULT}"
SURREALDB_REPLICAS="${SURREALDB_REPLICAS:-$SURREALDB_REPLICAS_DEFAULT}"
COOLIFY_REPLICAS="${COOLIFY_REPLICAS:-$COOLIFY_REPLICAS_DEFAULT}"
KUBERO_REPLICAS="${KUBERO_REPLICAS:-$KUBERO_REPLICAS_DEFAULT}"
PD_REPLICAS="${PD_REPLICAS:-$PD_REPLICAS_DEFAULT}"
TIKV_REPLICAS="${TIKV_REPLICAS:-$TIKV_REPLICAS_DEFAULT}"
STORAGE_PATH="${STORAGE_PATH:-$STORAGE_PATH_DEFAULT}"
# -------------------------
# SYSTEM UPDATE & FIREWALL
# -------------------------
echo "Updating system..."
sudo apt update -y && sudo apt upgrade -y
sudo apt install -y curl wget gnupg lsb-release ufw software-properties-common apt-transport-https
echo "Configuring firewall..."
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 6443/tcp
sudo ufw allow 80,443/tcp
sudo ufw --force enable
# -------------------------
# DOCKER INSTALLATION
# -------------------------
if ! command -v docker >/dev/null 2>&1; then
echo "Installing Docker..."
sudo apt remove -y docker docker-engine docker.io containerd runc || true
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update -y
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable docker
sudo systemctl start docker
fi
# -------------------------
# K3S INSTALL FUNCTIONS
# -------------------------
install_k3s_master() {
echo "Installing K3s master..."
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--write-kubeconfig-mode 644" sh -
sleep 10
echo $(sudo cat /var/lib/rancher/k3s/server/node-token) > $K3S_MASTER_TOKEN_FILE
echo $HOST_IP > $K3S_MASTER_IP_FILE
}
install_k3s_agent() {
local master_ip="$1"
local token="$2"
echo "Installing K3s agent..."
curl -sfL https://get.k3s.io | K3S_URL="https://$master_ip:6443" K3S_TOKEN="$token" sh -
}
wait_nodes_ready() {
echo "Waiting for all nodes to be ready..."
kubectl wait --for=condition=Ready nodes --all --timeout=180s
}
# -------------------------
# INSTALL K3S
# -------------------------
if [ -f /etc/rancher/k3s/k3s.yaml ]; then
echo "K3s already installed."
else
if [ ! -f $K3S_MASTER_TOKEN_FILE ]; then
install_k3s_master
else
MASTER_IP=$(cat $K3S_MASTER_IP_FILE)
K3S_TOKEN=$(cat $K3S_MASTER_TOKEN_FILE)
install_k3s_agent $MASTER_IP $K3S_TOKEN
fi
fi
wait_nodes_ready
# -------------------------
# CREATE NAMESPACES
# -------------------------
for ns in tikv surrealdb coolify kubero cert-manager monitoring; do
kubectl create namespace $ns --dry-run=client -o yaml | kubectl apply -f -
done
# -------------------------
# INSTALL NGINX INGRESS CONTROLLER
# -------------------------
echo "Installing Nginx Ingress Controller..."
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml
kubectl wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=180s
# -------------------------
# INSTALL METRICS SERVER
# -------------------------
echo "Installing Metrics Server..."
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
# -------------------------
# INSTALL CERT MANAGER
# -------------------------
echo "Installing Cert Manager..."
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.2/cert-manager.yaml
echo "Waiting for cert-manager to be ready..."
kubectl wait --namespace cert-manager \
--for=condition=ready pod \
--selector=app.kubernetes.io/instance=cert-manager \
--timeout=180s
# -------------------------
# INSTALL KUBERO OPERATOR
# -------------------------
echo "Installing Kubero Operator..."
kubectl apply -f https://raw.githubusercontent.com/kubero-dev/kubero-operator/main/deploy/operator.yaml
# -------------------------
# INSTALL MONITORING STACK (Prometheus)
# -------------------------
echo "Installing Prometheus monitoring stack..."
kubectl apply -f https://raw.githubusercontent.com/kubero-dev/kubero-operator/main/config/samples/application_v1alpha1_kuberoprometheus.yaml
# -------------------------
# INSTALL KUBERO UI
# -------------------------
echo "Installing Kubero UI..."
kubectl create namespace kubero --dry-run=client -o yaml | kubectl apply -f -
# Create Kubero secrets
echo "Creating Kubero secrets..."
kubectl create secret generic kubero-secrets \
--from-literal=KUBERO_WEBHOOK_SECRET=$(openssl rand -hex 20) \
--from-literal=KUBERO_SESSION_KEY=$(openssl rand -hex 20) \
-n kubero --dry-run=client -o yaml | kubectl apply -f -
# Deploy Kubero UI
kubectl apply -f https://raw.githubusercontent.com/kubero-dev/kubero-operator/main/config/samples/application_v1alpha1_kubero.yaml -n kubero
# -------------------------
# CLEAN UP EXISTING TIKV RESOURCES (if upgrading)
# -------------------------
echo "Checking for existing TiKV resources..."
if kubectl get statefulset pd -n tikv >/dev/null 2>&1; then
echo "Deleting existing PD StatefulSet..."
kubectl delete statefulset pd -n tikv --cascade=orphan || true
fi
if kubectl get statefulset tikv -n tikv >/dev/null 2>&1; then
echo "Deleting existing TiKV StatefulSet..."
kubectl delete statefulset tikv -n tikv --cascade=orphan || true
fi
if kubectl get deployment surrealdb -n surrealdb >/dev/null 2>&1; then
echo "Deleting existing SurrealDB Deployment..."
kubectl delete deployment surrealdb -n surrealdb || true
fi
echo "Waiting for resources to be cleaned up..."
sleep 5
# -------------------------
# DEPLOY PD (Placement Driver)
# -------------------------
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: pd
namespace: tikv
spec:
serviceName: pd
replicas: $PD_REPLICAS
podManagementPolicy: Parallel
selector:
matchLabels:
app: pd
template:
metadata:
labels:
app: pd
spec:
containers:
- name: pd
image: pingcap/pd:latest
command:
- /pd-server
args:
- --name=\$(POD_NAME)
- --data-dir=/pd/data
- --client-urls=http://0.0.0.0:2379
- --advertise-client-urls=http://\$(POD_NAME).pd.tikv.svc.cluster.local:2379
- --peer-urls=http://0.0.0.0:2380
- --advertise-peer-urls=http://\$(POD_NAME).pd.tikv.svc.cluster.local:2380
- --initial-cluster=pd-0=http://pd-0.pd.tikv.svc.cluster.local:2380,pd-1=http://pd-1.pd.tikv.svc.cluster.local:2380,pd-2=http://pd-2.pd.tikv.svc.cluster.local:2380
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
ports:
- containerPort: 2379
name: client
- containerPort: 2380
name: peer
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
readinessProbe:
httpGet:
path: /health
port: 2379
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
livenessProbe:
httpGet:
path: /health
port: 2379
initialDelaySeconds: 60
periodSeconds: 20
timeoutSeconds: 5
volumeMounts:
- name: pd-data
mountPath: /pd/data
volumeClaimTemplates:
- metadata:
name: pd-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: Service
metadata:
name: pd
namespace: tikv
spec:
clusterIP: None
selector:
app: pd
ports:
- port: 2379
targetPort: 2379
EOF
# -------------------------
# DEPLOY TiKV
# -------------------------
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: tikv
namespace: tikv
spec:
serviceName: tikv
replicas: $TIKV_REPLICAS
selector:
matchLabels:
app: tikv
template:
metadata:
labels:
app: tikv
spec:
initContainers:
- name: wait-for-pd
image: busybox:latest
command:
- sh
- -c
- |
until nc -z pd-0.pd.tikv.svc.cluster.local 2379; do
echo "Waiting for PD to be ready..."
sleep 5
done
containers:
- name: tikv
image: pingcap/tikv:latest
command:
- /tikv-server
args:
- --pd=pd-0.pd.tikv.svc.cluster.local:2379,pd-1.pd.tikv.svc.cluster.local:2379,pd-2.pd.tikv.svc.cluster.local:2379
- --addr=0.0.0.0:20160
- --advertise-addr=\$(POD_NAME).tikv.tikv.svc.cluster.local:20160
- --data-dir=/tikv/data
- --log-level=info
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
ports:
- containerPort: 20160
name: server
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"
readinessProbe:
tcpSocket:
port: 20160
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
livenessProbe:
tcpSocket:
port: 20160
initialDelaySeconds: 120
periodSeconds: 20
timeoutSeconds: 5
volumeMounts:
- name: tikv-data
mountPath: /tikv/data
volumeClaimTemplates:
- metadata:
name: tikv-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
name: tikv
namespace: tikv
spec:
clusterIP: None
selector:
app: tikv
ports:
- port: 20160
targetPort: 20160
EOF
# -------------------------
# DEPLOY SURREALDB
# -------------------------
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: surrealdb
namespace: surrealdb
spec:
replicas: $SURREALDB_REPLICAS
selector:
matchLabels:
app: surrealdb
template:
metadata:
labels:
app: surrealdb
spec:
initContainers:
- name: wait-for-tikv
image: busybox:latest
command:
- sh
- -c
- |
until nc -z pd-0.pd.tikv.svc.cluster.local 2379; do
echo "Waiting for PD to be ready..."
sleep 5
done
echo "PD is ready, waiting for TiKV..."
sleep 30
containers:
- name: surrealdb
image: surrealdb/surrealdb:latest
args:
- start
- "--log"
- "info"
- "--bind"
- "0.0.0.0:8000"
- "--kvs"
- "tikv://pd-0.pd.tikv.svc.cluster.local:2379"
ports:
- containerPort: 8000
name: http
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 60
periodSeconds: 20
timeoutSeconds: 5
EOF
# -------------------------
# DEPLOY COOLIFY
# -------------------------
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: coolify
namespace: coolify
spec:
replicas: $COOLIFY_REPLICAS
selector:
matchLabels:
app: coolify
template:
metadata:
labels:
app: coolify
spec:
containers:
- name: coolify
image: coollabsio/coolify:latest
ports:
- containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: coolify
namespace: coolify
spec:
selector:
app: coolify
ports:
- port: 80
targetPort: 3000
EOF
# Kubero UI is now installed via the official Kubero manifest above
# -------------------------
# PROMETHEUS + GRAFANA
# -------------------------
# Note: Prometheus is now installed via Kubero monitoring stack above
echo "Prometheus monitoring stack installed via Kubero..."
# -------------------------
# WAIT FOR ALL PODS READY
# -------------------------
echo "Waiting for critical pods to be ready..."
# Wait for PD StatefulSet
echo "Waiting for PD pods..."
kubectl rollout status statefulset/pd -n tikv --timeout=300s || true
# Wait for TiKV StatefulSet
echo "Waiting for TiKV pods..."
kubectl rollout status statefulset/tikv -n tikv --timeout=300s || true
# Wait for SurrealDB
echo "Waiting for SurrealDB pods..."
kubectl rollout status deployment/surrealdb -n surrealdb --timeout=300s || true
# Wait for Coolify
echo "Waiting for Coolify pods..."
kubectl rollout status deployment/coolify -n coolify --timeout=300s || true
# Wait for Kubero Operator
echo "Waiting for Kubero Operator..."
kubectl rollout status deployment/kubero-operator-controller-manager -n kubero-operator-system --timeout=300s || true
echo ""
echo "βœ… Full production-ready cluster installed!"
echo ""
echo "πŸ“Š Service URLs:"
echo " Coolify: https://coolify.$DOMAIN"
echo " SurrealDB: https://surrealdb.$DOMAIN"
echo " Kubero Dashboard: https://kubero.$DOMAIN"
echo " Grafana: https://grafana.$DOMAIN"
echo ""
echo "πŸ“ Check pod status with: kubectl get pods --all-namespaces"
echo "πŸŽ‰ Multi-node ready β€” copy k3s_token.txt & k3s_master_ip.txt to new nodes to join agents."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment