refactor: Enhance local development environment and documentation
- .gitignore: Ignore result* files. - Makefile: Refactored to improve clarity, remove unused targets, add new testing targets (test-usb, test-vm), and streamline cloud deployment commands. - README.md: Significantly updated to reflect the new single-participant environment focus, refine the learning flow, update commands, and improve overall readability. - flake.nix: Modified to enable Docker for local development, adjust desktop environment behavior (manual start), and implement improved systemd services for container setup and health monitoring, including retry logic.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,4 @@
|
|||||||
.env
|
.env
|
||||||
result/
|
result*
|
||||||
.direnv/
|
.direnv/
|
||||||
*.qcow2
|
*.qcow2
|
||||||
|
|||||||
85
Makefile
85
Makefile
@@ -1,38 +1,35 @@
|
|||||||
-include .env
|
-include .env
|
||||||
export
|
export
|
||||||
|
|
||||||
.PHONY: help deploy-cloud build-usb flash-usb local-vm-run clean status destroy-cloud opencode lint
|
.PHONY: help deploy-cloud build-usb flash-usb local-vm test-vm clean status destroy-cloud opencode lint
|
||||||
|
|
||||||
DOMAIN := $(or $(WORKSHOP_DOMAIN),codecrispi.es)
|
DOMAIN := $(or $(WORKSHOP_DOMAIN),codecrispi.es)
|
||||||
PARTICIPANTS := $(or $(PARTICIPANTS),3)
|
|
||||||
USB_DEVICE := $(or $(USB_DEVICE),/dev/sdX)
|
USB_DEVICE := $(or $(USB_DEVICE),/dev/sdX)
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "CODE CRISPIES Workshop Infrastructure"
|
@echo "CODE CRISPIES Workshop Infrastructure"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "🌍 Cloud Infrastructure (Hetzner):"
|
@echo "🌐 Cloud Infrastructure (Hetzner):"
|
||||||
@echo " make deploy-cloud - Deploy 15 VMs to Hetzner Cloud"
|
@echo " make deploy-cloud - Deploy 15 VMs to Hetzner Cloud"
|
||||||
@echo " make status-cloud - Check server health"
|
@echo " make status-cloud - Check server health"
|
||||||
@echo " make destroy-cloud - Destroy cloud infrastructure"
|
@echo " make destroy-cloud - Destroy cloud infrastructure"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "💾 USB Boot Drive:"
|
@echo "💾 USB Boot Drive (Single Participant Environment):"
|
||||||
@echo " make build-usb - Build NixOS workshop ISO"
|
@echo " make build-usb - Build NixOS workshop ISO"
|
||||||
@echo " make flash-usb - Flash ISO to USB drive"
|
@echo " make flash-usb - Flash ISO to USB drive"
|
||||||
|
@echo " make test-usb - Test USB environment in QEMU"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "🖥️ Local Development:"
|
@echo "🖥️ Local Development:"
|
||||||
@echo " make local-vm-run - Start local VM with containers"
|
@echo " make local-vm - Start single participant VM"
|
||||||
@echo " make local-vm-test - Test with 2 containers only"
|
@echo " make test-vm - Test VM without GUI"
|
||||||
@echo " make local-vm-full - Test with all 15 containers"
|
|
||||||
@echo " make clean - Clean build artifacts"
|
@echo " make clean - Clean build artifacts"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "⚙️ Development:"
|
@echo "⚙️ Development:"
|
||||||
@echo " make opencode - Start opencode in dev shell"
|
@echo " make opencode - Start opencode in dev shell"
|
||||||
@echo " make lint - Run linting checks"
|
@echo " make lint - Run linting checks"
|
||||||
@echo " make check-vm - Verify VM builds correctly"
|
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Current Config:"
|
@echo "Current Config:"
|
||||||
@echo " Domain: $(DOMAIN)"
|
@echo " Domain: $(DOMAIN)"
|
||||||
@echo " Participants: $(PARTICIPANTS)"
|
|
||||||
@echo " USB Device: $(USB_DEVICE)"
|
@echo " USB Device: $(USB_DEVICE)"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Required: HCLOUD_TOKEN, SSH key at ~/.ssh/id_ed25519.pub"
|
@echo "Required: HCLOUD_TOKEN, SSH key at ~/.ssh/id_ed25519.pub"
|
||||||
@@ -58,41 +55,44 @@ flash-usb: build-usb
|
|||||||
@read -p "Continue? [y/N]: " confirm && [ "$$confirm" = "y" ]
|
@read -p "Continue? [y/N]: " confirm && [ "$$confirm" = "y" ]
|
||||||
sudo dd if=result/iso/nixos.iso of=$(USB_DEVICE) bs=4M status=progress oflag=sync
|
sudo dd if=result/iso/nixos.iso of=$(USB_DEVICE) bs=4M status=progress oflag=sync
|
||||||
sync
|
sync
|
||||||
@echo "✅ USB drive ready for workshop!"
|
@echo "✅ USB drive ready!"
|
||||||
|
|
||||||
|
test-usb: build-usb
|
||||||
|
@echo "🧪 Testing USB environment in QEMU..."
|
||||||
|
qemu-system-x86_64 \
|
||||||
|
-cdrom result/iso/nixos.iso \
|
||||||
|
-m 2048 \
|
||||||
|
-enable-kvm \
|
||||||
|
-netdev user,id=net0 \
|
||||||
|
-device virtio-net,netdev=net0 \
|
||||||
|
-display gtk
|
||||||
|
|
||||||
|
local-vm:
|
||||||
|
@echo "🖥️ Starting workshop VM..."
|
||||||
|
nix run .#local-vm
|
||||||
|
|
||||||
|
test-vm:
|
||||||
|
@echo "🧪 Testing VM build..."
|
||||||
|
nix build .#local-vm
|
||||||
|
@echo "✅ VM builds successfully"
|
||||||
|
|
||||||
deploy-cloud:
|
deploy-cloud:
|
||||||
@if [ -z "$(HCLOUD_TOKEN)" ]; then \
|
@if [ -z "$(HCLOUD_TOKEN)" ]; then \
|
||||||
echo "❌ HCLOUD_TOKEN not set"; \
|
echo "❌ HCLOUD_TOKEN not set"; \
|
||||||
echo "Get token from: https://console.hetzner.cloud/"; \
|
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
@if [ ! -f ~/.ssh/id_ed25519.pub ]; then \
|
@echo "🚀 Deploying 15 workshop servers..."
|
||||||
echo "❌ SSH key not found at ~/.ssh/id_ed25519.pub"; \
|
|
||||||
echo "Generate with: ssh-keygen -t ed25519"; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
@echo "🚀 Deploying 15 workshop servers to Hetzner Cloud..."
|
|
||||||
@echo "Domain: $(DOMAIN)"
|
|
||||||
cd terraform && terraform init
|
cd terraform && terraform init
|
||||||
cd terraform && terraform apply -auto-approve \
|
cd terraform && terraform apply -auto-approve \
|
||||||
-var="hcloud_token=$(HCLOUD_TOKEN)" \
|
-var="hcloud_token=$(HCLOUD_TOKEN)" \
|
||||||
-var="hetzner_dns_token=$(HETZNER_DNS_TOKEN)" \
|
-var="domain=$(DOMAIN)"
|
||||||
-var="dns_zone_id=$(DNS_ZONE_ID)" \
|
|
||||||
-var="domain=$(DOMAIN)" \
|
|
||||||
-var="ssh_public_key=$$(cat ~/.ssh/id_ed25519.pub)"
|
|
||||||
@echo "⏳ Running health checks..."
|
|
||||||
@sleep 60
|
|
||||||
$(MAKE) status-cloud
|
|
||||||
@echo "✅ Cloud deployment complete!"
|
|
||||||
|
|
||||||
status-cloud:
|
status-cloud:
|
||||||
@echo "🔍 Checking server health..."
|
@echo "📊 Checking server health..."
|
||||||
@for name in hopper curie lovelace noether hamilton franklin johnson clarke goldberg liskov wing rosen shaw karp rich; do \
|
@for name in hopper curie lovelace noether hamilton franklin johnson clarke goldberg liskov wing rosen shaw karp rich; do \
|
||||||
printf "%-10s " "$$name:"; \
|
printf "%-10s " "$$name:"; \
|
||||||
if timeout 10 curl -s -f https://traefik.$$name.$(DOMAIN)/ping >/dev/null 2>&1; then \
|
if timeout 5 ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no workshop@$$name.$(DOMAIN) "echo ok" >/dev/null 2>&1; then \
|
||||||
echo "✅ Ready"; \
|
echo "✅ Ready"; \
|
||||||
elif timeout 5 ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no workshop@$$name.$(DOMAIN) "echo ok" >/dev/null 2>&1; then \
|
|
||||||
echo "⏳ SSH OK, Traefik starting..."; \
|
|
||||||
else \
|
else \
|
||||||
echo "❌ Not ready"; \
|
echo "❌ Not ready"; \
|
||||||
fi; \
|
fi; \
|
||||||
@@ -102,40 +102,15 @@ destroy-cloud:
|
|||||||
@echo "⚠️ This will destroy ALL workshop servers!"
|
@echo "⚠️ This will destroy ALL workshop servers!"
|
||||||
@read -p "Continue? [y/N]: " confirm && [ "$$confirm" = "y" ]
|
@read -p "Continue? [y/N]: " confirm && [ "$$confirm" = "y" ]
|
||||||
cd terraform && terraform destroy -auto-approve
|
cd terraform && terraform destroy -auto-approve
|
||||||
@echo "✅ Cloud infrastructure destroyed"
|
|
||||||
|
|
||||||
local-vm-run:
|
|
||||||
@echo "🖥️ Starting local workshop VM with $(PARTICIPANTS) containers..."
|
|
||||||
@echo "VM will open with desktop showing all participant containers"
|
|
||||||
PARTICIPANTS=$(PARTICIPANTS) nix run --impure .#local-vm
|
|
||||||
|
|
||||||
local-vm-test:
|
|
||||||
@echo "🧪 Testing with 2 containers only..."
|
|
||||||
PARTICIPANTS=2 nix run --impure .#local-vm
|
|
||||||
|
|
||||||
local-vm-full:
|
|
||||||
@echo "🚀 Testing with all 15 containers (heavy resource usage!)..."
|
|
||||||
PARTICIPANTS=15 nix run --impure .#local-vm
|
|
||||||
|
|
||||||
check-vm:
|
|
||||||
@echo "✅ Verifying VM builds correctly..."
|
|
||||||
PARTICIPANTS=2 nix build --impure .#local-vm
|
|
||||||
@echo "✅ VM build successful"
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf result .direnv terraform/.terraform terraform/terraform.tfstate*
|
rm -rf result .direnv terraform/.terraform terraform/terraform.tfstate*
|
||||||
@echo "🧹 Cleaned up build artifacts"
|
@echo "🧹 Cleaned up build artifacts"
|
||||||
|
|
||||||
opencode:
|
opencode:
|
||||||
@echo "💻 Starting opencode in Nix dev shell..."
|
|
||||||
nix develop --command opencode
|
nix develop --command opencode
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
@echo "🔍 Linting project files..."
|
@echo "🔍 Linting project files..."
|
||||||
@echo "Markdown files..."
|
|
||||||
@markdownlint-cli . || true
|
@markdownlint-cli . || true
|
||||||
@echo "JSON files..."
|
|
||||||
@find . -type f -name "*.json" -print0 | xargs -0 -I {} bash -c 'jq . "{}" >/dev/null || (echo "JSON lint error in {}" && exit 1)'
|
|
||||||
@echo "Nix files..."
|
|
||||||
@nixpkgs-fmt --check . || true
|
@nixpkgs-fmt --check . || true
|
||||||
@echo "✅ Linting complete"
|
|
||||||
|
|||||||
274
README.md
274
README.md
@@ -1,249 +1,101 @@
|
|||||||
# 🍪 CODE CRISPIES Workshop Infrastructure
|
# 🪐 CODE CRISPIES Workshop Infrastructure
|
||||||
|
|
||||||
This repository contains the infrastructure for the Co-op Cloud workshop, providing three distinct deployment environments with dynamic scaling support.
|
Single-participant learning environments with local practice and cloud deployment capabilities.
|
||||||
|
|
||||||
---
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Start the local development virtual machine (default: 3 containers)
|
# 1. Start local VM for development/testing
|
||||||
make local-vm-run
|
make local-vm
|
||||||
|
|
||||||
# 2. Test with different container counts
|
# 2. Build USB drives for participants
|
||||||
PARTICIPANTS=2 make local-vm-test # Lightweight testing
|
|
||||||
PARTICIPANTS=15 make local-vm-full # Full workshop simulation
|
|
||||||
|
|
||||||
# 3. Build & flash USB drives for participants
|
|
||||||
make build-usb
|
make build-usb
|
||||||
make flash-usb USB_DEVICE=/dev/sdX
|
make flash-usb USB_DEVICE=/dev/sdX
|
||||||
|
|
||||||
# 4. Deploy the production cloud infrastructure
|
# 3. Deploy cloud infrastructure
|
||||||
export HCLOUD_TOKEN="your_token_here"
|
export HCLOUD_TOKEN="your_token"
|
||||||
make deploy-cloud
|
make deploy-cloud
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## 🎯 Learning Flow
|
||||||
|
|
||||||
## 📁 Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
├── flake.nix # All Nix configurations (USB, VM, containers)
|
|
||||||
├── terraform/ # Hetzner Cloud infrastructure
|
|
||||||
├── scripts/deploy.sh # Cloud setup automation
|
|
||||||
├── docs/USB_BOOT_INSTRUCTIONS.md
|
|
||||||
└── Makefile # Build & deploy commands
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🌍 Three Environments
|
|
||||||
|
|
||||||
### 1. Cloud (Production)
|
|
||||||
|
|
||||||
- **What:** 15 Hetzner VMs named `hopper.codecrispi.es`, `curie.codecrispi.es`, etc.
|
|
||||||
- **Purpose:** The live environment for workshop participants.
|
|
||||||
- **Participants:** hopper, curie, lovelace, noether, hamilton, franklin, johnson, clarke, goldberg, liskov, wing, rosen, shaw, karp, rich
|
|
||||||
|
|
||||||
### 2. USB Boot (Workshop)
|
|
||||||
|
|
||||||
- **What:** A bootable NixOS live environment with SSH client tools.
|
|
||||||
- **Purpose:** Used by participants to connect to their cloud servers.
|
|
||||||
- **Features:** Pre-configured with helper functions like `connect hopper`, `recipes` command, and workshop-specific tooling.
|
|
||||||
|
|
||||||
### 3. Local (Development)
|
|
||||||
|
|
||||||
- **What:** A self-contained Virtual Machine (VM) that runs on your local computer with configurable container count.
|
|
||||||
- **Purpose:** Complete local testing environment that mirrors production setup without needing cloud servers.
|
|
||||||
- **Scalability:** Supports 1-15 containers via `PARTICIPANTS` environment variable.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Local Development Workflow
|
|
||||||
|
|
||||||
1. **Choose Your Scale**
|
|
||||||
```bash
|
|
||||||
# Lightweight development (2 containers)
|
|
||||||
PARTICIPANTS=2 make local-vm-run
|
|
||||||
|
|
||||||
# Production simulation (15 containers) - requires 8GB+ RAM
|
|
||||||
PARTICIPANTS=15 make local-vm-run
|
|
||||||
|
|
||||||
# Use default (3 containers) - good balance
|
|
||||||
make local-vm-run
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Work Inside the VM**
|
|
||||||
All testing is now done inside the VM's graphical desktop:
|
|
||||||
* Open the **Terminal** to run commands.
|
|
||||||
* Open **Firefox** to view the deployed web applications.
|
|
||||||
|
|
||||||
3. **Example: Deploying WordPress**
|
|
||||||
|
|
||||||
**In the VM's Terminal**, get a root shell and SSH into a participant's container:
|
|
||||||
```bash
|
|
||||||
# Become root (no password needed)
|
|
||||||
sudo -i
|
|
||||||
|
|
||||||
# Connect to participant 1 (hopper)
|
|
||||||
connect hopper
|
|
||||||
|
|
||||||
# Or direct SSH (password: root)
|
|
||||||
ssh root@192.168.100.11
|
|
||||||
```
|
|
||||||
|
|
||||||
**Inside the container**, deploy a WordPress site with `abra`:
|
|
||||||
```bash
|
|
||||||
abra app new wordpress -S --domain=blog.hopper.local
|
|
||||||
abra app deploy blog.hopper.local
|
|
||||||
```
|
|
||||||
|
|
||||||
**In the VM's Firefox**, navigate to `http://blog.hopper.local`. You will see the WordPress installation screen.
|
|
||||||
|
|
||||||
4. **Available Helper Commands**
|
|
||||||
```bash
|
|
||||||
sudo containers # List all containers with IPs
|
|
||||||
sudo logs # Show setup logs for all containers
|
|
||||||
sudo recipes # Display available Co-op Cloud recipes
|
|
||||||
sudo help # Show all available commands
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🌐 Cloud Deployment
|
|
||||||
|
|
||||||
The cloud environment creates 15 production servers:
|
|
||||||
|
|
||||||
|
### Local Practice (USB/VM)
|
||||||
```bash
|
```bash
|
||||||
# Set required environment variables
|
recipes # Show available apps
|
||||||
export HCLOUD_TOKEN="your_hetzner_token"
|
deploy wordpress # Deploy locally
|
||||||
export HETZNER_DNS_TOKEN="your_dns_token"
|
browser # View at wordpress.workshop.local
|
||||||
export DNS_ZONE_ID="your_zone_id"
|
|
||||||
|
|
||||||
# Deploy all 15 servers
|
|
||||||
make deploy-cloud
|
|
||||||
|
|
||||||
# Check server status
|
|
||||||
make status-cloud
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Each server is accessible at:
|
### Cloud Deployment
|
||||||
- `hopper.codecrispi.es`
|
```bash
|
||||||
- `curie.codecrispi.es`
|
connect hopper # SSH to cloud server
|
||||||
- `lovelace.codecrispi.es`
|
# Same abra commands work here
|
||||||
- ... (15 total)
|
abra app new wordpress -S --domain=blog.hopper.codecrispi.es
|
||||||
|
abra app deploy blog.hopper.codecrispi.es
|
||||||
---
|
```
|
||||||
|
|
||||||
## 💾 USB Workshop Environment
|
## 🏗️ Architecture
|
||||||
|
|
||||||
Build bootable USB drives for participants:
|
**Single Participant Model**: Each environment (USB/VM) is complete and self-contained.
|
||||||
|
|
||||||
|
- **USB Boot**: Bootable NixOS with Docker + abra for hands-on learning
|
||||||
|
- **Local VM**: Identical environment for development/testing
|
||||||
|
- **Cloud Servers**: 15 production servers (hopper, curie, lovelace, etc.)
|
||||||
|
|
||||||
|
## 💾 USB Environment
|
||||||
|
|
||||||
|
Pre-configured with:
|
||||||
|
- Docker Swarm + abra installation
|
||||||
|
- SSH client for cloud access
|
||||||
|
- Terminal-first interface (`desktop` command for GUI)
|
||||||
|
- Helper commands: `recipes`, `deploy`, `connect`, `help`
|
||||||
|
|
||||||
|
Build and flash:
|
||||||
```bash
|
```bash
|
||||||
# Build the ISO
|
|
||||||
make build-usb
|
make build-usb
|
||||||
|
|
||||||
# Flash to USB drive (replace /dev/sdX with your device)
|
|
||||||
make flash-usb USB_DEVICE=/dev/sdb
|
make flash-usb USB_DEVICE=/dev/sdb
|
||||||
```
|
```
|
||||||
|
|
||||||
The USB environment includes:
|
## 🌐 Cloud Deployment
|
||||||
- Pre-configured SSH client
|
|
||||||
- `connect <name>` command to SSH into assigned servers
|
|
||||||
- `recipes` command showing available Co-op Cloud applications
|
|
||||||
- Workshop-specific networking and WiFi helpers
|
|
||||||
|
|
||||||
---
|
Creates 15 Hetzner VMs at `{name}.codecrispi.es`:
|
||||||
|
|
||||||
## ⚙️ Environment Variables
|
|
||||||
|
|
||||||
Control workshop behavior with environment variables:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Number of containers (1-15, default: 3)
|
export HCLOUD_TOKEN="your_token"
|
||||||
export PARTICIPANTS=5
|
make deploy-cloud
|
||||||
make local-vm-run
|
make status-cloud # Check health
|
||||||
|
|
||||||
# Workshop domain for cloud deployment
|
|
||||||
export WORKSHOP_DOMAIN=myworkshop.com
|
|
||||||
|
|
||||||
# USB device for flashing
|
|
||||||
export USB_DEVICE=/dev/sdb
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## 🖥️ Local Development
|
||||||
|
|
||||||
## 🧹 Cleanup & Management
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clean local build artifacts
|
make local-vm # Start VM
|
||||||
make clean
|
make test-vm # Verify build
|
||||||
|
|
||||||
# Destroy Hetzner cloud infrastructure
|
|
||||||
make destroy-cloud
|
|
||||||
|
|
||||||
# Verify VM builds correctly
|
|
||||||
make check-vm
|
|
||||||
|
|
||||||
# Run development tools
|
|
||||||
make opencode # Start development environment
|
|
||||||
make lint # Code quality checks
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
The VM simulates the USB experience with identical configuration and commands.
|
||||||
|
|
||||||
## 🔑 Prerequisites
|
## 📚 Available Commands
|
||||||
|
|
||||||
- **SSH Key:** Ed25519 key at `~/.ssh/id_ed25519.pub`
|
**In USB/VM environments**:
|
||||||
```bash
|
- `recipes` - Show Co-op Cloud catalog
|
||||||
ssh-keygen -t ed25519
|
- `deploy <app>` - Deploy locally (e.g., `deploy wordpress`)
|
||||||
```
|
- `apps` - List deployed applications
|
||||||
- **Nix:** NixOS or Nix package manager with flakes enabled
|
- `connect <server>` - SSH to cloud server
|
||||||
- **Cloud Tokens:** Hetzner Cloud API token for deployment
|
- `cloud-deploy <app> <server>` - Direct cloud deployment
|
||||||
- **Resources:**
|
- `desktop` - Start GUI session
|
||||||
- 2-3 containers: 4GB+ RAM
|
- `browser` - Launch Firefox
|
||||||
- 5-10 containers: 8GB+ RAM
|
|
||||||
- 15 containers: 16GB+ RAM
|
|
||||||
|
|
||||||
---
|
## 🔧 Prerequisites
|
||||||
|
|
||||||
## 🎯 Workshop Flow
|
- Nix with flakes enabled
|
||||||
|
- SSH key at `~/.ssh/id_ed25519.pub`
|
||||||
|
- HCLOUD_TOKEN for cloud deployment
|
||||||
|
- 2GB+ RAM for VM testing
|
||||||
|
|
||||||
1. **Preparation:** Deploy cloud infrastructure with `make deploy-cloud`
|
## 🧹 Cleanup
|
||||||
2. **Distribution:** Flash USB drives for participants with `make build-usb && make flash-usb`
|
|
||||||
3. **Workshop:** Participants boot from USB and connect to their assigned cloud servers
|
|
||||||
4. **Development:** Use local VM with `make local-vm-run` for testing and development
|
|
||||||
|
|
||||||
The architecture ensures participants get identical environments whether connecting from USB boot drives to cloud servers, or testing locally in the development VM.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
### VM Won't Start
|
|
||||||
```bash
|
```bash
|
||||||
# Check if build works
|
make clean # Local artifacts
|
||||||
make check-vm
|
make destroy-cloud # Cloud infrastructure
|
||||||
|
|
||||||
# Try with fewer containers
|
|
||||||
PARTICIPANTS=2 make local-vm-run
|
|
||||||
```
|
|
||||||
|
|
||||||
### Containers Not Accessible
|
|
||||||
```bash
|
|
||||||
# Check container status inside VM
|
|
||||||
sudo containers
|
|
||||||
|
|
||||||
# View setup logs
|
|
||||||
sudo logs
|
|
||||||
|
|
||||||
# Manual SSH test
|
|
||||||
ssh root@192.168.100.11 # Password: root
|
|
||||||
```
|
|
||||||
|
|
||||||
### Abra Not Working in Container
|
|
||||||
```bash
|
|
||||||
# Inside container, check installation
|
|
||||||
ls -la /root/.local/bin/abra
|
|
||||||
export PATH="/root/.local/bin:$PATH"
|
|
||||||
abra --version
|
|
||||||
```
|
```
|
||||||
|
|||||||
310
flake.nix
310
flake.nix
@@ -54,11 +54,14 @@
|
|||||||
networking.networkmanager.enable = true;
|
networking.networkmanager.enable = true;
|
||||||
networking.hostName = "workshop-live";
|
networking.hostName = "workshop-live";
|
||||||
|
|
||||||
|
# Enable Docker for local development
|
||||||
|
virtualisation.docker.enable = true;
|
||||||
|
|
||||||
services.getty.autologinUser = "workshop";
|
services.getty.autologinUser = "workshop";
|
||||||
users.users.workshop = {
|
users.users.workshop = {
|
||||||
isNormalUser = true;
|
isNormalUser = true;
|
||||||
shell = pkgs.zsh;
|
shell = pkgs.zsh;
|
||||||
extraGroups = [ "networkmanager" "wheel" ];
|
extraGroups = [ "networkmanager" "wheel" "docker" ];
|
||||||
password = "";
|
password = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,18 +69,98 @@
|
|||||||
|
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
openssh curl git networkmanager firefox xterm
|
openssh curl git networkmanager firefox xterm
|
||||||
|
docker docker-compose
|
||||||
|
# For local abra installation
|
||||||
|
bash wget jq tree nano
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# Auto-install abra on boot
|
||||||
|
systemd.services.workshop-abra-setup = {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "network-online.target" "docker.service" ];
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
|
script = ''
|
||||||
|
export HOME=/home/workshop
|
||||||
|
|
||||||
|
# Wait for network
|
||||||
|
for i in {1..10}; do
|
||||||
|
if ${pkgs.curl}/bin/curl -s --max-time 5 google.com >/dev/null 2>&1; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
|
||||||
|
# Install abra for workshop user (DO NOT change installation method)
|
||||||
|
if [ ! -f /home/workshop/.local/bin/abra ]; then
|
||||||
|
sudo -u workshop mkdir -p /home/workshop/.local/bin
|
||||||
|
cd /home/workshop
|
||||||
|
sudo -u workshop ${pkgs.curl}/bin/curl -fsSL https://install.abra.coopcloud.tech | sudo -u workshop ${pkgs.bash}/bin/bash
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Initialize local Docker Swarm
|
||||||
|
${pkgs.docker}/bin/docker swarm init --advertise-addr 127.0.0.1 2>/dev/null || true
|
||||||
|
|
||||||
|
# Add workshop user to docker group
|
||||||
|
usermod -aG docker workshop
|
||||||
|
'';
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
User = "root";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
programs.zsh = {
|
programs.zsh = {
|
||||||
enable = true;
|
enable = true;
|
||||||
interactiveShellInit = ''
|
interactiveShellInit = ''
|
||||||
echo "CODE CRISPIES Workshop Environment"
|
echo "CODE CRISPIES Workshop Environment"
|
||||||
echo "Available servers:"
|
echo "Mode: Local Development + Cloud Access"
|
||||||
|
echo ""
|
||||||
|
echo "🏠 Local Development:"
|
||||||
|
echo " recipes - Show available app recipes"
|
||||||
|
echo " deploy <recipe> - Deploy app locally (e.g., deploy wordpress)"
|
||||||
|
echo " browser - Launch Firefox"
|
||||||
|
echo " desktop - Start GUI session"
|
||||||
|
echo ""
|
||||||
|
echo "☁️ Cloud Access:"
|
||||||
|
echo " Available servers:"
|
||||||
${builtins.concatStringsSep "\n" (map (name:
|
${builtins.concatStringsSep "\n" (map (name:
|
||||||
"echo \" - ${name}.codecrispi.es\""
|
"echo \" - ${name}.codecrispi.es\""
|
||||||
) allParticipantNames)}
|
) allParticipantNames)}
|
||||||
|
echo " connect <name> - SSH to cloud server"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Commands: connect <name> | recipes | help"
|
echo "📚 Commands: recipes | deploy | connect | browser | desktop | help"
|
||||||
|
|
||||||
|
# Ensure abra is in PATH
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
|
||||||
|
deploy() {
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "Usage: deploy <recipe>"
|
||||||
|
echo "Example: deploy wordpress"
|
||||||
|
echo "Run 'recipes' to see available options"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local recipe="$1"
|
||||||
|
local domain="$recipe.workshop.local"
|
||||||
|
|
||||||
|
echo "🚀 Deploying $recipe locally..."
|
||||||
|
echo "Domain: $domain"
|
||||||
|
|
||||||
|
# Check if abra is available
|
||||||
|
if ! command -v abra &> /dev/null; then
|
||||||
|
echo "❌ Abra not found. Run 'sudo systemctl restart workshop-abra-setup'"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Deploy with abra
|
||||||
|
abra app new "$recipe" -S --domain="$domain"
|
||||||
|
abra app deploy "$domain"
|
||||||
|
|
||||||
|
echo "✅ Deployed! Access at: http://$domain"
|
||||||
|
echo "🌐 Open browser with: browser"
|
||||||
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
[ -z "$1" ] && { echo "Usage: connect <name>"; return 1; }
|
[ -z "$1" ] && { echo "Usage: connect <name>"; return 1; }
|
||||||
@@ -88,44 +171,77 @@
|
|||||||
recipes() {
|
recipes() {
|
||||||
echo "Available Co-op Cloud Recipes:"
|
echo "Available Co-op Cloud Recipes:"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Content Management:"
|
echo "📝 Content Management:"
|
||||||
echo " wordpress ghost hedgedoc dokuwiki mediawiki"
|
echo " wordpress ghost hedgedoc dokuwiki mediawiki"
|
||||||
echo ""
|
echo ""
|
||||||
echo "File & Collaboration:"
|
echo "📁 File & Collaboration:"
|
||||||
echo " nextcloud seafile collabora onlyoffice"
|
echo " nextcloud seafile collabora onlyoffice"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Communication:"
|
echo "💬 Communication:"
|
||||||
echo " jitsi-meet matrix-synapse rocketchat mattermost"
|
echo " jitsi-meet matrix-synapse rocketchat mattermost"
|
||||||
echo ""
|
echo ""
|
||||||
echo "E-commerce & Business:"
|
echo "🛒 E-commerce & Business:"
|
||||||
echo " prestashop invoiceninja kimai pretix"
|
echo " prestashop invoiceninja kimai pretix"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Development & Tools:"
|
echo "⚙️ Development & Tools:"
|
||||||
echo " gitea drone n8n gitlab jupyter-lab"
|
echo " gitea drone n8n gitlab jupyter-lab"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Analytics & Monitoring:"
|
echo "📊 Analytics & Monitoring:"
|
||||||
echo " plausible matomo uptime-kuma grafana"
|
echo " plausible matomo uptime-kuma grafana"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Media & Social:"
|
echo "🎵 Media & Social:"
|
||||||
echo " peertube funkwhale mastodon pixelfed jellyfin"
|
echo " peertube funkwhale mastodon pixelfed jellyfin"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Deploy: abra app new <recipe> -S --domain=myapp.<name>.codecrispi.es"
|
echo "🚀 Local Deploy: deploy <recipe>"
|
||||||
echo "Browse all: https://recipes.coopcloud.tech"
|
echo "☁️ Cloud Deploy: connect <server> then use abra commands"
|
||||||
|
echo "📖 Browse all: https://recipes.coopcloud.tech"
|
||||||
|
}
|
||||||
|
|
||||||
|
browser() {
|
||||||
|
echo "🌐 Starting Firefox..."
|
||||||
|
if [ -n "$DISPLAY" ]; then
|
||||||
|
firefox &
|
||||||
|
else
|
||||||
|
echo "❌ No GUI session. Run 'desktop' first"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
desktop() {
|
||||||
|
echo "🖥️ Starting GUI session..."
|
||||||
|
if [ -z "$DISPLAY" ]; then
|
||||||
|
startx &
|
||||||
|
export DISPLAY=:0
|
||||||
|
sleep 3
|
||||||
|
echo "✅ GUI started. Run 'browser' to open Firefox"
|
||||||
|
else
|
||||||
|
echo "ℹ️ GUI already running"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
help() {
|
help() {
|
||||||
echo "CODE CRISPIES Workshop Commands:"
|
echo "CODE CRISPIES Workshop Commands:"
|
||||||
echo ""
|
echo ""
|
||||||
echo "connect <name> - SSH to your assigned server"
|
echo "🏠 Local Development:"
|
||||||
echo "recipes - Show available app recipes"
|
echo " recipes - Show all available app recipes"
|
||||||
echo "sudo nmcli dev wifi connect SSID password PASSWORD"
|
echo " deploy <recipe> - Deploy app locally (e.g., deploy wordpress)"
|
||||||
|
echo " browser - Launch Firefox browser"
|
||||||
|
echo " desktop - Start GUI desktop session"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Examples:"
|
echo "☁️ Cloud Access:"
|
||||||
echo " connect hopper"
|
echo " connect <name> - SSH to cloud server (e.g., connect hopper)"
|
||||||
echo " sudo nmcli dev wifi connect CODE_CRISPIES_GUEST password workshop2024"
|
echo ""
|
||||||
|
echo "🔧 System:"
|
||||||
|
echo " sudo nmcli dev wifi connect SSID password PASSWORD"
|
||||||
|
echo " sudo systemctl restart workshop-abra-setup # Reinstall abra"
|
||||||
|
echo ""
|
||||||
|
echo "📚 Learning Flow:"
|
||||||
|
echo " 1. Try local: recipes → deploy wordpress → browser"
|
||||||
|
echo " 2. Try cloud: connect hopper → same abra commands"
|
||||||
|
echo ""
|
||||||
|
echo "Available servers: ${builtins.concatStringsSep " " allParticipantNames}"
|
||||||
}
|
}
|
||||||
|
|
||||||
export -f connect recipes help
|
export -f deploy connect recipes browser desktop help
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -134,16 +250,17 @@
|
|||||||
desktopManager.xfce.enable = true;
|
desktopManager.xfce.enable = true;
|
||||||
displayManager = {
|
displayManager = {
|
||||||
lightdm.enable = true;
|
lightdm.enable = true;
|
||||||
autoLogin.enable = true;
|
autoLogin.enable = false; # Manual desktop start
|
||||||
autoLogin.user = "workshop";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Don't auto-start GUI, let user choose
|
||||||
systemd.user.services.workshop-welcome = {
|
systemd.user.services.workshop-welcome = {
|
||||||
wantedBy = [ "graphical-session.target" ];
|
wantedBy = [ "default.target" ];
|
||||||
after = [ "graphical-session.target" ];
|
script = ''
|
||||||
script = "${pkgs.xterm}/bin/xterm -title 'CODE CRISPIES Workshop' -e 'zsh' &";
|
echo "Welcome! Run 'desktop' to start GUI session"
|
||||||
serviceConfig.Type = "forking";
|
'';
|
||||||
|
serviceConfig.Type = "oneshot";
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
@@ -317,7 +434,7 @@
|
|||||||
) (builtins.length participantNames));
|
) (builtins.length participantNames));
|
||||||
};
|
};
|
||||||
|
|
||||||
# Dynamic container generation
|
# Dynamic container generation with improved stability
|
||||||
containers = builtins.listToAttrs (builtins.genList
|
containers = builtins.listToAttrs (builtins.genList
|
||||||
(i:
|
(i:
|
||||||
let
|
let
|
||||||
@@ -332,6 +449,9 @@
|
|||||||
hostAddress = "192.168.100.1";
|
hostAddress = "192.168.100.1";
|
||||||
localAddress = ip;
|
localAddress = ip;
|
||||||
|
|
||||||
|
# Add restart policy for container itself
|
||||||
|
restartIfChanged = true;
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
system.stateVersion = "25.05";
|
system.stateVersion = "25.05";
|
||||||
|
|
||||||
@@ -363,61 +483,111 @@
|
|||||||
docker curl git wget jq bash nano tree
|
docker curl git wget jq bash nano tree
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# Improved workshop setup with retry logic
|
||||||
systemd.services.workshop-setup = {
|
systemd.services.workshop-setup = {
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
after = [ "network-online.target" "docker.service" ];
|
after = [ "network-online.target" "docker.service" ];
|
||||||
wants = [ "network-online.target" ];
|
wants = [ "network-online.target" ];
|
||||||
script = ''
|
|
||||||
echo "Setting up ${name} container..."
|
|
||||||
|
|
||||||
# Wait for network connectivity
|
# Add restart capability for failed setups
|
||||||
for i in {1..15}; do
|
|
||||||
if ${pkgs.curl}/bin/curl -s --max-time 5 google.com >/dev/null 2>&1; then
|
|
||||||
echo "Network ready"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
echo "Waiting for network... ($i/15)"
|
|
||||||
sleep 3
|
|
||||||
done
|
|
||||||
|
|
||||||
# Initialize Docker Swarm
|
|
||||||
${pkgs.docker}/bin/docker swarm init --advertise-addr ${ip} || echo "Swarm already initialized or failed"
|
|
||||||
|
|
||||||
# Install abra
|
|
||||||
export HOME=/root
|
|
||||||
if [ ! -f /root/.local/bin/abra ]; then
|
|
||||||
echo "Installing abra..."
|
|
||||||
${pkgs.curl}/bin/curl -fsSL https://install.abra.coopcloud.tech | ${pkgs.bash}/bin/bash
|
|
||||||
echo "Abra installed to /root/.local/bin/abra"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Setup PATH in .bashrc
|
|
||||||
if ! grep -q "/.local/bin" /root/.bashrc 2>/dev/null; then
|
|
||||||
echo 'export PATH="$HOME/.local/bin:$PATH"' >> /root/.bashrc
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create system symlink for abra
|
|
||||||
if [ -f /root/.local/bin/abra ]; then
|
|
||||||
ln -sf /root/.local/bin/abra /usr/local/bin/abra 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Add abra server config
|
|
||||||
if [ -f /root/.local/bin/abra ]; then
|
|
||||||
export PATH="/root/.local/bin:$PATH"
|
|
||||||
/root/.local/bin/abra server add ${name}.local 2>/dev/null || echo "Server already added or command failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "${name} container ready!"
|
|
||||||
echo "SSH: ssh root@${ip} (password: root)"
|
|
||||||
echo "Workshop user: ssh workshop@${ip} (password: workshop)"
|
|
||||||
echo "Abra: Available via 'abra' command"
|
|
||||||
'';
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
RemainAfterExit = true;
|
RemainAfterExit = true;
|
||||||
StandardOutput = "journal";
|
StandardOutput = "journal";
|
||||||
StandardError = "journal";
|
StandardError = "journal";
|
||||||
TimeoutStartSec = "300";
|
TimeoutStartSec = "300";
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "30s";
|
||||||
|
StartLimitBurst = 3;
|
||||||
|
StartLimitIntervalSec = "10min";
|
||||||
|
};
|
||||||
|
|
||||||
|
script = ''
|
||||||
|
set -e
|
||||||
|
echo "Setting up ${name} container (attempt started)..."
|
||||||
|
|
||||||
|
# Wait for network connectivity with timeout
|
||||||
|
network_ready=false
|
||||||
|
for i in {1..20}; do
|
||||||
|
if ${pkgs.curl}/bin/curl -s --max-time 5 google.com >/dev/null 2>&1; then
|
||||||
|
echo "Network ready after $i attempts"
|
||||||
|
network_ready=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "Waiting for network... ($i/20)"
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$network_ready" != "true" ]; then
|
||||||
|
echo "❌ Network failed after 20 attempts"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Initialize Docker Swarm (idempotent)
|
||||||
|
if ! ${pkgs.docker}/bin/docker info | grep -q "Swarm: active"; then
|
||||||
|
echo "Initializing Docker Swarm..."
|
||||||
|
${pkgs.docker}/bin/docker swarm init --advertise-addr ${ip} || {
|
||||||
|
echo "Swarm init failed, but continuing..."
|
||||||
|
}
|
||||||
|
else
|
||||||
|
echo "Docker Swarm already active"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install abra (DO NOT change installation method - keep curl approach)
|
||||||
|
export HOME=/root
|
||||||
|
if [ ! -f /root/.local/bin/abra ]; then
|
||||||
|
echo "Installing abra..."
|
||||||
|
${pkgs.curl}/bin/curl -fsSL https://install.abra.coopcloud.tech | ${pkgs.bash}/bin/bash
|
||||||
|
echo "Abra installed to /root/.local/bin/abra"
|
||||||
|
else
|
||||||
|
echo "Abra already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Setup PATH in .bashrc (idempotent)
|
||||||
|
if ! grep -q "/.local/bin" /root/.bashrc 2>/dev/null; then
|
||||||
|
echo 'export PATH="$HOME/.local/bin:$PATH"' >> /root/.bashrc
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create system symlink for abra (idempotent)
|
||||||
|
if [ -f /root/.local/bin/abra ]; then
|
||||||
|
ln -sf /root/.local/bin/abra /usr/local/bin/abra 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add abra server config (idempotent)
|
||||||
|
if [ -f /root/.local/bin/abra ]; then
|
||||||
|
export PATH="/root/.local/bin:$PATH"
|
||||||
|
/root/.local/bin/abra server add ${name}.local 2>/dev/null || echo "Server config handled"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ ${name} container setup completed successfully!"
|
||||||
|
echo "SSH: ssh root@${ip} (password: root)"
|
||||||
|
echo "Workshop user: ssh workshop@${ip} (password: workshop)"
|
||||||
|
echo "Abra: Available via 'abra' command"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Add container health monitoring
|
||||||
|
systemd.services.workshop-health = {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "workshop-setup.service" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
Restart = "always";
|
||||||
|
RestartSec = "60s";
|
||||||
|
ExecStart = "${pkgs.writeScript "health-check" ''
|
||||||
|
#!/bin/bash
|
||||||
|
while true; do
|
||||||
|
# Check if abra is accessible
|
||||||
|
if [ -f /root/.local/bin/abra ]; then
|
||||||
|
export PATH="/root/.local/bin:$PATH"
|
||||||
|
/root/.local/bin/abra --version >/dev/null 2>&1 || {
|
||||||
|
echo "⚠️ Abra health check failed, triggering restart"
|
||||||
|
systemctl restart workshop-setup.service
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
sleep 300 # Check every 5 minutes
|
||||||
|
done
|
||||||
|
''}";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user