Start Tomorrow - Everything Explained Simply


Table of Contents

  1. What We Are Building
  2. Key Words Dictionary
  3. Day 1: Install K3s
  4. Day 2: Install Basic Tools
  5. Day 3: Create Your First App
  6. Day 4: Setup Auto-Deploy
  7. Day 5: Connect Dashboard
  8. Week 2: Add All Features
  9. Common Problems & Solutions
  10. Complete Scripts Ready to Use

Part 1: What We Are Building

Simple Explanation

Current Situation (BAD):

  • You manually create subdomain for each client (3 hours work)
  • You manually update each client’s system
  • If system crashes, you fix manually
  • Managing 150+ clients is very hard

New System (GOOD):

  • Computer creates subdomain automatically (5 minutes)
  • GitHub automatically builds and deploys your code
  • If system crashes, K3s fixes it automatically
  • Managing 1000+ clients becomes easy

Picture of New System

Your Code (GitHub)
    ↓ (automatic)
Build System (GitHub Actions)
    ↓ (automatic)
K3s Server (Your Ubuntu Server)
    ↓ (automatic)
150+ Clients (Each in separate container)
    ↓ (automatic)
MongoDB Database (Your existing database)

What Each Part Does

Part What It Does Why You Need It
K3s Manages all containers Like a smart PM2 that fixes itself
Docker Packages your app Makes your app portable
GitHub Actions Builds code automatically No manual building
Helm Template for clients Create 100 clients with same template
ArgoCD Deploys automatically Watches GitHub and deploys
Traefik Routes web traffic Like nginx but automatic

Part 2: Key Words Dictionary

Simple Words for Complex Things

Complex Word Simple Meaning Example
Container Box with your app inside Like a mini-computer for each client
Pod Container running in K3s Your app running
Node Physical server Your Ubuntu server
Cluster Group of servers Your 3 Ubuntu servers working together
Deployment Instructions to run app “Run 3 copies of my app”
Service Internal web address How pods talk to each other
Ingress External web address How internet reaches your app
Namespace Isolated area Each client gets own namespace
Helm Chart Template Like a form you fill for each client
Image Packaged app Your code turned into Docker image
Registry Image storage Where Docker images are saved
kubectl K3s command tool How you talk to K3s
YAML Configuration file Settings file (like JSON)

Part 3: Day 1 - Install K3s

🎯 Goal for Day 1

Install K3s on your Ubuntu server and verify it works.

📝 What You Need Before Starting

✅ 1 Ubuntu server (minimum: 4 CPU, 8GB RAM, 100GB disk)
✅ Root or sudo access
✅ Internet connection
✅ Ports open: 6443, 80, 443
✅ Your server IP address

Step 1.1: Connect to Your Server

# From your computer
ssh root@YOUR_SERVER_IP

# Example:
ssh [email protected]

🔑 Key Points:

  • Replace YOUR_SERVER_IP with your actual IP
  • Use your username if not root
  • You should see Ubuntu terminal after connecting

Step 1.2: Update Your Server

# Update package list
sudo apt update

# Upgrade all packages
sudo apt upgrade -y

# Install basic tools
sudo apt install -y curl wget git vim

🔑 Key Points:

  • apt update = Get list of new software
  • apt upgrade = Install new versions
  • -y = Say “yes” automatically
  • This takes 5-10 minutes

Step 1.3: Configure Firewall

# Allow K3s port
sudo ufw allow 6443/tcp

# Allow web traffic
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Allow Kubernetes port
sudo ufw allow 10250/tcp

# Enable firewall
sudo ufw enable

# Check status
sudo ufw status

🔑 Key Points:

  • Firewall = Security that blocks unwanted connections
  • Port 6443 = K3s API (how you control K3s)
  • Port 80 = HTTP (web traffic)
  • Port 443 = HTTPS (secure web traffic)
  • Port 10250 = Kubelet (K3s internal communication)

Step 1.4: Install K3s

# Download and install K3s
curl -sfL https://get.k3s.io | sh -

# Wait 30 seconds for K3s to start
sleep 30

# Check if K3s is running
sudo systemctl status k3s

# Get list of nodes
sudo k3s kubectl get nodes

🔑 Key Points:

  • This installs K3s in 1 minute
  • K3s starts automatically
  • You should see “active (running)” in green
  • You should see your server name in nodes list

Step 1.5: Configure kubectl (Command Tool)

# Create config directory
mkdir -p ~/.kube

# Copy K3s config
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config

# Set correct permissions
sudo chown $(whoami) ~/.kube/config
chmod 600 ~/.kube/config

# Install kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl

# Test kubectl
kubectl get nodes

🔑 Key Points:

  • kubectl = Tool to control K3s
  • Config file = Tells kubectl how to connect to K3s
  • You should see your server name when running kubectl get nodes

Step 1.6: Verify Everything Works

# Check all K3s components
kubectl get all -A

# You should see:
# ✅ kube-system namespace with pods
# ✅ All pods showing "Running"
# ✅ Traefik pod (this is your ingress)

🔑 Key Points:

  • -A = Show all namespaces
  • All pods should be “Running” or “Completed”
  • If any pod is “Error” or “CrashLoopBackOff”, there’s a problem

🎉 Day 1 Complete!

What you achieved:

  • ✅ K3s installed and running
  • ✅ kubectl configured
  • ✅ Firewall configured
  • ✅ Ready for Day 2

Part 4: Day 2 - Install Basic Tools

🎯 Goal for Day 2

Install Helm, Docker Registry, and ArgoCD for automatic deployments.

Step 2.1: Install Helm (Package Manager)

# Download Helm installer
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# Verify Helm installed
helm version

# You should see version 3.x.x

🔑 Key Points:

  • Helm = Package manager for K3s (like npm for Node.js)
  • Helm helps install complex applications easily
  • Helm uses “charts” (templates) to install apps

Step 2.2: Install Docker on Server

# Install Docker
curl -fsSL https://get.docker.com | sh

# Add your user to docker group
sudo usermod -aG docker $USER

# Restart shell or logout/login
exit
# Then SSH back in

# Test Docker
docker version

🔑 Key Points:

  • Docker = Tool to build container images
  • We need Docker to build your app images
  • Adding user to docker group = Run docker without sudo

Step 2.3: Setup Local Docker Registry

# Create namespace for registry
kubectl create namespace docker-registry

# Create registry deployment
kubectl create deployment docker-registry \
  --image=registry:2 \
  --namespace=docker-registry

# Expose registry
kubectl expose deployment docker-registry \
  --type=NodePort \
  --port=5000 \
  --namespace=docker-registry

# Get registry port
kubectl get svc -n docker-registry

# You'll see something like 5000:32000/TCP
# The 32000 is your registry port (yours will be different)

🔑 Key Points:

  • Registry = Place to store Docker images
  • Local registry = Free alternative to Docker Hub
  • NodePort = Makes registry accessible from outside
  • Note your port number (like 32000) - you’ll need it

Step 2.4: Test Docker Registry

# Tag and push test image
docker pull nginx:latest
docker tag nginx:latest localhost:32000/nginx:test
docker push localhost:32000/nginx:test

# If successful, you'll see "Pushed"

🔑 Key Points:

  • Replace 32000 with your actual port
  • This tests if registry works
  • “Pushed” means registry is working

Step 2.5: Install ArgoCD (Auto-Deployment)

# Create ArgoCD namespace
kubectl create namespace argocd

# Install ArgoCD
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# Wait for ArgoCD to start (2-3 minutes)
kubectl wait --for=condition=Ready pods --all -n argocd --timeout=300s

# Get ArgoCD password
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d

# Save this password! You need it to login
# Example password: 4Xy2PqR8nNmZwL3K

🔑 Key Points:

  • ArgoCD = Watches GitHub and deploys automatically
  • This takes 2-3 minutes to install
  • Save the password - you need it!
  • Username is always “admin”

Step 2.6: Access ArgoCD UI

# Option 1: Port forward (temporary)
kubectl port-forward svc/argocd-server -n argocd 8080:443 &

# Access at: https://localhost:8080
# Username: admin
# Password: (the one you got above)

#  Install kubectl on macOS
brew install kubectl

# Copy your kubeconfig from the server, run on your (local)
scp user@YOUR_SERVER_IP:~/.kube/config ~/.kube/config

# Edit the kube config file from your local
nano ~/.kube/config

# from the line   server: https://127.0.0.1:6443 replace with tour real IP address

# Verify connection to verify it connect to the server (local)
kubectl get pods -n argocd

# Start port-forwarding, this make temp forwarding to access the ArgoCD from your local machine (local)
kubectl port-forward svc/argocd-server -n argocd 8080:443

# Option 2: Expose via LoadBalancer (permanent)
# Note: this not recommended for production
kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "NodePort"}}'

# Get the port
kubectl get svc argocd-server -n argocd

# Access at: https://YOUR_SERVER_IP:PORT

🔑 Key Points:

  • ArgoCD has web interface
  • Use “admin” and password to login
  • Accept the SSL warning (it’s safe)
  • You’ll see ArgoCD dashboard

🎉 Day 2 Complete!

What you achieved:

  • ✅ Helm installed (package manager)
  • ✅ Docker installed (build images)
  • ✅ Docker Registry running (store images)
  • ✅ ArgoCD installed (auto-deployment)

Part 5: Day 3 - Create Your First App

🎯 Goal for Day 3

Deploy your first client application to K3s.

Step 3.1: Create Helm Chart Template

# Create charts directory
mkdir -p ~/charts
cd ~/charts

# Create chart for your SaaS app
helm create saas-app

# Go to chart directory
cd saas-app

🔑 Key Points:

  • Chart = Template for your application
  • This creates basic files you’ll edit
  • All clients will use this template

Step 3.2: Edit Chart Values

# Edit values.yaml
nano values.yaml

# Delete everything and add this:
# Client configuration
client:
  name: "" # Client name (e.g., pethouse)
  subdomain: "" # Full domain (e.g., pethouse.yourdomain.com)

# Image configuration
image:
  repository: localhost:32000/saas-app
  tag: "latest"
  pullPolicy: Always

# MongoDB configuration
mongodb:
  uri: "" # MongoDB connection string
  database: "" # Database name

# Resources
resources:
  limits:
    cpu: 500m # Maximum CPU (0.5 core)
    memory: 512Mi # Maximum RAM
  requests:
    cpu: 100m # Minimum CPU (0.1 core)
    memory: 256Mi # Minimum RAM

# Scaling
replicas: 1 # Number of pods
autoscaling:
  enabled: true
  minReplicas: 1
  maxReplicas: 5
  targetCPU: 70 # Scale at 70% CPU

🔑 Key Points:

  • This file contains default settings
  • Each client can override these values
  • CPU: 1000m = 1 full CPU core
  • Memory: 1024Mi = 1GB RAM

Step 3.3: Create Deployment Template

# Edit deployment template
vim templates/deployment.yaml

# Delete everything and add:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.client.name }}-app
  labels:
    app: {{ .Values.client.name }}
    version: {{ .Values.image.tag }}
spec:
  replicas: {{ .Values.replicas }}
  selector:
    matchLabels:
      app: {{ .Values.client.name }}
  template:
    metadata:
      labels:
        app: {{ .Values.client.name }}
    spec:
      containers:
      - name: app
        image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
        ports:
        - containerPort: 3000
        env:
        - name: CLIENT_NAME
          value: {{ .Values.client.name }}
        - name: MONGODB_URI
          value: {{ .Values.mongodb.uri }}
        - name: DATABASE_NAME
          value: {{ .Values.mongodb.database }}
        resources:
          limits:
            cpu: {{ .Values.resources.limits.cpu }}
            memory: {{ .Values.resources.limits.memory }}
          requests:
            cpu: {{ .Values.resources.requests.cpu }}
            memory: {{ .Values.resources.requests.memory }}
        # Health checks
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 5

🔑 Key Points:

  • {{ .Values.xxx }} = Gets replaced with actual values
  • livenessProbe = Checks if app is alive
  • readinessProbe = Checks if app is ready for traffic
  • This creates pods for your app

Step 3.4: Create Service Template

# Edit service template
vim templates/service.yaml

# Delete everything and add:
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.client.name }}-service
spec:
  type: ClusterIP
  selector:
    app: {{ .Values.client.name }}
  ports:
  - port: 80
    targetPort: 3000
    protocol: TCP

🔑 Key Points:

  • Service = Internal address for your app
  • ClusterIP = Only accessible inside K3s
  • Port 80 = Service port
  • Port 3000 = Your app port

Step 3.5: Create Ingress Template

# Edit ingress template
vim templates/ingress.yaml

# Delete everything and add:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ .Values.client.name }}-ingress
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
spec:
  rules:
  - host: {{ .Values.client.subdomain }}
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: {{ .Values.client.name }}-service
            port:
              number: 80
  tls:
  - hosts:
    - {{ .Values.client.subdomain }}
    secretName: {{ .Values.client.name }}-tls

🔑 Key Points:

  • Ingress = External address (subdomain)
  • Automatically gets SSL certificate
  • Routes traffic from internet to your service

Step 3.6: Create Auto-Scaling Template

# Create HPA template
vim templates/hpa.yaml

# Add this content:
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: {{ .Values.client.name }}-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: {{ .Values.client.name }}-app
  minReplicas: {{ .Values.autoscaling.minReplicas }}
  maxReplicas: {{ .Values.autoscaling.maxReplicas }}
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: {{ .Values.autoscaling.targetCPU }}
{{- end }}

🔑 Key Points:

  • HPA = Horizontal Pod Autoscaler
  • Automatically adds/removes pods based on CPU
  • Scales between min and max replicas
  • Only created if autoscaling.enabled = true

Step 3.7: Test Your First Deployment

# Create test namespace
kubectl create namespace test-client

# Create test values file
cat > test-values.yaml <<EOF
client:
  name: testclient
  subdomain: testclient.yourdomain.com

mongodb:
  uri: "mongodb://user:pass@mongodb-server:27017/testclient_db"
  database: "testclient_db"

image:
  repository: nginx
  tag: latest
EOF

# Deploy using Helm
helm install testclient ./saas-app \
  --namespace test-client \
  --values test-values.yaml

# Check deployment
kubectl get all -n test-client

# You should see:
# ✅ deployment.apps/testclient-app
# ✅ service/testclient-service
# ✅ pod/testclient-app-xxxxx (Running)

🔑 Key Points:

  • This deploys nginx as test
  • Replace with your actual app later
  • All resources created automatically
  • Pod should show “Running”

🎉 Day 3 Complete!

What you achieved:

  • ✅ Created Helm chart template
  • ✅ Configured deployment, service, ingress
  • ✅ Added auto-scaling
  • ✅ Deployed first test client

Part 6: Day 4 - Setup Auto-Deploy

🎯 Goal for Day 4

Setup GitHub Actions to build and deploy automatically.

Step 4.1: Create GitHub Repository

# On your local computer (not server)
# Create new repository for your app

mkdir my-saas-app
cd my-saas-app
git init

# Create basic Node.js app
cat > package.json <<EOF
{
  "name": "saas-app",
  "version": "1.0.0",
  "scripts": {
    "start": "node server.js",
    "test": "echo 'Tests passed'"
  }
}
EOF

# Create simple server
cat > server.js <<EOF
const http = require('http');
const mongoUri = process.env.MONGODB_URI;
const clientName = process.env.CLIENT_NAME;

const server = http.createServer((req, res) => {
  if (req.url === '/health') {
    res.writeHead(200);
    res.end('OK');
  } else if (req.url === '/ready') {
    res.writeHead(200);
    res.end('Ready');
  } else {
    res.writeHead(200);
    res.end(\`Hello from \${clientName}\`);
  }
});

server.listen(3000, () => {
  console.log(\`Server running for \${clientName}\`);
});
EOF

# Create Dockerfile
cat > Dockerfile <<EOF
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
EOF

🔑 Key Points:

  • This creates simple Node.js app
  • Has /health and /ready endpoints for K3s
  • Reads CLIENT_NAME from environment
  • You’ll replace with your actual app

Step 4.2: Create GitHub Actions Workflow

# Create workflows directory
mkdir -p .github/workflows

# Create workflow file
cat > .github/workflows/build-deploy.yml <<EOF
name: Build and Deploy

# When to run
on:
  push:
    branches: [main, beta, develop]
  pull_request:
    branches: [main]

# Environment variables
env:
  REGISTRY: localhost:32000  # Your registry (change port!)
  IMAGE_NAME: saas-app

jobs:
  # Job 1: Run tests
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '16'

    - name: Install dependencies
      run: npm install

    - name: Run tests
      run: npm test

  # Job 2: Build and push image
  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - name: Set version
      id: version
      run: |
        # Create version from commit
        SHORT_SHA=\$(echo \${{ github.sha }} | cut -c1-7)
        BRANCH=\${GITHUB_REF#refs/heads/}

        # Set version based on branch
        if [[ "\$BRANCH" == "main" ]]; then
          VERSION="lts-\$SHORT_SHA"
          echo "version=\$VERSION" >> \$GITHUB_OUTPUT
          echo "environment=lts" >> \$GITHUB_OUTPUT
        elif [[ "\$BRANCH" == "beta" ]]; then
          VERSION="beta-\$SHORT_SHA"
          echo "version=\$VERSION" >> \$GITHUB_OUTPUT
          echo "environment=beta" >> \$GITHUB_OUTPUT
        else
          VERSION="dev-\$SHORT_SHA"
          echo "version=\$VERSION" >> \$GITHUB_OUTPUT
          echo "environment=dev" >> \$GITHUB_OUTPUT
        fi

    - name: Build Docker image
      run: |
        docker build -t \${{ env.REGISTRY }}/\${{ env.IMAGE_NAME }}:\${{ steps.version.outputs.version }} .
        docker tag \${{ env.REGISTRY }}/\${{ env.IMAGE_NAME }}:\${{ steps.version.outputs.version }} \${{ env.REGISTRY }}/\${{ env.IMAGE_NAME }}:latest

    - name: Push to registry
      run: |
        # Note: In real setup, you'd push to your server's registry
        # For now, this is example
        echo "Would push: \${{ env.REGISTRY }}/\${{ env.IMAGE_NAME }}:\${{ steps.version.outputs.version }}"

    - name: Update deployment file
      run: |
        # Create deployment record
        cat > deployment.json <<END
        {
          "version": "\${{ steps.version.outputs.version }}",
          "commit": "\${{ github.sha }}",
          "branch": "\${GITHUB_REF#refs/heads/}",
          "author": "\${{ github.actor }}",
          "message": "\${{ github.event.head_commit.message }}",
          "timestamp": "\$(date -u +%Y-%m-%dT%H:%M:%SZ)",
          "environment": "\${{ steps.version.outputs.environment }}"
        }
        END

        # In real setup, send to your API
        # curl -X POST https://your-api/deployments -d @deployment.json
EOF

🔑 Key Points:

  • Runs on every push to main/beta/develop
  • Creates version like “lts-abc123”
  • Builds Docker image
  • Tags based on branch (lts/beta/dev)
  • You need to update REGISTRY port

Step 4.3: Push to GitHub

# Add all files
git add .

# Commit
git commit -m "Initial commit"

# Create repository on GitHub (via website)
# Then add remote
git remote add origin https://github.com/yourusername/my-saas-app.git

# Push
git push -u origin main

🔑 Key Points:

  • Create repository on GitHub.com first
  • Replace “yourusername” with your GitHub username
  • GitHub Actions will run automatically

Step 4.4: Create Deployment Script

# Back on your server
cd ~

# Create scripts directory
mkdir -p k3s-scripts
cd k3s-scripts

# Create deployment script
cat > deploy-client.sh <<'EOF'
#!/bin/bash

# Script to deploy new client
# Usage: ./deploy-client.sh CLIENT_NAME SUBDOMAIN

CLIENT_NAME=$1
SUBDOMAIN=$2

if [ -z "$CLIENT_NAME" ] || [ -z "$SUBDOMAIN" ]; then
    echo "Usage: ./deploy-client.sh CLIENT_NAME SUBDOMAIN"
    echo "Example: ./deploy-client.sh pethouse pethouse.mydomain.com"
    exit 1
fi

echo "======================================"
echo "Deploying new client: $CLIENT_NAME"
echo "Subdomain: $SUBDOMAIN"
echo "======================================"

# Step 1: Create namespace
echo "Creating namespace..."
kubectl create namespace client-$CLIENT_NAME

# Step 2: Create MongoDB URI (adjust for your MongoDB)
MONGO_URI="mongodb://username:password@mongodb-server:27017/${CLIENT_NAME}_db"
MONGO_DB="${CLIENT_NAME}_db"

# Step 3: Create values file
echo "Creating values file..."
cat > /tmp/${CLIENT_NAME}-values.yaml <<END
client:
  name: $CLIENT_NAME
  subdomain: $SUBDOMAIN

mongodb:
  uri: "$MONGO_URI"
  database: "$MONGO_DB"

image:
  repository: localhost:32000/saas-app
  tag: "latest"

autoscaling:
  enabled: true
  minReplicas: 1
  maxReplicas: 5
  targetCPU: 70
END

# Step 4: Deploy using Helm
echo "Deploying with Helm..."
helm install $CLIENT_NAME ~/charts/saas-app \
  --namespace client-$CLIENT_NAME \
  --values /tmp/${CLIENT_NAME}-values.yaml

# Step 5: Wait for deployment
echo "Waiting for pods to be ready..."
kubectl wait --for=condition=ready pod \
  -l app=$CLIENT_NAME \
  -n client-$CLIENT_NAME \
  --timeout=120s

# Step 6: Check status
echo "======================================"
echo "Deployment complete!"
echo "======================================"
kubectl get all -n client-$CLIENT_NAME

echo ""
echo "Access at: https://$SUBDOMAIN"
echo "Note: DNS may take a few minutes to propagate"
EOF

# Make executable
chmod +x deploy-client.sh

🔑 Key Points:

  • This script deploys new client
  • Creates namespace automatically
  • Uses Helm to deploy
  • Waits for pods to be ready
  • Shows final status

Step 4.5: Create Update Script

# Create update script
cat > update-client.sh <<'EOF'
#!/bin/bash

# Script to update client version
# Usage: ./update-client.sh CLIENT_NAME VERSION

CLIENT_NAME=$1
VERSION=$2

if [ -z "$CLIENT_NAME" ] || [ -z "$VERSION" ]; then
    echo "Usage: ./update-client.sh CLIENT_NAME VERSION"
    echo "Example: ./update-client.sh pethouse beta-abc123"
    echo "Version options: latest, lts-xxx, beta-xxx, or commit hash"
    exit 1
fi

echo "======================================"
echo "Updating client: $CLIENT_NAME"
echo "New version: $VERSION"
echo "======================================"

# Update using Helm
helm upgrade $CLIENT_NAME ~/charts/saas-app \
  --namespace client-$CLIENT_NAME \
  --reuse-values \
  --set image.tag=$VERSION

# Watch rollout
kubectl rollout status deployment/${CLIENT_NAME}-app \
  -n client-$CLIENT_NAME

echo "======================================"
echo "Update complete!"
echo "======================================"
kubectl get pods -n client-$CLIENT_NAME
EOF

# Make executable
chmod +x update-client.sh

🔑 Key Points:

  • Updates existing client to new version
  • Keeps all settings except image version
  • Shows rollout progress
  • Zero-downtime update

🎉 Day 4 Complete!

What you achieved:

  • ✅ Created GitHub repository
  • ✅ Setup GitHub Actions workflow
  • ✅ Created deployment scripts
  • ✅ Ready for automatic deployments

Part 7: Day 5 - Connect Dashboard

🎯 Goal for Day 5

Create API to control everything from your SaaS dashboard.

Step 5.1: Install Python and Flask

# Install Python and pip
sudo apt update
sudo apt install -y python3 python3-pip

# Install Flask
pip3 install flask pyyaml kubernetes

# Verify installation
python3 -c "import flask; print('Flask installed')"

🔑 Key Points:

  • Flask = Simple web framework for API
  • We’ll create REST API
  • Your dashboard will call this API

Step 5.2: Create API Server

# Create API directory
mkdir -p ~/saas-api
cd ~/saas-api

# Create API file
cat > api.py <<'EOF'
from flask import Flask, request, jsonify
import subprocess
import json
import yaml
import os
from datetime import datetime

app = Flask(__name__)

# Store client information
clients = {}

# API Routes

@app.route('/health', methods=['GET'])
def health():
    """Health check endpoint"""
    return jsonify({'status': 'healthy'})

@app.route('/api/clients', methods=['GET'])
def list_clients():
    """List all clients"""
    try:
        # Get all namespaces starting with client-
        result = subprocess.run(
            ['kubectl', 'get', 'namespaces', '-o', 'json'],
            capture_output=True,
            text=True
        )

        namespaces = json.loads(result.stdout)
        client_list = []

        for ns in namespaces['items']:
            name = ns['metadata']['name']
            if name.startswith('client-'):
                client_name = name.replace('client-', '')
                client_list.append({
                    'name': client_name,
                    'namespace': name,
                    'created': ns['metadata']['creationTimestamp']
                })

        return jsonify({
            'success': True,
            'clients': client_list,
            'count': len(client_list)
        })
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)}), 500

@app.route('/api/clients/<name>', methods=['GET'])
def get_client(name):
    """Get specific client information"""
    try:
        # Get pods for client
        result = subprocess.run(
            ['kubectl', 'get', 'pods', '-n', f'client-{name}', '-o', 'json'],
            capture_output=True,
            text=True
        )

        if result.returncode != 0:
            return jsonify({'success': False, 'error': 'Client not found'}), 404

        pods = json.loads(result.stdout)
        pod_list = []

        for pod in pods['items']:
            pod_list.append({
                'name': pod['metadata']['name'],
                'status': pod['status']['phase'],
                'ready': all(c['ready'] for c in pod['status'].get('containerStatuses', []))
            })

        return jsonify({
            'success': True,
            'client': name,
            'pods': pod_list
        })
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)}), 500

@app.route('/api/clients', methods=['POST'])
def create_client():
    """Create new client"""
    try:
        data = request.json
        name = data['name']
        subdomain = data['subdomain']

        # Run deployment script
        result = subprocess.run(
            ['/home/ubuntu/k3s-scripts/deploy-client.sh', name, subdomain],
            capture_output=True,
            text=True
        )

        if result.returncode == 0:
            return jsonify({
                'success': True,
                'message': f'Client {name} created successfully',
                'output': result.stdout
            })
        else:
            return jsonify({
                'success': False,
                'error': result.stderr
            }), 400

    except Exception as e:
        return jsonify({'success': False, 'error': str(e)}), 500

@app.route('/api/clients/<name>', methods=['DELETE'])
def delete_client(name):
    """Delete client"""
    try:
        # Delete namespace (this deletes everything)
        result = subprocess.run(
            ['kubectl', 'delete', 'namespace', f'client-{name}'],
            capture_output=True,
            text=True
        )

        if result.returncode == 0:
            return jsonify({
                'success': True,
                'message': f'Client {name} deleted'
            })
        else:
            return jsonify({
                'success': False,
                'error': result.stderr
            }), 400

    except Exception as e:
        return jsonify({'success': False, 'error': str(e)}), 500

@app.route('/api/clients/<name>/version', methods=['PUT'])
def update_client_version(name):
    """Update client to specific version"""
    try:
        data = request.json
        version = data['version']

        # Run update script
        result = subprocess.run(
            ['/home/ubuntu/k3s-scripts/update-client.sh', name, version],
            capture_output=True,
            text=True
        )

        if result.returncode == 0:
            return jsonify({
                'success': True,
                'message': f'Client {name} updated to {version}',
                'output': result.stdout
            })
        else:
            return jsonify({
                'success': False,
                'error': result.stderr
            }), 400

    except Exception as e:
        return jsonify({'success': False, 'error': str(e)}), 500

@app.route('/api/clients/<name>/scale', methods=['PUT'])
def scale_client(name):
    """Scale client pods"""
    try:
        data = request.json
        replicas = data['replicas']

        # Scale deployment
        result = subprocess.run(
            ['kubectl', 'scale', 'deployment', f'{name}-app',
             '-n', f'client-{name}', '--replicas', str(replicas)],
            capture_output=True,
            text=True
        )

        if result.returncode == 0:
            return jsonify({
                'success': True,
                'message': f'Scaled {name} to {replicas} replicas'
            })
        else:
            return jsonify({
                'success': False,
                'error': result.stderr
            }), 400

    except Exception as e:
        return jsonify({'success': False, 'error': str(e)}), 500

@app.route('/api/clients/<name>/restart', methods=['POST'])
def restart_client(name):
    """Restart client pods"""
    try:
        # Restart deployment
        result = subprocess.run(
            ['kubectl', 'rollout', 'restart', 'deployment',
             f'{name}-app', '-n', f'client-{name}'],
            capture_output=True,
            text=True
        )

        if result.returncode == 0:
            return jsonify({
                'success': True,
                'message': f'Restarted {name} pods'
            })
        else:
            return jsonify({
                'success': False,
                'error': result.stderr
            }), 400

    except Exception as e:
        return jsonify({'success': False, 'error': str(e)}), 500

@app.route('/api/versions', methods=['GET'])
def list_versions():
    """List available versions"""
    # In real setup, this would check your registry
    return jsonify({
        'success': True,
        'versions': [
            {'tag': 'latest', 'type': 'latest'},
            {'tag': 'lts-abc123', 'type': 'lts'},
            {'tag': 'beta-def456', 'type': 'beta'},
            {'tag': 'dev-ghi789', 'type': 'dev'}
        ]
    })

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)
EOF

🔑 Key Points:

  • This API controls K3s
  • Each endpoint does specific task
  • Returns JSON for easy use
  • Calls your scripts internally

Step 5.3: Create Systemd Service

# Create service file
sudo cat > /etc/systemd/system/saas-api.service <<EOF
[Unit]
Description=SaaS API Server
After=network.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/saas-api
ExecStart=/usr/bin/python3 /home/ubuntu/saas-api/api.py
Restart=always

[Install]
WantedBy=multi-user.target
EOF

# Start service
sudo systemctl daemon-reload
sudo systemctl start saas-api
sudo systemctl enable saas-api

# Check status
sudo systemctl status saas-api

🔑 Key Points:

  • Service starts automatically
  • Restarts if crashes
  • Runs on port 5000
  • Access at http://YOUR_SERVER_IP:5000

Step 5.4: Test API

# Test health endpoint
curl http://localhost:5000/health

# List clients
curl http://localhost:5000/api/clients

# Create test client
curl -X POST http://localhost:5000/api/clients \
  -H "Content-Type: application/json" \
  -d '{"name":"testapi","subdomain":"testapi.domain.com"}'

# Check specific client
curl http://localhost:5000/api/clients/testapi

# Update version
curl -X PUT http://localhost:5000/api/clients/testapi/version \
  -H "Content-Type: application/json" \
  -d '{"version":"latest"}'

🔑 Key Points:

  • Test each endpoint
  • Should return JSON responses
  • Check for “success”: true
  • Errors show helpful messages

Step 5.5: Create Dashboard HTML

# Create dashboard file
cat > ~/saas-api/dashboard.html <<'EOF'
<!DOCTYPE html>
<html>
<head>
    <title>K3s SaaS Dashboard</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            background: #f5f5f5;
        }
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background: white;
            padding: 20px;
            border-radius: 10px;
        }
        h1 {
            color: #333;
            border-bottom: 2px solid #4CAF50;
            padding-bottom: 10px;
        }
        .client-card {
            border: 1px solid #ddd;
            padding: 15px;
            margin: 10px 0;
            border-radius: 5px;
            background: #fafafa;
        }
        .status-running {
            color: green;
            font-weight: bold;
        }
        .status-pending {
            color: orange;
            font-weight: bold;
        }
        .status-failed {
            color: red;
            font-weight: bold;
        }
        button {
            background: #4CAF50;
            color: white;
            padding: 8px 15px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin: 2px;
        }
        button:hover {
            background: #45a049;
        }
        button.danger {
            background: #f44336;
        }
        button.danger:hover {
            background: #da190b;
        }
        input, select {
            padding: 8px;
            margin: 5px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        .form-section {
            background: #f0f0f0;
            padding: 15px;
            margin: 20px 0;
            border-radius: 5px;
        }
        table {
            width: 100%;
            border-collapse: collapse;
            margin: 20px 0;
        }
        th, td {
            padding: 10px;
            text-align: left;
            border-bottom: 1px solid #ddd;
        }
        th {
            background: #4CAF50;
            color: white;
        }
        .message {
            padding: 10px;
            margin: 10px 0;
            border-radius: 5px;
        }
        .success {
            background: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }
        .error {
            background: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🚀 K3s SaaS Management Dashboard</h1>

        <div id="message"></div>

        <!-- Create New Client Section -->
        <div class="form-section">
            <h2>➕ Create New Client</h2>
            <input type="text" id="newClientName" placeholder="Client name (e.g., pethouse)">
            <input type="text" id="newClientDomain" placeholder="Subdomain (e.g., pethouse.domain.com)">
            <button onclick="createClient()">Create Client</button>
        </div>

        <!-- Client List -->
        <h2>📋 Current Clients</h2>
        <button onclick="loadClients()">🔄 Refresh</button>
        <div id="clientsList"></div>

        <!-- Version Management -->
        <div class="form-section">
            <h2>📦 Version Management</h2>
            <select id="versionSelect">
                <option value="latest">Latest</option>
                <option value="lts-latest">LTS Latest</option>
                <option value="beta-latest">Beta Latest</option>
            </select>
            <button onclick="loadVersions()">Load Versions</button>
            <div id="versionsList"></div>
        </div>
    </div>

    <script>
        const API_URL = 'http://' + window.location.hostname + ':5000';

        // Show message
        function showMessage(text, type = 'success') {
            const messageDiv = document.getElementById('message');
            messageDiv.className = 'message ' + type;
            messageDiv.textContent = text;
            setTimeout(() => {
                messageDiv.textContent = '';
                messageDiv.className = '';
            }, 5000);
        }

        // Load clients list
        async function loadClients() {
            try {
                const response = await fetch(API_URL + '/api/clients');
                const data = await response.json();

                const listDiv = document.getElementById('clientsList');

                if (data.success && data.clients.length > 0) {
                    let html = '<table><tr><th>Client Name</th><th>Namespace</th><th>Created</th><th>Actions</th></tr>';

                    for (const client of data.clients) {
                        html += '<tr>';
                        html += '<td>' + client.name + '</td>';
                        html += '<td>' + client.namespace + '</td>';
                        html += '<td>' + new Date(client.created).toLocaleDateString() + '</td>';
                        html += '<td>';
                        html += '<button onclick="getClientDetails(\'' + client.name + '\')">📊 Details</button>';
                        html += '<button onclick="updateClient(\'' + client.name + '\')">🔄 Update</button>';
                        html += '<button onclick="restartClient(\'' + client.name + '\')">🔃 Restart</button>';
                        html += '<button class="danger" onclick="deleteClient(\'' + client.name + '\')">🗑️ Delete</button>';
                        html += '</td>';
                        html += '</tr>';
                    }
                    html += '</table>';
                    listDiv.innerHTML = html;
                } else {
                    listDiv.innerHTML = '<p>No clients found. Create your first client above!</p>';
                }
            } catch (error) {
                showMessage('Error loading clients: ' + error, 'error');
            }
        }

        // Create new client
        async function createClient() {
            const name = document.getElementById('newClientName').value;
            const subdomain = document.getElementById('newClientDomain').value;

            if (!name || !subdomain) {
                showMessage('Please enter client name and subdomain', 'error');
                return;
            }

            try {
                const response = await fetch(API_URL + '/api/clients', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({name: name, subdomain: subdomain})
                });

                const data = await response.json();

                if (data.success) {
                    showMessage('Client ' + name + ' created successfully!', 'success');
                    document.getElementById('newClientName').value = '';
                    document.getElementById('newClientDomain').value = '';
                    loadClients();
                } else {
                    showMessage('Error: ' + data.error, 'error');
                }
            } catch (error) {
                showMessage('Error creating client: ' + error, 'error');
            }
        }

        // Get client details
        async function getClientDetails(name) {
            try {
                const response = await fetch(API_URL + '/api/clients/' + name);
                const data = await response.json();

                if (data.success) {
                    let status = 'Pods for ' + name + ':\n';
                    data.pods.forEach(pod => {
                        status += '- ' + pod.name + ' (' + pod.status + ')\n';
                    });
                    alert(status);
                } else {
                    showMessage('Error: ' + data.error, 'error');
                }
            } catch (error) {
                showMessage('Error getting details: ' + error, 'error');
            }
        }

        // Update client version
        async function updateClient(name) {
            const version = prompt('Enter version (latest, lts-xxx, beta-xxx):');
            if (!version) return;

            try {
                const response = await fetch(API_URL + '/api/clients/' + name + '/version', {
                    method: 'PUT',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({version: version})
                });

                const data = await response.json();

                if (data.success) {
                    showMessage('Client ' + name + ' updated to ' + version, 'success');
                } else {
                    showMessage('Error: ' + data.error, 'error');
                }
            } catch (error) {
                showMessage('Error updating client: ' + error, 'error');
            }
        }

        // Restart client
        async function restartClient(name) {
            if (!confirm('Restart client ' + name + '?')) return;

            try {
                const response = await fetch(API_URL + '/api/clients/' + name + '/restart', {
                    method: 'POST'
                });

                const data = await response.json();

                if (data.success) {
                    showMessage('Client ' + name + ' restarted', 'success');
                } else {
                    showMessage('Error: ' + data.error, 'error');
                }
            } catch (error) {
                showMessage('Error restarting client: ' + error, 'error');
            }
        }

        // Delete client
        async function deleteClient(name) {
            if (!confirm('Delete client ' + name + '? This cannot be undone!')) return;

            try {
                const response = await fetch(API_URL + '/api/clients/' + name, {
                    method: 'DELETE'
                });

                const data = await response.json();

                if (data.success) {
                    showMessage('Client ' + name + ' deleted', 'success');
                    loadClients();
                } else {
                    showMessage('Error: ' + data.error, 'error');
                }
            } catch (error) {
                showMessage('Error deleting client: ' + error, 'error');
            }
        }

        // Load available versions
        async function loadVersions() {
            try {
                const response = await fetch(API_URL + '/api/versions');
                const data = await response.json();

                if (data.success) {
                    const listDiv = document.getElementById('versionsList');
                    let html = '<h3>Available Versions:</h3><ul>';
                    data.versions.forEach(v => {
                        html += '<li>' + v.tag + ' (' + v.type + ')</li>';
                    });
                    html += '</ul>';
                    listDiv.innerHTML = html;
                }
            } catch (error) {
                showMessage('Error loading versions: ' + error, 'error');
            }
        }

        // Load clients on page load
        window.onload = function() {
            loadClients();
            loadVersions();
        };
    </script>
</body>
</html>
EOF

# Serve dashboard
cd ~/saas-api
python3 -m http.server 8080 &

🔑 Key Points:

  • Simple HTML dashboard
  • Uses JavaScript to call API
  • Shows all clients
  • Create, update, delete clients
  • Access at http://YOUR_SERVER_IP:8080/dashboard.html

🎉 Day 5 Complete!

What you achieved:

  • ✅ Created REST API
  • ✅ API controls all K3s operations
  • ✅ Created web dashboard
  • ✅ Can manage clients from browser

Part 8: Week 2 - Add All Features

Quick Setup for Remaining Features

Add SSL Certificates (30 minutes)

# Install cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml

# Wait for cert-manager
kubectl wait --for=condition=Ready pods --all -n cert-manager --timeout=300s

# Create Let's Encrypt issuer
cat > letsencrypt-issuer.yaml <<EOF
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: [email protected]  # Change this!
    privateKeySecretRef:
      name: letsencrypt-key
    solvers:
    - http01:
        ingress:
          class: traefik
EOF

kubectl apply -f letsencrypt-issuer.yaml

🔑 Key Points:

  • Free SSL certificates
  • Automatic renewal
  • Works with your ingress

Add Monitoring (1 hour)

# Install Prometheus and Grafana
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

helm install monitoring prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --create-namespace \
  --set grafana.adminPassword=admin123  # Change password!

# Access Grafana
kubectl port-forward -n monitoring svc/monitoring-grafana 3000:80

# Login: admin / admin123
# URL: http://localhost:3000

🔑 Key Points:

  • Prometheus collects metrics
  • Grafana shows dashboards
  • Pre-built K8s dashboards

Add Backup System (30 minutes)

# Create backup script
cat > backup-clients.sh <<'EOF'
#!/bin/bash

# Backup all client configs
BACKUP_DIR="/backups/k3s/$(date +%Y%m%d)"
mkdir -p $BACKUP_DIR

# Backup each client namespace
for ns in $(kubectl get ns -o name | grep client- | cut -d/ -f2); do
    echo "Backing up $ns..."
    kubectl get all -n $ns -o yaml > $BACKUP_DIR/$ns.yaml
done

echo "Backup complete: $BACKUP_DIR"
EOF

chmod +x backup-clients.sh

# Add to crontab (daily at 2 AM)
(crontab -l 2>/dev/null; echo "0 2 * * * /home/ubuntu/backup-clients.sh") | crontab -

🔑 Key Points:

  • Daily automatic backups
  • Saves all client configurations
  • Easy restore if needed

Part 9: Common Problems & Solutions

Problem Reference Guide

Problem Solution
Pod stuck in “Pending” Check resources: kubectl describe pod POD_NAME -n NAMESPACE
Pod keeps restarting Check logs: kubectl logs POD_NAME -n NAMESPACE
Can’t access website Check ingress: kubectl get ingress -A
Out of disk space Clean images: docker system prune -a
MongoDB connection fails Check secret: kubectl get secret -n NAMESPACE
SSL not working Check cert: kubectl describe certificate -n NAMESPACE
High CPU usage Check metrics: kubectl top nodes and kubectl top pods -A
Deployment failed Rollback: helm rollback CLIENT_NAME -n NAMESPACE

Debug Commands

# Check pod issues
kubectl describe pod POD_NAME -n NAMESPACE

# View pod logs
kubectl logs POD_NAME -n NAMESPACE

# Get into pod shell
kubectl exec -it POD_NAME -n NAMESPACE -- /bin/sh

# Check events
kubectl get events -n NAMESPACE --sort-by='.lastTimestamp'

# Check resource usage
kubectl top nodes
kubectl top pods -A

# Force delete stuck pod
kubectl delete pod POD_NAME -n NAMESPACE --force --grace-period=0

Part 10: Complete Scripts Ready to Use

Master Installation Script

#!/bin/bash
# Save as: install-everything.sh

echo "================================"
echo "K3s Complete Installation Script"
echo "================================"

# Step 1: Install K3s
echo "Installing K3s..."
curl -sfL https://get.k3s.io | sh -
sleep 30

# Step 2: Configure kubectl
echo "Configuring kubectl..."
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(whoami) ~/.kube/config

# Step 3: Install Helm
echo "Installing Helm..."
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# Step 4: Install Docker
echo "Installing Docker..."
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER

# Step 5: Create registry
echo "Creating Docker registry..."
kubectl create namespace docker-registry
kubectl create deployment docker-registry --image=registry:2 --namespace=docker-registry
kubectl expose deployment docker-registry --type=NodePort --port=5000 --namespace=docker-registry

# Step 6: Install ArgoCD
echo "Installing ArgoCD..."
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# Step 7: Install cert-manager
echo "Installing cert-manager..."
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml

# Step 8: Install monitoring
echo "Installing Prometheus and Grafana..."
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install monitoring prometheus-community/kube-prometheus-stack --namespace monitoring --create-namespace

echo "================================"
echo "Installation Complete!"
echo "================================"
echo "Next steps:"
echo "1. Logout and login again (for Docker)"
echo "2. Get ArgoCD password: kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath=\"{.data.password}\" | base64 -d"
echo "3. Access Grafana: kubectl port-forward -n monitoring svc/monitoring-grafana 3000:80"

Client Management Script

#!/bin/bash
# Save as: client-manager.sh

case "$1" in
    create)
        CLIENT=$2
        DOMAIN=$3
        kubectl create namespace client-$CLIENT
        helm install $CLIENT ./saas-app --namespace client-$CLIENT \
            --set client.name=$CLIENT \
            --set client.subdomain=$DOMAIN
        echo "Client $CLIENT created"
        ;;

    delete)
        CLIENT=$2
        kubectl delete namespace client-$CLIENT
        echo "Client $CLIENT deleted"
        ;;

    list)
        echo "Current clients:"
        kubectl get namespaces | grep client-
        ;;

    update)
        CLIENT=$2
        VERSION=$3
        helm upgrade $CLIENT ./saas-app --namespace client-$CLIENT \
            --set image.tag=$VERSION
        echo "Client $CLIENT updated to $VERSION"
        ;;

    status)
        CLIENT=$2
        kubectl get all -n client-$CLIENT
        ;;

    *)
        echo "Usage: $0 {create|delete|list|update|status} [client] [options]"
        ;;
esac

🚀 Start Tomorrow Checklist

Day 1 (4 hours)

  • [ ] Morning: Prepare Ubuntu server
  • [ ] Install K3s (30 minutes)
  • [ ] Install kubectl (15 minutes)
  • [ ] Test everything works (15 minutes)
  • [ ] Break
  • [ ] Install Helm (15 minutes)
  • [ ] Install Docker (30 minutes)
  • [ ] Setup registry (30 minutes)
  • [ ] Install ArgoCD (45 minutes)

Day 2 (4 hours)

  • [ ] Create Helm chart (1 hour)
  • [ ] Deploy test client (30 minutes)
  • [ ] Create scripts (1 hour)
  • [ ] Test scripts (30 minutes)
  • [ ] Setup GitHub repo (1 hour)

Day 3 (4 hours)

  • [ ] Create API (2 hours)
  • [ ] Create dashboard (1 hour)
  • [ ] Test everything (1 hour)

Day 4-5 (8 hours)

  • [ ] Migrate first 3 clients
  • [ ] Fix any issues
  • [ ] Document problems
  • [ ] Train team

Week 2

  • [ ] Add monitoring
  • [ ] Add backups
  • [ ] Migrate 10 more clients
  • [ ] Optimize performance

📞 Need Help?

Resources

  1. K3s Documentation: https://docs.k3s.io
  2. Kubernetes Basics: https://kubernetes.io/docs/tutorials/
  3. Helm Guide: https://helm.sh/docs/intro/quickstart/
  4. Docker Basics: https://docs.docker.com/get-started/

Common Questions

Q: How much will this cost monthly? A: About $150/month for 3 servers that can handle 500+ clients

Q: How long to set everything up? A: 3-5 days for basic setup, 2 weeks for full migration

Q: What if something breaks? A: K3s self-heals. If pod crashes, it restarts automatically

Q: Can I rollback if update fails? A: Yes, one command: helm rollback CLIENT_NAME

Q: How to add more servers? A: Just run K3s worker install on new server


🎯 Final Summary

What You Get

Automatic Everything

  • Client creation: 3 hours → 5 minutes
  • Deployment: Manual → Automatic
  • Scaling: Manual → Automatic
  • Healing: Manual → Automatic

Cost Savings

  • Current: $1,500/month labor
  • New: $150/month servers
  • Savings: $1,350/month

Features

  • Every commit builds automatically
  • Deploy any version to any client
  • Auto-scaling (1-10 pods)
  • Self-healing
  • Zero-downtime updates
  • One-click rollback
  • Dashboard control

Start Tomorrow Morning!

  1. Get your Ubuntu server ready
  2. Copy the installation script
  3. Run it step by step
  4. Follow this guide exactly
  5. Ask for help when stuck

You can do this! The guide has everything you need. Good luck! 🚀