This repository has been archived on 2026-06-19. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
homelab-mastery/build-guide/without-ai/README.md

15 KiB

Build KiteStacks Manually

This is the step-by-step, command-by-command build guide. Every file, every command, every setting. No AI. Build this from scratch on a blank Ubuntu 24.04 machine.

Convention: Replace <REDACTED> with your actual values. Replace kitestacks.com with your own domain.


Step 1 — Docker Engine

# Remove any old Docker installs
sudo apt remove docker docker-engine docker.io containerd runc

# Install dependencies
sudo apt update
sudo apt install -y ca-certificates curl gnupg lsb-release

# Add Docker's official GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# Add Docker repository
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

# Install Docker Engine + Compose v2
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# Add your user to docker group (re-login after this)
sudo usermod -aG docker $USER

# Create the shared network for all homelab containers
docker network create kitestacks

# Verify
docker run --rm hello-world
docker network ls | grep kitestacks

Step 2 — Tailscale

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
# Follow the auth link printed in the terminal
tailscale status

Do the same on the cloud VPS when you get to Step 10.


Step 3 — Project Structure

mkdir -p ~/kitestacks-live/docker/{authentik,bookstack,cloudflared,forgejo,grafana,karakeep,kavita,kitestacks-portal,osticket,portainer,prometheus}

Step 4 — Cloudflare Tunnel

mkdir -p ~/kitestacks-live/docker/cloudflared

Create ~/kitestacks-live/docker/cloudflared/.env:

TUNNEL_TOKEN=<REDACTED>

Create ~/kitestacks-live/docker/cloudflared/docker-compose.yml:

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    restart: unless-stopped
    command: tunnel --no-autoupdate run
    environment:
      - TUNNEL_TOKEN=${TUNNEL_TOKEN}
    networks:
      - kitestacks

networks:
  kitestacks:
    external: true
cd ~/kitestacks-live/docker/cloudflared
docker compose up -d
docker logs cloudflared --tail 20

In Cloudflare Zero Trust → Networks → Tunnels → your tunnel → Public Hostnames, add:

  • www.kitestacks.comhttp://kitestacks-portal:80
  • auth.kitestacks.comhttp://authentik:9000 (Add more subdomains as you deploy each service)

Step 5 — Authentik (SSO)

Authentik needs PostgreSQL and Redis. In this setup, both live on kscloud1 (Step 10). For a single-machine setup, run them locally.

Create ~/kitestacks-live/docker/authentik/docker-compose.yml:

services:
  postgresql:
    image: docker.io/library/postgres:16-alpine
    restart: unless-stopped
    container_name: authentik-postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -d ${POSTGRES_DB} -U ${POSTGRES_USER}"]
      start_period: 20s
      interval: 30s
      retries: 5
      timeout: 5s
    volumes:
      - database:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: ${PG_PASS}
      POSTGRES_USER: ${PG_USER}
      POSTGRES_DB: ${PG_DB}
    networks:
      - kitestacks

  redis:
    image: docker.io/library/redis:alpine
    restart: unless-stopped
    container_name: authentik-redis
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
      start_period: 20s
      interval: 30s
      retries: 5
      timeout: 3s
    networks:
      - kitestacks

  server:
    image: ghcr.io/goauthentik/server:2024.12.3
    restart: unless-stopped
    container_name: authentik
    command: server
    environment:
      AUTHENTIK_REDIS__HOST: redis
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__USER: ${PG_USER}
      AUTHENTIK_POSTGRESQL__NAME: ${PG_DB}
      AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
      AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
    volumes:
      - ./media:/media
      - ./custom-templates:/templates
    depends_on:
      - postgresql
      - redis
    networks:
      - kitestacks

  worker:
    image: ghcr.io/goauthentik/server:2024.12.3
    restart: unless-stopped
    container_name: authentik-worker
    command: worker
    environment:
      AUTHENTIK_REDIS__HOST: redis
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__USER: ${PG_USER}
      AUTHENTIK_POSTGRESQL__NAME: ${PG_DB}
      AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
      AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./media:/media
      - ./custom-templates:/templates
    depends_on:
      - postgresql
      - redis
    networks:
      - kitestacks

volumes:
  database:

networks:
  kitestacks:
    external: true

Create ~/kitestacks-live/docker/authentik/.env:

PG_PASS=<REDACTED>
PG_USER=authentik
PG_DB=authentik
AUTHENTIK_SECRET_KEY=<REDACTED>

Generate the secret key:

openssl rand -hex 32
cd ~/kitestacks-live/docker/authentik
docker compose up -d

Browse to https://auth.kitestacks.com/if/flow/initial-setup/ to complete setup.


Step 6 — Portainer

Create ~/kitestacks-live/docker/portainer/docker-compose.yml:

services:
  portainer:
    image: portainer/portainer-ce:latest
    container_name: portainer
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - portainer_data:/data
    networks:
      - kitestacks

volumes:
  portainer_data:

networks:
  kitestacks:
    external: true
cd ~/kitestacks-live/docker/portainer
docker compose up -d

Add Cloudflare Tunnel route: portainer.kitestacks.comhttps://portainer:9443

In Portainer UI → Settings → Authentication → OAuth:

  • Client ID: portainer (from Authentik provider)
  • Auth URL: https://auth.kitestacks.com/application/o/authorize/
  • Token URL: https://auth.kitestacks.com/application/o/token/
  • Resource URL: https://auth.kitestacks.com/application/o/userinfo/
  • Redirect URL: https://portainer.kitestacks.com

Pre-create your Authentik user as admin before their first login:

TOKEN=$(curl -sk -X POST https://portainer.kitestacks.com/api/auth \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"<REDACTED>"}' | python3 -c \
  "import sys,json; print(json.load(sys.stdin)['jwt'])")

curl -sk -X POST "https://portainer.kitestacks.com/api/users" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"username":"user@example.com","role":1}'

Step 7 — Forgejo

Create ~/kitestacks-live/docker/forgejo/docker-compose.yml:

services:
  forgejo:
    image: codeberg.org/forgejo/forgejo:11
    container_name: forgejo
    restart: unless-stopped
    volumes:
      - ./data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "2222:22"
      - "3006:3000"
    networks:
      - kitestacks

networks:
  kitestacks:
    external: true
cd ~/kitestacks-live/docker/forgejo
docker compose up -d

Browse to http://localhost:3006 to complete initial setup.

Add CF Tunnel route: gitforge.kitestacks.comhttp://forgejo:3000

Set up SSH config (~/.ssh/config):

Host gitforge.kitestacks.com
  HostName gitforge.kitestacks.com
  Port 2222
  User git
  IdentityFile ~/.ssh/id_ed25519

Generate API token:

docker exec -u git forgejo forgejo admin user generate-access-token \
  --username <your-username> --token-name "cli-token" --raw \
  --scopes "read:user,write:user,read:repository,write:repository"

Step 8 — BookStack

Create ~/kitestacks-live/docker/bookstack/docker-compose.yml:

services:
  bookstack:
    image: lscr.io/linuxserver/bookstack:latest
    container_name: bookstack
    restart: unless-stopped
    environment:
      - PUID=1000
      - PGID=1000
      - APP_URL=https://wiki.kitestacks.com
      - DB_HOST=bookstack-db
      - DB_PORT=3306
      - DB_USER=bookstack
      - DB_PASS=<REDACTED>
      - DB_DATABASE=bookstackapp
      - AUTH_METHOD=oidc
      - OIDC_ISSUER=https://auth.kitestacks.com/application/o/bookstack/
      - OIDC_ISSUER_DISCOVER=true
      - OIDC_CLIENT_ID=bookstack
      - OIDC_CLIENT_SECRET=<REDACTED>
      - OIDC_USER_ATTRIBUTE=email
      - APP_KEY=<REDACTED>
    volumes:
      - ./config:/config
    ports:
      - "6875:80"
    depends_on:
      - bookstack-db
    networks:
      - kitestacks

  bookstack-db:
    image: lscr.io/linuxserver/mariadb:latest
    container_name: bookstack-db
    restart: unless-stopped
    environment:
      - PUID=1000
      - PGID=1000
      - MYSQL_ROOT_PASSWORD=<REDACTED>
      - MYSQL_DATABASE=bookstackapp
      - MYSQL_USER=bookstack
      - MYSQL_PASSWORD=<REDACTED>
    volumes:
      - ./db:/config
    networks:
      - kitestacks

networks:
  kitestacks:
    external: true

Generate APP_KEY:

docker run --rm --entrypoint /bin/bash lscr.io/linuxserver/bookstack:latest appkey

In Authentik, create an OAuth2 Provider for BookStack:

  • Name: bookstack
  • Redirect URIs: https://wiki.kitestacks.com/oidc/callback
  • issuer_mode: per_provider
cd ~/kitestacks-live/docker/bookstack
docker compose up -d

# Fix cache permissions (prevents "unknown error" on OIDC login)
docker exec bookstack chown -R abc:users /config/www/framework/cache/

Add CF Tunnel route: wiki.kitestacks.comhttp://bookstack:80


Step 9 — Monitoring

Prometheus + Node Exporter

Create ~/kitestacks-live/docker/prometheus/docker-compose.yml:

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    restart: unless-stopped
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    networks:
      - kitestacks

  node-exporter:
    image: prom/node-exporter:latest
    container_name: node-exporter
    restart: unless-stopped
    network_mode: host
    pid: host
    volumes:
      - /:/host:ro,rslave
    command:
      - '--path.rootfs=/host'

volumes:
  prometheus_data:

networks:
  kitestacks:
    external: true

Create ~/kitestacks-live/docker/prometheus/prometheus.yml:

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'node-monk'
    static_configs:
      - targets: ['localhost:9100']

  - job_name: 'node-kscloud1'
    static_configs:
      - targets: ['<KSCLOUD1_TAILSCALE_IP>:9100']

Grafana

Create ~/kitestacks-live/docker/grafana/docker-compose.yml:

services:
  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: unless-stopped
    environment:
      - GF_SERVER_ROOT_URL=https://grafana.kitestacks.com
      - GF_AUTH_GENERIC_OAUTH_ENABLED=true
      - GF_AUTH_GENERIC_OAUTH_NAME=Authentik
      - GF_AUTH_GENERIC_OAUTH_CLIENT_ID=grafana
      - GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET=<REDACTED>
      - GF_AUTH_GENERIC_OAUTH_SCOPES=openid email profile
      - GF_AUTH_GENERIC_OAUTH_AUTH_URL=https://auth.kitestacks.com/application/o/authorize/
      - GF_AUTH_GENERIC_OAUTH_TOKEN_URL=https://auth.kitestacks.com/application/o/token/
      - GF_AUTH_GENERIC_OAUTH_API_URL=https://auth.kitestacks.com/application/o/userinfo/
    volumes:
      - grafana_data:/var/lib/grafana
    networks:
      - kitestacks

volumes:
  grafana_data:

networks:
  kitestacks:
    external: true
cd ~/kitestacks-live/docker/grafana
docker compose up -d

After first login: Configuration → Data Sources → Add Prometheus → URL http://prometheus:9090


Step 10 — Cloud Replica (kscloud1)

On the VPS

# Install Docker (same commands as Step 1)
# Install Tailscale (same as Step 2)
# Create project directories
mkdir -p /opt/kitestacks/docker/{authentik,bookstack,cloudflared,grafana,portainer}

# Copy docker-compose files from monk via SCP or git
# Example for cloudflared:
scp ~/kitestacks-live/docker/cloudflared/docker-compose.yml root@<KSCLOUD1>:/opt/kitestacks/docker/cloudflared/

Move Authentik DB to kscloud1

# On kscloud1: bring up just postgres and redis from authentik compose
cd /opt/kitestacks/docker/authentik
docker compose up -d postgresql redis

# On monk: update Authentik env to point to kscloud1 Tailscale IP
# AUTHENTIK_REDIS__HOST: <KSCLOUD1_TAILSCALE_IP>
# AUTHENTIK_POSTGRESQL__HOST: <KSCLOUD1_TAILSCALE_IP>
docker compose up -d

Add second cloudflared connector on kscloud1

# Same TUNNEL_TOKEN as monk
cd /opt/kitestacks/docker/cloudflared
docker compose up -d

# Verify 2 connectors in Cloudflare Zero Trust
docker exec cloudflared cloudflared tunnel info <TUNNEL_ID>

Test failover

# Stop all services on monk
docker stop $(docker ps -q)

# Browse to https://wiki.kitestacks.com from a browser — kscloud1 should serve it
# Check Cloudflare analytics for which connector is active

Step 11 — OSTicket (Help Desk)

Create ~/kitestacks-live/docker/osticket/docker-compose.yml:

services:
  osticket-db:
    image: mariadb:11
    container_name: osticket-db
    restart: unless-stopped
    environment:
      - MYSQL_ROOT_PASSWORD=<REDACTED>
      - MYSQL_DATABASE=osticket
      - MYSQL_USER=osticket
      - MYSQL_PASSWORD=<REDACTED>
    volumes:
      - ./db:/var/lib/mysql
    networks:
      - kitestacks

  osticket:
    image: ghcr.io/tiredofit/docker-osticket:latest
    container_name: osticket-app
    restart: unless-stopped
    environment:
      - DB_HOST=osticket-db
      - DB_NAME=osticket
      - DB_USER=osticket
      - DB_PASS=<REDACTED>
      - SMTP_HOST=smtp.gmail.com
      - SMTP_PORT=587
      - SMTP_FROM=kitestacks.helpdesk@gmail.com
      - SMTP_USER=kitestacks.helpdesk@gmail.com
      - SMTP_PASS=<REDACTED>
      - SMTP_TLS=true
    depends_on:
      - osticket-db
    networks:
      - kitestacks

networks:
  kitestacks:
    external: true

Reference: Useful Commands

# Check all running containers
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"

# Follow logs for a service
docker logs <container> --tail 50 -f

# Restart a service
cd ~/kitestacks-live/docker/<service> && docker compose restart

# Pull latest image and redeploy
docker compose pull && docker compose up -d

# BookStack: clear config cache
docker exec bookstack php /app/www/artisan config:clear
docker exec bookstack php /app/www/artisan cache:clear

# Portainer: reset admin password
docker stop portainer
docker run --rm -v portainer_data:/data portainer/helper-reset-password
docker start portainer

# Check Tailscale connectivity
tailscale status

# SSH to kscloud1
ssh -i ~/.ssh/id_ed25519_kscloud1 root@<KSCLOUD1_TAILSCALE_IP>