# NUC Server - Claude Code Instructions ## Server Access The NUC server is accessible via SSH: ```bash ssh nuc ``` **Connection Details:** - Hostname: `192.168.1.3` - User: `alezmad` - SSH Key: `~/.ssh/id_ed25519_nuc` ## Service Management ### Coolify (Primary Service Manager) All services are managed through Coolify at `http://192.168.1.3:8000` **Prefer using Coolify MCP** (`mcp__coolify__*`) for service management - it's faster and more reliable than SSH commands. ### ⚠️ STRICT RULE: Container Deployment Priority **ALWAYS attempt Coolify first when adding any container/service:** 1. **First:** Try `mcp__coolify__service(action="create", type="", ...)` 2. **If type invalid:** Deploy via docker-compose in Coolify using `docker_compose_raw` 3. **Last resort:** Direct Docker commands via SSH (only if Coolify can't handle it) ```python # Step 1: Try native Coolify service type mcp__coolify__service(action="create", type="servicename", name="ServiceName", server_uuid="qk84w0goo4w48g4ggsoo0oss", project_uuid="a8484ggc88c40w4g4k004ow0", environment_name="production", instant_deploy=True) # Step 2: If "Invalid service type", use docker-compose mcp__coolify__service(action="create", name="ServiceName", server_uuid="qk84w0goo4w48g4ggsoo0oss", project_uuid="a8484ggc88c40w4g4k004ow0", environment_name="production", docker_compose_raw=""" services: myservice: image: organization/image:tag restart: unless-stopped ports: - "8080:8080" volumes: - data:/app/data volumes: data: """, instant_deploy=True) ``` **Why Coolify first:** - Centralized management in one UI - Automatic restarts, health checks - Easy updates and rollbacks - Visible in infrastructure overview - Consistent with existing services ### ⚠️ STRICT RULE: Browser MCP for Manual Configurations **When a task requires manual web UI interaction (OAuth setup, API key generation, admin consoles), ALWAYS use Browser MCP instead of asking the user to do it manually.** **Priority order for browser automation:** 1. `mcp__playwriter-nuc-01__*` - Remote NUC browser (preferred, no local resources) 2. `mcp__chrome-devtools-nuc-01__*` - Chrome DevTools for NUC browser 3. `mcp__playwriter-local__*` - Local browser (fallback, uses local resources) **Browser MCP Naming Convention:** ``` playwriter-- chrome-devtools-- Examples: - playwriter-local → Local machine browser - playwriter-nuc-01 → First NUC browser container - playwriter-nuc-02 → Second NUC browser (future) - playwriter-cloud-01 → Cloud browser instance (future) ``` **Common use cases:** - Generating API keys/tokens (Tailscale, OAuth apps, etc.) - Configuring OAuth/OIDC providers - Admin console settings not available via API - Any "go to website and click" tasks ```python # Example: Navigate and interact mcp__chrome-devtools__navigate_page(type="url", url="https://admin.example.com") mcp__chrome-devtools__take_snapshot() # See current state mcp__chrome-devtools__click(uid="") mcp__chrome-devtools__fill(uid="", value="text") ``` **NEVER say "please go to X and do Y manually" - use browser MCP instead.** ### ⚠️ STRICT RULE: Parallel Subtasks for Multiple Operations **When multiple independent services, configurations, or entities need to be set up, ALWAYS use parallel Task agents instead of sequential operations.** ```python # WRONG - Sequential (slow) # Step 1: Configure Tailscale # Step 2: Configure WireGuard # Step 3: Add to Homepage # CORRECT - Parallel subtasks (fast) Task(subagent_type="general-purpose", prompt="Configure Tailscale auth key...", description="Setup Tailscale") Task(subagent_type="general-purpose", prompt="Install WireGuard on router...", description="Setup WireGuard") Task(subagent_type="general-purpose", prompt="Add services to Homepage...", description="Update Homepage") # All three run simultaneously! ``` **When to parallelize:** - Multiple service deployments - Multiple configuration changes across different systems - Independent API calls or browser automations - Any tasks that don't depend on each other's output **How to parallelize:** - Use multiple `Task` tool calls in a single message - Each task gets its own agent with full context - Results are collected when all complete ### Available MCPs for NUC Management | MCP | Purpose | |-----|---------| | `mcp__coolify__*` | Service management, deployments, env vars | | `mcp__ssh-manager__*` | Direct SSH commands, file transfers | | `mcp__n8n__*` | Workflow automation (if configured) | | `mcp__playwriter__*` | Browser automation fallback (see below) | ### ⚠️ STRICT RULE: MCP Research Protocol **When asked to find/recommend new MCPs**, read the detailed guide at `docs/mcp-research-guide.md` which contains: - MCP directories (MCP.so, Smithery, MCPHub, etc.) - GitHub verification resources - Evaluation criteria checklist - Research workflow - Category quick reference **Quick workflow:** 1. Search directories: `mcp.so`, `smithery.ai`, `mcpservers.org` 2. Verify on GitHub: stars, last commit, issues 3. Evaluate: 50+ stars, <6 months active, 3+ tools, OSS license ### Playwriter as Fallback When SSH, API endpoints, or other MCPs can't accomplish a task (e.g., no API available, UI-only settings), use **Playwriter MCP** to automate browser interactions: ```javascript // Navigate to service UI await page.goto('http://192.168.1.3:8000'); // Get page state console.log(await accessibilitySnapshot({ page })); // Interact with elements await page.locator('aria-ref=e5').click(); ``` **Use cases:** - Configuring services that lack APIs (Coolify UI settings, etc.) - Creating OAuth apps, API keys through web interfaces - Debugging issues by inspecting service dashboards - Any task where clicking through a UI is the only option ### Remote Browser Container (NUC) A dedicated browser container runs on the NUC for AI-controlled browsing without local resources: **Access:** - noVNC Web: `http://192.168.1.3:6081/vnc.html` - Playwriter Relay: `ws://192.168.1.3:19988` - Chrome DevTools: `http://192.168.1.3:9222` **MCP connects remotely via:** ```json { "playwriter-nuc-01": { "_id": "nuc-01", "_host": "192.168.1.3", "args": ["playwriter", "--host", "ws://192.168.1.3:19988", "--token", "nuc-browser-token"] } } ``` **First-time setup:** Access noVNC, install Playwriter extension, click to activate (turns green). **Container location:** `~/playwriter-browser/` on NUC (deployed via docker compose) **Coolify CLI Commands:** ```bash # Access Coolify's Laravel tinker for direct database/service manipulation ssh nuc "docker exec coolify php artisan tinker --execute=\"\"" # Restart a service (example for service ID 9 - Outline) ssh nuc "docker exec coolify php artisan tinker --execute=\" use App\Actions\Service\StartService; use App\Models\Service; \\\$service = Service::find(9); StartService::run(\\\$service); \"" # Update environment variable (encrypted) ssh nuc "docker exec coolify php artisan tinker --execute=\" use App\Models\EnvironmentVariable; \\\$var = EnvironmentVariable::where('key', 'VAR_NAME')->where('resourceable_id', SERVICE_ID)->first(); \\\$var->value = encrypt('new_value'); \\\$var->save(); \"" ``` ### Docker Commands ```bash # List all containers ssh nuc "docker ps -a --format '{{.Names}}\t{{.Status}}'" # View container logs ssh nuc "docker logs 2>&1 | tail -50" # Restart a container ssh nuc "docker restart " # Execute command in container ssh nuc "docker exec " ``` ## Services & Ports | Service | Port | URL | Container | |---------|------|-----|-----------| | Homepage | 3000 | http://192.168.1.3:3000 | homepage-* | | Coolify | 8000 | http://192.168.1.3:8000 | coolify | | Gitea | 3030 | http://192.168.1.3:3030 | gitea-* | | Outline | 3080 | http://192.168.1.3:3080 | outline-* | | n8n | 5678 | http://192.168.1.3:5678 | n8n-* | | Vaultwarden | 8222 | http://192.168.1.3:8222 | vaultwarden-* | | Ntfy | 8333 | http://192.168.1.3:8333 | ntfy-* | | MinIO Console | 9001 | http://192.168.1.3:9001 | minio-* | | MinIO API | 9000 | http://192.168.1.3:9000 | minio-* | | Authentik | 9090 | http://192.168.1.3:9090 | authentik-* | | FileBrowser | 8085 | http://192.168.1.3:8085 | filebrowser-* | | Adminer | 8088 | http://192.168.1.3:8088 | adminer | | Uptime Kuma | 3001 | http://192.168.1.3:3001 | uptime-kuma | | Kopia | 51515 | http://192.168.1.3:51515 | kopia | | Dozzle | 9999 | http://192.168.1.3:9999 | dozzle | ## Port Forwarding Some services use port forwarding containers (alpine/socat or nginx) to expose internal Coolify services: ```bash # Create a port forwarder for a Coolify service ssh nuc "docker run -d --name port-fwd- --network -p : alpine/socat tcp-listen:,fork,reuseaddr tcp-connect::" ``` ## Configuration Files **Homepage Config:** - Location: `/opt/homepage/config/` - Services: `/opt/homepage/config/services.yaml` **Coolify Data:** - Location: `/data/coolify/` ## Important Notes 1. **After Coolify redeploy**: Containers may be in "Created" state - manually start with `docker start ` 2. **Environment Variables in Coolify**: Are encrypted with Laravel encryption. Use `encrypt()` when updating. 3. **HSTS Issues**: Some services send HSTS headers. Use nginx proxy with `proxy_hide_header Strict-Transport-Security;` to strip them. 4. **Network Discovery**: Find container's network with `docker inspect --format '{{.NetworkSettings.Networks}}'` ## Troubleshooting ### Coolify MCP vs Direct Docker **Always verify Coolify status with Docker** - Coolify's status can lag behind actual container state: ```bash # Coolify may show "exited" but container is actually running ssh nuc "docker ps -a --format 'table {{.Names}}\t{{.Status}}' | grep " ``` ### Common Issues 1. **Containers stuck in "Created" state**: After Coolify restart/redeploy, containers may not auto-start ```bash ssh nuc "docker start " ``` 2. **Service shows "running:unknown"**: No healthcheck configured. Add one via Coolify service update: ```yaml healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:"] interval: 30s timeout: 10s retries: 3 start_period: 30s ``` 3. **Service dependencies not starting**: Services with `depends_on: condition: service_healthy` won't start until dependencies are healthy. Check dependency containers first. 4. **Stale database entries in Coolify**: Coolify may have database/service entries with no corresponding container. Safe to delete if container doesn't exist: ```bash # Verify container doesn't exist ssh nuc "docker ps -a | grep " # Then delete via Coolify MCP or UI ``` 5. **Embedded vs Standalone databases**: Services like Outline and Authentik have their own PostgreSQL containers (e.g., `postgres-pccg80...`) bundled in the service compose. These are separate from standalone Coolify databases. 6. **Wrong healthcheck endpoint**: Some services use `/healthz` instead of `/`. Verify with: ```bash ssh nuc "docker exec wget -qO- http://127.0.0.1:/healthz" ``` 7. **Creating API keys when no UI available** (e.g., n8n): ```bash # Stop container, insert directly into SQLite, restart ssh nuc "docker run --rm -v :/data keinos/sqlite3 sqlite3 /data/database.sqlite \"\"" ``` ### Coolify MCP Quick Reference ```python # Check infrastructure overview mcp__coolify__get_infrastructure_overview() # Start/stop/restart service mcp__coolify__control(resource="service", action="start|stop|restart", uuid="") # Get service details (including docker_compose) mcp__coolify__get_service(uuid="") # Update service config (e.g., add healthcheck) mcp__coolify__service(action="update", uuid="", docker_compose_raw="") # Delete stale database mcp__coolify__database(action="delete", uuid="", delete_volumes=True) ``` ## OpenWrt Router The network is managed by an OpenWrt router at `192.168.1.1`. ### SSH Access ```bash # Connect to router ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 # Or via NUC as jump host ssh nuc "ssh root@192.168.1.1 ''" ``` **Router Details:** - IP: `192.168.1.1` - User: `root` - SSH Key: `~/.ssh/id_ed25519_nuc` - Firmware: OpenWrt 23.05.0 - Architecture: ARM Cortex-A9 (mvebu/cortexa9) - LuCI Web UI: `http://192.168.1.1` ### OpenWrt MCP Server An MCP server runs on the router for AI integration: - **Location:** `/opt/mcp-server/openwrt-mcp-server` - **Config:** `/opt/mcp-server/config.toml` - **HTTP API:** `http://192.168.1.1:8090` - **API Token:** `openwrt-mcp-secret-2026` - **Init Script:** `/etc/init.d/mcp-server` ```bash # Control MCP server ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "/etc/init.d/mcp-server start|stop|restart" ``` ### Common Router Tasks **Port Forwarding:** ```bash # List current port forwards ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "uci show firewall | grep redirect" # Add port forward ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 " uci add firewall redirect uci set firewall.@redirect[-1].name='' uci set firewall.@redirect[-1].src='wan' uci set firewall.@redirect[-1].src_dport='' uci set firewall.@redirect[-1].dest='lan' uci set firewall.@redirect[-1].dest_ip='' uci set firewall.@redirect[-1].dest_port='' uci set firewall.@redirect[-1].proto='tcp udp' uci set firewall.@redirect[-1].target='DNAT' uci commit firewall /etc/init.d/firewall restart " ``` **Firewall Rules:** ```bash # Show firewall zones ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "uci show firewall | grep zone" # Allow traffic from WAN to specific port ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 " uci add firewall rule uci set firewall.@rule[-1].name='Allow-' uci set firewall.@rule[-1].src='wan' uci set firewall.@rule[-1].dest_port='' uci set firewall.@rule[-1].proto='tcp' uci set firewall.@rule[-1].target='ACCEPT' uci commit firewall /etc/init.d/firewall restart " ``` **DNS/DHCP (dnsmasq):** ```bash # View DNS/DHCP config ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "uci show dhcp" # Force DNS cache refresh ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "/etc/init.d/dnsmasq restart" # Add static DHCP lease ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 " uci add dhcp host uci set dhcp.@host[-1].name='' uci set dhcp.@host[-1].mac='' uci set dhcp.@host[-1].ip='' uci commit dhcp /etc/init.d/dnsmasq restart " # Add custom DNS entry ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 " uci add dhcp domain uci set dhcp.@domain[-1].name='' uci set dhcp.@domain[-1].ip='' uci commit dhcp /etc/init.d/dnsmasq restart " ``` **Network Diagnostics:** ```bash # Check WAN status ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "ifstatus wan" # View connected clients ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "cat /tmp/dhcp.leases" # Check routing table ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "ip route" # View system logs ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "logread | tail -50" ``` **Package Management (opkg):** ```bash # Update package lists ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "opkg update" # Install package ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "opkg install " # List installed packages ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "opkg list-installed" ``` **Browser Automation (Chrome DevTools MCP):** When SSH commands aren't sufficient, use Chrome DevTools MCP to automate LuCI: ```python # Navigate to router UI mcp__chrome-devtools__navigate_page(type="url", url="http://192.168.1.1/cgi-bin/luci/admin/...") # Take snapshot of UI state mcp__chrome-devtools__take_snapshot() # Interact with elements by uid mcp__chrome-devtools__click(uid="") mcp__chrome-devtools__fill(uid="", value="") ``` ## Authentication **Outline OIDC (via Gitea):** - Client ID: `249a3a1d-92d4-47d8-b4a9-81c64e1da6ab` - Auth URL: `http://192.168.1.3:3030/login/oauth/authorize` - Token URL: `http://192.168.1.3:3030/login/oauth/access_token` - Userinfo URL: `http://192.168.1.3:3030/login/oauth/userinfo` ## Public Access & Security Architecture **Full architecture details:** `docs/architecture.md` ### Why Tailscale Funnel (Not Cloudflare) Cloudflare shared IPs get blocked by Spanish ISPs during LaLiga matches. Tailscale Funnel: - Uses different IP infrastructure (not blocked) - Handles dynamic ISP IP changes automatically - No ports exposed on router - HTTPS termination included ### Tailscale Funnel (Public Access) | Property | Value | |----------|-------| | **Funnel URL** | `https://nuc-tailscale.tail58f5ad.ts.net` | | **Tailscale IP** | `100.x.x.x` (stable, never changes) | | **Status** | `ssh nuc "tailscale funnel status"` | **Start Funnel for a service:** ```bash # Expose port 3000 via Funnel ssh nuc "tailscale funnel 3000" # Or with background (use screen/tmux) ssh nuc "screen -dmS funnel tailscale funnel 3000" ``` ### Current Domain Routes | Domain | Destination | Method | |--------|-------------|--------| | whyrating.com | `nuc-tailscale.tail58f5ad.ts.net` | Namecheap 301 redirect | ### Adding a New Domain 1. **Check availability** (if needed): ```python mcp__namecheap__namecheap_check_domain_availability(domains=["example.com"]) ``` 2. **Configure redirect at registrar** (Namecheap): - Go to Domain List → Manage → Redirect Domain - Source: `example.com` - Destination: `https://nuc-tailscale.tail58f5ad.ts.net` - Type: Permanent (301) 3. **Start Funnel** on NUC for the target port ### Security Layers ``` Internet → Tailscale Funnel (HTTPS) → CrowdSec → Traefik → Container ↓ Blocks malicious IPs ``` | Layer | Purpose | |-------|---------| | **Tailscale Funnel** | Only entry point, HTTPS termination | | **CrowdSec** | DDoS protection, threat intelligence | | **Traefik** | Domain routing, rate limiting | | **Docker Networks** | Container isolation | ### Tailscale Mesh (Admin Access) Private encrypted access from anywhere: | From | Command | |------|---------| | **Remote SSH** | `ssh nuc-tailscale` | | **Remote Coolify** | `http://nuc-tailscale:8000` | | **Home network** | `ssh nuc` or `http://192.168.1.3:8000` | **NOT exposed to internet:** SSH (22), Coolify (8000), databases, MinIO, Authentik, router admin. ### Dynamic IP Handling ISP can change your public IP anytime. Tailscale handles this automatically: - Tailscale IP (100.x.x.x) stays stable - Funnel URL stays stable - Domain redirects stay stable - Tunnels auto-reconnect in ~10-30 seconds ## Artifacts Folder **Location:** `.artifacts/` This folder stores important information artifacts that should be preserved for future reference. Claude should proactively save artifacts here when relevant information is generated during sessions. ### Naming Convention Files must use datetime-prefixed names: ``` YYYY-MM-DD_HH-MM_.md ``` Examples: - `2026-02-01_13-45_coolify-api-token.md` - `2026-02-01_14-30_n8n-mcp-setup.md` - `2026-02-01_15-00_service-health-report.md` ### When to Save Artifacts **Always save artifacts for:** - API tokens, keys, or credentials generated during sessions - Configuration changes made to services - Troubleshooting steps that resolved issues - Infrastructure changes or deployments - MCP server configurations and setup details - Database schema changes or migrations - Important command outputs that may be needed later - Service health reports or diagnostics ### Artifact Format ```markdown # **Date:** YYYY-MM-DD HH:MM **Context:** <Brief description of what was being done> ## Details <Relevant information, configs, tokens, commands, etc.> ## Related - <Links to services, docs, or other artifacts> ``` ## OpenWrt Interaction Methods (Quick Reference) | Method | When to Use | Example | |--------|-------------|---------| | **SSH** | Direct commands, config changes, package management | `ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "uci show network"` | | **OpenWrt MCP** | AI-driven automation, status queries | `curl -H "x-api-token: openwrt-mcp-secret-2026" http://192.168.1.1:8090/status` | | **Chrome DevTools MCP** | LuCI UI automation when no CLI/API exists | `mcp__chrome-devtools__navigate_page(url="http://192.168.1.1/...")` | | **LuCI Web UI** | Manual configuration, visual inspection | `http://192.168.1.1` (user: root) | **Priority Order:** SSH > OpenWrt MCP > Chrome DevTools > Manual UI ## Next Steps / Migration Candidates ### Priority 1: Safe to Delete (Duplicates/Old Versions) | Image | Size | Action | Reason | |-------|------|--------|--------| | `google-reviews-scraper-pro-api` (old) | 3.62GB | ❌ DELETE | Old version, newer exists | | `claudefarm-claudefarm` | 3.87GB | ❌ DELETE | Replaced by claudefarm-browser + claudefarm-api | | `postgres:16` | 657MB | ❌ DELETE | Using `16-alpine` (389MB) instead | | `prom/mysqld-exporter:v0.14.0` | 28MB | ❌ DELETE | 3 years old, likely unused | **Savings: ~8.2GB** ### Priority 2: Migrate to NUC (High Value) | Image | Size | Priority | Notes | |-------|------|----------|-------| | `nocodb/nocodb` | 1.24GB | ⭐ HIGH | Airtable alternative - great for self-hosted data | | `grafana/grafana` | 932MB | ⭐ HIGH | Pairs with existing Uptime Kuma for monitoring | | `prom/prometheus` | 479MB | ⭐ HIGH | Metrics backend for Grafana | | `timescale/timescaledb` | 1.45GB | ⭐ HIGH | Time-series data, useful for IoT/metrics | ### Priority 3: Migrate to NUC (Medium Value) | Image | Size | Priority | Notes | |-------|------|----------|-------| | `mysql:8` | 1.07GB | 🔶 MEDIUM | Only if you have MySQL-specific apps | | `minio/minio + minio/mc` | 340MB | 🔶 SKIP | Already running on NUC via Coolify | ### Priority 4: MCP Tools - Evaluate Usage | Image | Size | Recommendation | Notes | |-------|------|----------------|-------| | `mcp/n8n` | 675MB | 🔶 SKIP | n8n already on NUC; this is just MCP wrapper | | `mcp/youtube-transcript` | 321MB | ✅ KEEP LOCAL | Useful for AI workflows | | `mcp/context7` | 423MB | ✅ KEEP LOCAL | Documentation lookup, AI essential | | `mcp/fetch` | ? | ✅ KEEP LOCAL | Web fetching for AI | ### Priority 5: Review Before Deleting | Image | Size | Action | Why Review | |-------|------|--------|------------| | `mysql:8` | 1.07GB | ⚠️ CHECK | May have local databases; verify before delete | | `timescale/timescaledb` | 1.45GB | ⚠️ CHECK | May have local time-series data | ### Recommended Coolify Deployments ```bash # 1. NocoDB (Airtable alternative) mcp__coolify__service(action="create", type="nocodb", name="NocoDB", server_uuid="qk84w0goo4w48g4ggsoo0oss", project_uuid="a8484ggc88c40w4g4k004ow0", environment_name="production", instant_deploy=True) # 2. Prometheus + Grafana stack mcp__coolify__service(action="create", type="grafana", ...) mcp__coolify__service(action="create", type="prometheus", ...) ``` ### Migration Checklist - [ ] Delete old/duplicate images locally - [ ] Deploy NocoDB to NUC - [ ] Deploy Grafana + Prometheus monitoring stack - [ ] Consider TimescaleDB if IoT/metrics needed - [ ] Verify MySQL data before deleting - [ ] Add CloudBeaver to Uptime Kuma monitoring - [ ] Configure OpenWrt MCP MQTT broker (optional)