fix: simplify systemd service script for workshop setup

This commit is contained in:
2025-08-17 01:45:34 +02:00
parent 2486b381d5
commit cc8e8dca93

View File

@@ -9,7 +9,7 @@ let
}; };
}; };
# Complete Co-op Cloud recipe list (based on your ABRA_RECIPES.md and more) # Complete Co-op Cloud recipe list
allRecipes = [ allRecipes = [
# Tier 1 - Production Ready (Score 5) # Tier 1 - Production Ready (Score 5)
"gitea" "gitea"
@@ -40,7 +40,7 @@ let
"owncast" "owncast"
"rallly" "rallly"
# Additional recipes from Co-op Cloud catalog # Extended catalog
"hedgedoc" "hedgedoc"
"mediawiki" "mediawiki"
"seafile" "seafile"
@@ -65,11 +65,12 @@ let
"pixelfed" "pixelfed"
"jellyfin" "jellyfin"
]; ];
in
in
isoConfig // { isoConfig // {
system.stateVersion = "25.05"; system.stateVersion = "25.05";
# SSH Configuration
services.openssh = { services.openssh = {
enable = true; enable = true;
settings = { settings = {
@@ -80,69 +81,55 @@ isoConfig // {
ports = [ 22 ]; ports = [ 22 ];
}; };
# Network Configuration
networking = { networking = {
wireless.enable = false; wireless.enable = false;
networkmanager = { networkmanager = {
enable = true; enable = true;
dns = "none"; dns = "none"; # We use dnsmasq
}; };
hostName = if isLiveIso then "workshop-live" else "workshop-vm"; hostName = if isLiveIso then "workshop-live" else "workshop-vm";
hosts = { hosts."127.0.0.1" = [ "workshop.local" "localhost" ];
"127.0.0.1" = [ "workshop.local" "localhost" ]; nameservers = lib.mkForce [ "127.0.0.1" ];
}; firewall.enable = false; # Workshop environment
}; };
# Configure dnsmasq properly for wildcard DNS # DNS Configuration - Wildcard *.workshop.local -> 127.0.0.1
services.dnsmasq = { services.dnsmasq = {
enable = true; enable = true;
settings = { settings = {
# Wildcard: *.workshop.local -> 127.0.0.1
address = "/.workshop.local/127.0.0.1"; address = "/.workshop.local/127.0.0.1";
# Use upstream DNS for everything else
server = [ "8.8.8.8" "1.1.1.1" ]; server = [ "8.8.8.8" "1.1.1.1" ];
# Listen on all interfaces (important for VM/container access)
listen-address = [ "127.0.0.1" ]; listen-address = [ "127.0.0.1" ];
# Bind to interfaces
bind-interfaces = true; bind-interfaces = true;
# Don't read /etc/hosts for our custom domains
no-hosts = false;
# Cache settings
cache-size = 1000; cache-size = 1000;
log-queries = true;
log-dhcp = true;
# Local domain handling
local = "/workshop.local/"; local = "/workshop.local/";
domain-needed = true; domain-needed = true;
bogus-priv = true; bogus-priv = true;
}; };
}; };
# Force system to use our dnsmasq # Disable systemd-resolved (conflicts with dnsmasq)
networking.nameservers = lib.mkForce [ "127.0.0.1" ];
# Disable systemd-resolved to avoid conflicts
services.resolved.enable = false; services.resolved.enable = false;
# Enable Docker for local development # Container Runtime
virtualisation.docker.enable = true; virtualisation.docker.enable = true;
services.getty.autologinUser = "workshop"; # User Configuration
users.users.root.password = "root"; users = {
users.users.workshop = { users.root.password = "root";
isNormalUser = true; users.workshop = {
shell = pkgs.bash; isNormalUser = true;
extraGroups = [ "networkmanager" "wheel" "docker" ]; shell = pkgs.bash;
password = "workshop"; extraGroups = [ "networkmanager" "wheel" "docker" ];
password = "workshop";
};
}; };
services.getty.autologinUser = "workshop";
security.sudo.wheelNeedsPassword = false; security.sudo.wheelNeedsPassword = false;
# System Packages
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
openssh openssh
curl curl
@@ -157,30 +144,27 @@ isoConfig // {
tree tree
nano nano
dnsutils dnsutils
dig # For DNS debugging dig
]; ];
# Auto-install abra and setup Docker Swarm # Workshop Setup Service - REFACTORED
systemd.services.workshop-abra-setup = { systemd.services.workshop-abra-setup = {
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" "docker.service" "dnsmasq.service" ]; after = [ "network-online.target" "docker.service" "dnsmasq.service" ];
wants = [ "network-online.target" ]; wants = [ "network-online.target" ];
script = '' script = ''
export HOME=/home/workshop # Wait for network and services
export PATH="/run/current-system/sw/bin:/usr/bin:/bin"
# Wait for network and services with better testing
echo "Waiting for services to start..." echo "Waiting for services to start..."
for i in {1..30}; do for i in {1..30}; do
# Test external connectivity if curl -s --max-time 3 google.com >/dev/null 2>&1; then
if /run/current-system/sw/bin/curl -s --max-time 3 google.com >/dev/null 2>&1; then
echo " External network ready" echo " External network ready"
break break
fi fi
sleep 2 sleep 2
done done
# Test DNS resolution specifically # Test DNS resolution
for i in {1..20}; do for i in {1..20}; do
if /run/current-system/sw/bin/nslookup test.workshop.local 127.0.0.1 >/dev/null 2>&1; then if nslookup test.workshop.local 127.0.0.1 >/dev/null 2>&1; then
echo " Wildcard DNS ready" echo " Wildcard DNS ready"
break break
fi fi
@@ -189,7 +173,7 @@ isoConfig // {
done done
# Test Docker # Test Docker
for i in {1..10}; do for i in {1..10}; do
if /run/current-system/sw/bin/docker info >/dev/null 2>&1; then if docker info >/dev/null 2>&1; then
echo " Docker ready" echo " Docker ready"
break break
fi fi
@@ -198,24 +182,22 @@ isoConfig // {
# Install abra for workshop user # Install abra for workshop user
if [ ! -f /home/workshop/.local/bin/abra ]; then if [ ! -f /home/workshop/.local/bin/abra ]; then
echo "🚀 Installing abra for user workshop..." echo "🚀 Installing abra for user workshop..."
/usr/bin/su - workshop -c "mkdir -p /home/workshop/.local/bin" sudo -u workshop bash -c "mkdir -p /home/workshop/.local/bin"
# Run installer and log output sudo -u workshop bash -c "curl -fsSL https://install.abra.coopcloud.tech | bash"
install_log="/tmp/abra-install.log"
/usr/bin/su - workshop -c "bash -c \"cd /home/workshop && /run/current-system/sw/bin/curl -fsSL https://install.abra.coopcloud.tech | bash\"" &> "$install_log"
if [ -f /home/workshop/.local/bin/abra ]; then if [ -f /home/workshop/.local/bin/abra ]; then
echo " abra installed successfully." echo " abra installed successfully."
else else
echo " abra installation failed. See logs: cat $install_log" echo " abra installation failed."
fi fi
else else
echo " abra already installed." echo " abra already installed."
fi fi
# Initialize Docker Swarm # Initialize Docker Swarm
echo "🔄 Checking Docker Swarm status..." echo "🔄 Checking Docker Swarm status..."
if ! /run/current-system/sw/bin/docker info | grep -q "Swarm: active"; then if ! docker info | grep -q "Swarm: active"; then
echo "🔥 Initializing Docker Swarm..." echo "🔥 Initializing Docker Swarm..."
/run/current-system/sw/bin/docker swarm init --advertise-addr 127.0.0.1 2>/dev/null || true docker swarm init --advertise-addr 127.0.0.1 2>/dev/null || true
if /run/current-system/sw/bin/docker info | grep -q "Swarm: active"; then if docker info | grep -q "Swarm: active"; then
echo " Docker Swarm initialized." echo " Docker Swarm initialized."
else else
echo " Docker Swarm initialization failed." echo " Docker Swarm initialization failed."
@@ -225,22 +207,18 @@ isoConfig // {
fi fi
# Ensure workshop user is in docker group # Ensure workshop user is in docker group
echo "🔄 Ensuring workshop user is in docker group..." echo "🔄 Ensuring workshop user is in docker group..."
/usr/bin/usermod -aG docker workshop usermod -aG docker workshop
if id -nG workshop | grep -q "docker"; then if id -nG workshop | grep -q "docker"; then
echo " workshop user is in docker group." echo " workshop user is in docker group."
else else
echo " Failed to add workshop user to docker group." echo " Failed to add workshop user to docker group."
fi fi
# Create proper abra server configuration
if [ ! -f /home/workshop/.abra/servers/workshop.local.env ]; then
/usr/bin/su - workshop -c "mkdir -p /home/workshop/.abra/servers/"
fi
# Set up autocomplete # Set up autocomplete
if command -v abra &> /dev/null; then if command -v abra &> /dev/null; then
/run/current-system/sw/bin/su - workshop -c "source <(/home/workshop/.local/bin/abra autocomplete bash)" sudo -u workshop bash -c "source <(/home/workshop/.local/bin/abra autocomplete bash)"
fi fi
# Test final DNS resolution # Test final DNS resolution
if /run/current-system/sw/bin/nslookup test.workshop.local 127.0.0.1; then if nslookup test.workshop.local 127.0.0.1; then
echo "🎉 All services ready!" echo "🎉 All services ready!"
else else
echo " DNS may need manual restart: sudo systemctl restart dnsmasq" echo " DNS may need manual restart: sudo systemctl restart dnsmasq"
@@ -250,139 +228,139 @@ isoConfig // {
Type = "oneshot"; Type = "oneshot";
RemainAfterExit = true; RemainAfterExit = true;
User = "root"; User = "root";
Environment = [
"PATH=/run/current-system/sw/bin:/usr/bin:/bin"
];
}; };
}; };
# Enhanced bash configuration with complete recipe support # Enhanced Bash Configuration with All Features
programs.bash = { programs.bash.interactiveShellInit =
interactiveShellInit = '' let
# Workshop welcome and command definitions recipeList = builtins.concatStringsSep " " allRecipes;
serverList = builtins.concatStringsSep " " cloudServerNames;
in
''
# Workshop Environment Welcome
echo "🚀 CODE CRISPIES Workshop Environment" echo "🚀 CODE CRISPIES Workshop Environment"
echo "Mode: Local Development + Cloud Access" echo "Mode: Local Development + Cloud Access"
echo "" echo ""
# Test DNS immediately on login # DNS Health Check
if command -v nslookup &> /dev/null; then if command -v nslookup >/dev/null 2>&1; then
if nslookup test.workshop.local 127.0.0.1 >/dev/null 2>&1; then if nslookup test.workshop.local 127.0.0.1 >/dev/null 2>&1; then
echo " DNS wildcard ready: *.workshop.local 127.0.0.1" echo " DNS wildcard ready: *.workshop.local 127.0.0.1"
else else
echo " DNS not working! Run: sudo systemctl restart dnsmasq" echo " DNS not working! Run: sudo systemctl restart dnsmasq"
echo "🔧 Debug: nslookup test.workshop.local 127.0.0.1"
fi fi
fi fi
# Ensure abra is in PATH # Ensure abra is in PATH
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
# Complete recipe list for bash completion # Bash Completion Configuration
ALL_RECIPES="${builtins.concatStringsSep " " allRecipes}"
# Enable tab completion for deploy and browser commands
_workshop_completion() { _workshop_completion() {
local cur prev opts local cur prev
COMPREPLY=() COMPREPLY=()
cur="''${COMP_WORDS[COMP_CWORD]}" cur="''${COMP_WORDS[COMP_CWORD]}"
prev="''${COMP_WORDS[COMP_CWORD-1]}" prev="''${COMP_WORDS[COMP_CWORD-1]}"
case "''${prev}" in case "$prev" in
deploy|browser) deploy|browser)
opts="$ALL_RECIPES" COMPREPLY=($(compgen -W "${recipeList}" -- "$cur"))
COMPREPLY=( $(compgen -W "''${opts}" -- ''${cur}) )
return 0 return 0
;; ;;
connect) connect)
opts="${builtins.concatStringsSep " " cloudServerNames}" COMPREPLY=($(compgen -W "${serverList}" -- "$cur"))
COMPREPLY=( $(compgen -W "''${opts}" -- ''${cur}) )
return 0 return 0
;; ;;
esac esac
} }
complete -F _workshop_completion deploy browser connect complete -F _workshop_completion deploy browser connect
setup-traefik() { # Core Workshop Functions
echo "🔧 Setting up local Traefik proxy..." setup-traefik() {
echo "🔧 Setting up local Traefik proxy..."
# Ensure we can SSH to workshop.local first (tutorial requirement) # Test SSH capability (tutorial requirement)
if ! ssh -o ConnectTimeout=3 -o BatchMode=yes workshop@workshop.local echo "SSH OK" 2>/dev/null; then if ! timeout 3 ssh -o BatchMode=yes workshop@workshop.local echo "SSH OK" 2>/dev/null; then
echo " SSH to workshop.local not working, but continuing with local setup..." echo " SSH to workshop.local not working, continuing with local setup..."
fi fi
# DNS check # Verify DNS
if ! nslookup traefik.workshop.local 127.0.0.1 >/dev/null 2>&1; then if ! nslookup traefik.workshop.local 127.0.0.1 >/dev/null 2>&1; then
echo " DNS not resolving *.workshop.local" echo "🔄 Restarting DNS..."
sudo systemctl restart dnsmasq sudo systemctl restart dnsmasq
sleep 3 sleep 3
fi fi
# Docker Swarm + proxy network # Ensure Docker Swarm + proxy network
if ! docker info 2>/dev/null | grep -q "Swarm: active"; then if ! docker info 2>/dev/null | grep -q "Swarm: active"; then
echo "🔥 Initializing Docker Swarm..." echo "🔥 Initializing Docker Swarm..."
docker swarm init --advertise-addr 127.0.0.1 docker swarm init --advertise-addr 127.0.0.1
fi fi
if ! docker network ls | grep -q "proxy"; then if ! docker network ls | grep -q "proxy"; then
echo "📡 Creating proxy overlay network..." echo "🌐 Creating proxy network..."
docker network create -d overlay proxy docker network create -d overlay proxy
fi fi
# Add server (tutorial step) # Add server
if ! abra server ls 2>/dev/null | grep -q "workshop.local"; then if ! abra server ls 2>/dev/null | grep -q "workshop.local"; then
echo "🏗 Adding workshop.local server..." echo "🏗 Adding workshop.local server..."
# Try to add as proper domain first, fallback to --local abra server add workshop.local 2>/dev/null || abra server add --local
abra server add workshop.local 2>/dev/null || abra server add --local fi
fi
# Create Traefik app (tutorial step 1) # Create, configure, and deploy Traefik
if ! abra app ls 2>/dev/null | grep -q "traefik"; then if ! abra app ls 2>/dev/null | grep -q "traefik"; then
echo "🚀 Creating Traefik app..." echo "🚀 Creating Traefik app..."
abra app new traefik --domain=traefik.workshop.local abra app new traefik --domain=traefik.workshop.local
fi
# Configure Traefik (tutorial step 2) echo " Configuring Traefik..."
echo " Configuring Traefik..." abra app config traefik.workshop.local
abra app config traefik.workshop.local
# Deploy Traefik (tutorial step 3) echo "📦 Deploying Traefik..."
echo "📦 Deploying Traefik..." abra app deploy traefik.workshop.local
abra app deploy traefik.workshop.local
# Wait and verify echo " Waiting for Traefik..."
echo " Waiting for Traefik..." for i in {1..30}; do
for i in {1..30}; do if curl -s http://traefik.workshop.local >/dev/null 2>&1; then
if curl -s http://traefik.workshop.local >/dev/null 2>&1; then echo " Traefik ready! Dashboard: http://traefik.workshop.local"
echo " Traefik ready! Dashboard: http://traefik.workshop.local" return 0
return 0 fi
fi sleep 2
sleep 2 done
done
echo " Traefik may still be starting. Check: abra app logs traefik.workshop.local" echo " Traefik may still be starting. Check: abra app logs traefik.workshop.local"
} else
echo " Traefik already exists"
fi
}
deploy() { deploy() {
if [ -z "$1" ]; then if [[ -z "$1" ]]; then
echo "Usage: deploy <recipe>" echo "Usage: deploy <recipe>"
echo "Available recipes: $ALL_RECIPES" echo "Available: ${recipeList}"
return 1 return 1
fi fi
local recipe="$1" local recipe="$1"
local domain="$recipe.workshop.local" local domain="$recipe.workshop.local"
echo "🚀 Deploying $recipe locally..." echo "🚀 Deploying $recipe locally..."
echo "Domain: $domain" echo "Domain: $domain"
# Ensure Traefik is running first
# Ensure Traefik is running
if ! curl -s --max-time 3 http://traefik.workshop.local/ping >/dev/null 2>&1; then if ! curl -s --max-time 3 http://traefik.workshop.local/ping >/dev/null 2>&1; then
echo " Traefik not responding. Setting up..." echo " Traefik not responding. Setting up..."
setup-traefik || return 1 setup-traefik || return 1
fi fi
# Create and deploy app
echo "📦 Creating app: $recipe" echo "📦 Creating app: $recipe"
# Use correct server name abra app new "$recipe" --domain="$domain" --server=default 2>/dev/null || \
abra app new "$recipe" --domain="$domain" --server=default -S 2>/dev/null || \ abra app new "$recipe" --domain="$domain"
abra app new "$recipe" --domain="$domain" --server=default
echo "🚀 Deploying app: $domain" echo "🚀 Deploying: $domain"
abra app deploy "$domain" abra app deploy "$domain"
echo " Waiting for deployment..." echo " Waiting for deployment..."
for i in {1..60}; do for i in {1..60}; do
if curl -s --max-time 3 http://$domain >/dev/null 2>&1; then if curl -s --max-time 3 http://$domain >/dev/null 2>&1; then
@@ -397,7 +375,11 @@ isoConfig // {
} }
connect() { connect() {
[ -z "$1" ] && { echo "Usage: connect <name>"; echo "Available: ${builtins.concatStringsSep " " cloudServerNames}"; return 1; } if [[ -z "$1" ]]; then
echo "Usage: connect <name>"
echo "Available: ${serverList}"
return 1
fi
echo "🔌 Connecting to $1.codecrispi.es..." echo "🔌 Connecting to $1.codecrispi.es..."
ssh -o StrictHostKeyChecking=no workshop@$1.codecrispi.es ssh -o StrictHostKeyChecking=no workshop@$1.codecrispi.es
} }
@@ -405,15 +387,14 @@ isoConfig // {
browser() { browser() {
local target_url="about:blank" local target_url="about:blank"
if [ -n "$1" ]; then if [[ -n "$1" ]]; then
# Specific app requested
target_url="http://$1.workshop.local" target_url="http://$1.workshop.local"
echo "🌐 Opening $1 at $target_url" echo "🌐 Opening $1 at $target_url"
else else
echo "🌐 Opening Firefox browser" echo "🌐 Opening Firefox browser"
fi fi
if [ -n "$DISPLAY" ]; then if [[ -n "$DISPLAY" ]]; then
firefox "$target_url" & firefox "$target_url" &
else else
echo " No GUI session. Run 'desktop' first" echo " No GUI session. Run 'desktop' first"
@@ -424,111 +405,73 @@ isoConfig // {
recipes() { recipes() {
echo "📚 Complete Co-op Cloud Recipe Catalog:" echo "📚 Complete Co-op Cloud Recipe Catalog:"
echo "" echo ""
echo " Tier 1 - Production Ready (Score 5):" echo " Tier 1 - Production Ready: gitea mealie nextcloud"
echo " gitea mealie nextcloud" echo "🔧 Tier 2 - Stable: gotosocial wordpress"
echo "" echo "🧪 Tier 3 - Community: collabora croc dokuwiki ghost loomio..."
echo "🔧 Tier 2 - Stable (Score 4):" echo "🌐 Extended: matrix-synapse rocketchat gitlab n8n mastodon..."
echo " gotosocial wordpress"
echo ""
echo "🧪 Tier 3 - Community (Score 3):"
echo " collabora croc custom-php dokuwiki engelsystem"
echo " fab-manager ghost karrot lauti loomio mattermost"
echo " mattermost-lts mrbs onlyoffice open-inventory outline"
echo " owncast rallly"
echo ""
echo "🌐 Extended Catalog:"
echo " Content: hedgedoc mediawiki seafile"
echo " Chat: jitsi-meet matrix-synapse rocketchat"
echo " Business: prestashop invoiceninja kimai pretix"
echo " Dev Tools: drone n8n gitlab jupyter-lab"
echo " Analytics: plausible matomo uptime-kuma grafana"
echo " Media: peertube funkwhale mastodon pixelfed jellyfin"
echo "" echo ""
echo "🚀 Usage:" echo "🚀 Usage:"
echo " deploy <recipe> - Deploy locally" echo " deploy <recipe> - Deploy locally"
echo " browser <recipe> - Open app in browser" echo " browser <recipe> - Open in browser"
echo " 📖 Full catalog: https://recipes.coopcloud.tech" echo " 📖 Full catalog: https://recipes.coopcloud.tech"
echo "" echo ""
echo "💡 Use tab completion: type 'deploy <TAB>' or 'browser <TAB>'" echo "💡 Tab completion: deploy <TAB> or browser <TAB>"
} }
desktop() { desktop() {
echo "🖥 Starting GUI session..." echo "🖥 Starting GUI session..."
if command -v startx &> /dev/null; then if command -v startx >/dev/null 2>&1; then
if [ -z "$DISPLAY" ]; then if [[ -z "$DISPLAY" ]]; then
startx & startx &
export DISPLAY=:0 export DISPLAY=:0
sleep 3 sleep 3
echo " GUI started. Check QEMU window or run 'browser'" echo " GUI started"
else else
echo " GUI already running" echo " GUI already running"
fi fi
else else
echo "💡 GUI available in QEMU window (Alt+Tab to switch)" echo "💡 GUI available in QEMU window"
echo "🖱 Click on QEMU graphics window to use desktop"
fi fi
} }
abra-status() { abra-status() {
echo "🔍 Checking workshop-abra-setup service status..." echo "🔍 Workshop setup service status:"
systemctl status workshop-abra-setup systemctl status workshop-abra-setup --no-pager
echo "" echo ""
if [ -f /tmp/abra-install.log ]; then echo "💡 Commands: which abra | abra --version"
echo "📚 Last abra installation log (/tmp/abra-install.log):"
cat /tmp/abra-install.log
else
echo " No abra installation log found at /tmp/abra-install.log"
fi
echo ""
echo "💡 To check if abra is in your PATH: which abra"
echo "💡 To check abra version: abra --version"
} }
help() { help() {
echo "🚀 CODE CRISPIES Workshop Commands:" echo "🚀 CODE CRISPIES Workshop Commands:"
echo "" echo ""
echo "🏠 Local Development:" echo "🏠 Local Development:"
echo " setup-traefik - Setup local Traefik proxy (REQUIRED FIRST!)" echo " setup-traefik - Setup local proxy (REQUIRED FIRST!)"
echo " recipes - Show all available app recipes" echo " recipes - Show all available apps"
echo " deploy <recipe> - Deploy app locally (e.g., deploy wordpress)" echo " deploy <recipe> - Deploy app locally"
echo " browser [recipe] - Launch Firefox [to specific app]" echo " browser [recipe] - Launch Firefox [to app]"
echo " desktop - Start GUI desktop session" echo " desktop - Start GUI session"
echo " abra-status - Check the status of the abra setup service"
echo "" echo ""
echo " Cloud Access:" echo " Cloud Access:"
echo " connect <name> - SSH to cloud server (e.g., connect hopper)" echo " connect <name> - SSH to cloud server"
echo " Available: ${serverList}"
echo "" echo ""
echo "Available servers: ${builtins.concatStringsSep " " cloudServerNames}" echo "🔍 Debug:"
echo " abra-status - Check setup service"
echo " docker service ls - List running services"
echo " systemctl status dnsmasq - Check DNS"
echo "" echo ""
echo "📚 Learning Flow:" echo "📚 Learning Flow:"
echo " 1. First time: setup-traefik" echo " 1. setup-traefik"
echo " 2. Try local: recipes deploy wordpress browser wordpress" echo " 2. deploy wordpress"
echo " 3. Try cloud: connect hopper same abra commands" echo " 3. browser wordpress"
echo "" echo " 4. connect hopper"
echo "🔍 Debug Commands:"
echo " docker service ls - Check running services"
echo " dig @127.0.0.1 app.workshop.local - Test DNS resolution"
echo " systemctl status dnsmasq - Check DNS service"
echo ""
echo "💡 Tab completion available for deploy, browser, connect commands"
} }
''; '';
};
# GUI Configuration
services.xserver = { services.xserver = {
enable = true; enable = true;
desktopManager.xfce.enable = true; desktopManager.xfce.enable = true;
displayManager.lightdm.enable = true; displayManager.lightdm.enable = true;
}; };
# Don't auto-start GUI, let user choose
systemd.user.services.workshop-welcome = {
wantedBy = [ "default.target" ];
script = ''
echo "Welcome! Run 'desktop' to start GUI session"
'';
serviceConfig.Type = "oneshot";
};
} }