# NUC Server - Claude Code Instructions ## Server Access The NUC server is accessible via SSH: ```bash ssh nuc ``` **Connection Details:** - Hostname: `192.168.1.3` (local) or `100.113.153.45` (Tailscale) - User: `alezmad` - SSH Key: `~/.ssh/id_ed25519_nuc` ## DNS & Tailscale Setup ### Why Tailscale IP for DNS All `.nuc.lan` domains resolve to the **Tailscale IP** (`100.113.153.45`) instead of the local IP (`192.168.1.3`). This ensures services work from **anywhere** regardless of your current network's subnet. **Problem solved:** When connecting from a remote network that also uses `192.168.x.x`, traffic to `192.168.1.3` stays local instead of going through Tailscale. Using Tailscale IP (`100.x.x.x`) avoids this conflict. ### Configured Domains (OpenWrt Router DNS) | Domain | Resolves To | Service | |--------|-------------|---------| | `nuc.lan` | `100.113.153.45` | NUC Portal | | `nuc.local` | `100.113.153.45` | NUC Portal | | `coolify.nuc.lan` | `100.113.153.45` | Coolify | | `gitea.nuc.lan` | `100.113.153.45` | Gitea | | `outline.nuc.lan` | `100.113.153.45` | Outline Wiki | | `files.nuc.lan` | `100.113.153.45` | FileBrowser | | `mail.nuc.lan` | `100.113.153.45` | Snappymail | | `vault.nuc.lan` | `100.113.153.45` | Vaultwarden | | `homepage.nuc.lan` | `100.113.153.45` | NUC Portal | | `brand.nuc.lan` | `100.113.153.45` | Whyrating Brand | | `templates.nuc.lan` | `100.113.153.45` | Whyrating Templates | | `whyrating.nuc.lan` | `100.113.153.45` | Whyrating Hub | ### Traefik Routing (Dynamic Config) Traefik routes domain-based requests to the correct backend. Config location: `/data/coolify/proxy/dynamic/nuc-services.yaml` ```yaml # Routes for port-based services via domain names http: routers: coolify: rule: Host(`coolify.nuc.lan`) service: coolify services: coolify: loadBalancer: servers: - url: http://host.docker.internal:8000 ``` ### Adding a New Domain ```bash # 1. Add DNS entry on router (via NUC jump host) ssh nuc "ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 ' uci add dhcp domain uci set dhcp.@domain[-1].name=\"newservice.nuc.lan\" uci set dhcp.@domain[-1].ip=\"100.113.153.45\" uci commit dhcp /etc/init.d/dnsmasq restart '" # 2. Add Traefik route (if needed for port-based service) # Edit /data/coolify/proxy/dynamic/nuc-services.yaml ``` ### Always-On Tailscale **Keep Tailscale running** - it's designed to be always-on: - When on home network: Uses direct connection (no relay, same performance as local) - When remote: Routes through Tailscale mesh - Minimal resource usage (~0% CPU when idle) ## Service Management ### Coolify (Primary Service Manager) All services are managed through Coolify at `http://coolify.nuc.lan` (or `http://100.113.153.45: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__stalwart-mail__*` | Email server management (users, domains, queue) | | `mcp__email-client__*` | Read/send emails via IMAP/SMTP (see below) | | `mcp__nocodb__*` | Database operations, table management | | `mcp__ssh-manager__*` | Direct SSH commands, file transfers | | `mcp__n8n__*` | Workflow automation (if configured) | | `mcp__playwriter__*` | Browser automation fallback (see below) | ### Stalwart Mail MCP (Quick Guide) **Location:** `~/mcp-servers/stalwart-mail/` Manage the self-hosted Stalwart mail server via natural language. **Available Tools:** | Category | Tools | |----------|-------| | **Users** | `list_users`, `get_user`, `create_user`, `update_user_password`, `delete_user`, `add_email_alias` | | **Domains** | `create_domain`, `generate_dkim` | | **Queue** | `list_queue`, `get_queue_status`, `delete_queued_message`, `retry_queued_message` | | **Monitoring** | `get_metrics`, `get_dmarc_reports`, `get_server_logs` | | **DNS** | `check_dns_records`, `troubleshoot_delivery` | | **Spam** | `train_spam`, `train_ham`, `update_spam_filter` | **Usage Examples:** ``` "List all mail users" "Create user sales with email sales@whyrating.com and password Secret123" "Check the mail queue" "Verify DNS records for whyrating.com" "Show server metrics" "Delete user john" ``` **Direct API Test (if MCP not responding):** ```bash curl -s -u "admin:QfKYjCJdxu" "http://192.168.1.3:8081/api/principal" | jq . ``` **Reconfigure MCP:** ```bash claude mcp remove stalwart-mail claude mcp add stalwart-mail \ -e STALWART_URL=http://192.168.1.3:8081 \ -e STALWART_USER=admin \ -e STALWART_PASS=QfKYjCJdxu \ --scope user \ -- ~/mcp-servers/stalwart-mail/.venv/bin/python ~/mcp-servers/stalwart-mail/server.py ``` **⚠️ SMTP Authentication Requirements:** 1. **Password format:** Must be SHA-512 hashed (not plaintext). When creating users via API: ```python import crypt hashed = crypt.crypt('password', crypt.mksalt(crypt.METHOD_SHA512)) # Use hashed value in 'secrets' field ``` 2. **SMTP login:** Use username only (e.g., `info`), NOT full email (`info@whyrating.com`) 3. **Port 465 (SMTPS):** Supports PLAIN/LOGIN auth with implicit TLS 4. **Port 587 (Submission):** Requires STARTTLS, only OAuth supported without TLS **Send email via Python (from NUC):** ```python import smtplib, ssl from email.mime.text import MIMEText context = ssl.create_default_context() context.check_hostname = False context.verify_mode = ssl.CERT_NONE with smtplib.SMTP_SSL('localhost', 465, context=context) as server: server.login('info', 'whyrating2026') # Username only! server.sendmail('info@whyrating.com', 'recipient@example.com', msg.as_string()) ``` ### Email Client MCP (Read/Send Emails) **Package:** [mcp-email-server](https://github.com/ai-zerolab/mcp-email-server) Read and send emails via IMAP/SMTP directly from Claude. **Configured for:** `info@whyrating.com` on Stalwart **Usage Examples:** ``` "Check my inbox" "Read the latest email" "Send an email to john@example.com with subject Hello" "Search emails from support@" "List email folders" ``` **Reconfigure:** ```bash claude mcp remove email-client claude mcp add email-client \ -e MCP_EMAIL_SERVER_EMAIL_ADDRESS=info@whyrating.com \ -e MCP_EMAIL_SERVER_PASSWORD=whyrating2026 \ -e MCP_EMAIL_SERVER_IMAP_HOST=192.168.1.3 \ -e MCP_EMAIL_SERVER_IMAP_PORT=143 \ -e MCP_EMAIL_SERVER_SMTP_HOST=192.168.1.3 \ -e MCP_EMAIL_SERVER_SMTP_PORT=587 \ -e MCP_EMAIL_SERVER_SMTP_VERIFY_SSL=false \ -e MCP_EMAIL_SERVER_ENABLE_ATTACHMENT_DOWNLOAD=true \ --scope user \ -- uvx mcp-email-server@latest stdio ``` ### Adding Remote MCP Servers (HTTP Transport) **Use `claude mcp add --transport http` for remote MCP endpoints** - this is the recommended method for services with native MCP support. ```bash # Basic syntax claude mcp add --transport http --scope user --header "
" # Example: NocoDB MCP (globally available) claude mcp add --transport http nocodb http://192.168.1.3:8084/mcp/ncnyir1cy6n9bf5p \ --scope user \ --header "xc-mcp-token: qjjAXRxuYzRtEn-cA4lbPFi5km_pojTX" ``` **Scope options:** - `--scope user` - Available across all projects (stored in `~/.claude.json`) - `--scope local` - Current project only (default) - `--scope project` - Shared via `.mcp.json` (committed to repo) **Why CLI over JSON config:** - JSON config with `mcp-remote` often fails to load tools - CLI `--transport http` handles HTTP endpoints natively - No need for `--allow-http` flag or other workarounds **Managing MCP servers:** ```bash claude mcp list # List all configured servers claude mcp get nocodb # Get details for specific server claude mcp remove nocodb # Remove a server /mcp # Check status in Claude Code ``` ### ⚠️ 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 **Preferred access via domain names** (works from anywhere via Tailscale): | Service | Domain | Port-based URL | Container | |---------|--------|----------------|-----------| | NUC Portal | `http://nuc.lan` | - | nuc-portal-* | | Coolify | `http://coolify.nuc.lan` | `http://100.113.153.45:8000` | coolify | | Gitea | `http://gitea.nuc.lan` | `http://100.113.153.45:3030` | gitea-* | | Outline | `http://outline.nuc.lan` | `http://100.113.153.45:3080` | outline-* | | FileBrowser | `http://files.nuc.lan` | `http://100.113.153.45:8085` | filebrowser-* | | Snappymail | `http://mail.nuc.lan` | `http://100.113.153.45:8082` | snappymail-* | | Vaultwarden | `http://vault.nuc.lan` | `http://100.113.153.45:8222` | vaultwarden-* | | Homepage | `http://homepage.nuc.lan` | `http://100.113.153.45:3000` | nuc-portal-* | | NocoDB | - | `http://100.113.153.45:8084` | nocodb-* | | n8n | - | `http://100.113.153.45:5678` | n8n-* | | Ntfy | - | `http://100.113.153.45:8333` | ntfy-* | | MinIO Console | - | `http://100.113.153.45:9001` | minio-* | | MinIO API | - | `http://100.113.153.45:9000` | minio-* | | Authentik | - | `http://100.113.153.45:9090` | authentik-* | | Adminer | - | `http://100.113.153.45:8088` | adminer | | Uptime Kuma | - | `http://100.113.153.45:3001` | uptime-kuma | | Kopia | - | `http://100.113.153.45:51515` | kopia | | Dozzle | - | `http://100.113.153.45:9999` | dozzle | | CloudBeaver | - | `http://100.113.153.45:8978` | cloudbeaver-* | **Note:** Use Tailscale IP (`100.113.153.45`) instead of `192.168.1.3` to avoid subnet conflicts when remote. ## 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` ## ⚠️ Critical Credentials & Access ### CloudBeaver (Database Manager) | Property | Value | |----------|-------| | **URL** | `http://192.168.1.3:8978` | | **Admin User** | `cbadmin` | | **Admin Password** | `CloudBeaver2026!` | | **Service UUID** | `joo4g4k0w08k8kcosgsgswc0` | **Pre-configured connections:** 9 databases across 3 folders. Turbostarter DB is now in service `v4gogwwc8wkk4888ksscc4k4` (container: `db-v4gogwwc8wkk4888ksscc4k4`). Connected to 7 Docker networks for direct container-to-container access. ### Vaultwarden (Password Manager) **⚠️ CRITICAL: Vaultwarden REQUIRES HTTPS** - The Web Crypto API needs a secure context for client-side encryption. HTTP access will NOT work (blank page/loading forever). | Property | Value | |----------|-------| | **HTTPS URL** | `https://nuc-tailscale.tail58f5ad.ts.net:8443` | | **HTTP URL** | `http://192.168.1.3:8222` (won't load - HTTPS required) | | **Admin Email** | `admin@nuc.lan` | | **Admin Password** | `NucVault2026!Secure` | **Access via Tailscale Funnel:** ```bash # Vaultwarden is exposed on port 8443 via Tailscale Funnel open "https://nuc-tailscale.tail58f5ad.ts.net:8443" ``` ### Stalwart Mail Server | Property | Value | |----------|-------| | **Admin URL** | `http://192.168.1.3:8081` | | **Username** | `admin` | | **Password** | `QfKYjCJdxu` | | **Webmail (Snappymail)** | `http://192.168.1.3:8082` | | **Service UUID** | `kw00kok0w0s8gcok008gk04k` | | **MCP Server** | `mcp__stalwart-mail__*` (see Quick Guide above) | **Mail Users:** | Email | Username | Password | |-------|----------|----------| | `info@whyrating.com` | `info` | `whyrating2026` | **DNS Records Configured:** SPF, DKIM (Ed25519 + RSA), DMARC, MX for `whyrating.com` **Container Status:** Running ### Gitea Users (for Outline OIDC) | Username | Password | Notes | |----------|----------|-------| | `nedas` | `NedasNUC2026!` | Regular user account | ## Gitea-Coolify Integration (Git Auto-Deploy) Deploy Next.js apps from self-hosted Gitea with auto-deploy on push. Full docs: `docs/gitea-coolify-auto-deploy.md` ### ⚠️ CRITICAL: Gitea Webhook Allowed Hosts **Gitea blocks webhooks to internal hosts by default!** Before setting up webhooks, configure `ALLOWED_HOST_LIST` in Gitea's app.ini: ```bash # Edit Gitea config ssh nuc "docker exec gitea-ho0cwgcwos88cwc48g84c0g8 vi /data/gitea/conf/app.ini" # Add/modify [webhook] section: [webhook] ALLOWED_HOST_LIST = coolify,10.0.1.5,192.168.1.3,localhost,host.docker.internal,external # Restart Gitea ssh nuc "docker restart gitea-ho0cwgcwos88cwc48g84c0g8" ``` **Without this, webhooks fail with:** `webhook can only call allowed HTTP servers` ### Key References | Resource | Value | |----------|-------| | **Deploy Key UUID** | `akssgwowsccgwgoggs4ks8ck` | | **Gitea Container** | `gitea-ho0cwgcwos88cwc48g84c0g8` | | **Webhook Secret** | `9eb07a77964563378c5d66d99006e06ba3da39d232905d4b12554ff91ca39718` | ### ⚠️ CRITICAL: Webhook Secret Must Be Set in BOTH Places **Auto-deploy won't work unless the webhook secret is configured in BOTH Coolify AND Gitea!** **Step 1: Set secret in Coolify** (via tinker or when creating app): ```bash ssh nuc "docker exec coolify php artisan tinker --execute=\" use App\\Models\\Application; \\\$app = Application::where('uuid', '')->first(); \\\$app->manual_webhook_secret_gitea = '9eb07a77964563378c5d66d99006e06ba3da39d232905d4b12554ff91ca39718'; \\\$app->save(); \"" ``` **Step 2: Set secret in Gitea webhook**: 1. Go to repo → Settings → Webhooks → Add/Edit Webhook 2. Target URL: `http://coolify:8080/webhooks/source/gitea/events/manual?uuid=` 3. Secret: `9eb07a77964563378c5d66d99006e06ba3da39d232905d4b12554ff91ca39718` 4. Trigger: Push Events 5. Active: ✓ **Common symptom when secret is missing:** Git pushes succeed but no deployment is triggered. Check: ```bash # Verify Coolify has the secret ssh nuc "docker exec coolify php artisan tinker --execute=\" use App\\Models\\Application; \\\$app = Application::where('uuid', '')->first(); echo 'Secret: ' . (\\\$app->manual_webhook_secret_gitea ? 'SET' : 'MISSING'); \"" ``` ### Local Development (Clone & Push) SSH config is set up for direct git operations: ```bash # Clone a repo git clone gitea:alezmad/nuc-portal.git # Push changes (triggers auto-deploy via webhook) git push origin main ``` **SSH Host Config** (in `~/.ssh/config`): ``` Host gitea HostName 192.168.1.3 Port 22222 User git IdentityFile ~/.ssh/id_ed25519_nuc ``` ### Webhook URL Format (MUST include UUID and use port 8080!) ``` http://coolify:8080/webhooks/source/gitea/events/manual?uuid= ``` **⚠️ Port 8080 is required** - Coolify listens on 8080 internally (not 8000, which is the external mapping). | App | UUID | Webhook URL | |-----|------|-------------| | nuc-portal | `t80w0cw0oooc4g0soswos4so` | `...?uuid=t80w0cw0oooc4g0soswos4so` | | whyrating-hub | `vw4ggc40socwkgwg4osc8wg8` | `...?uuid=vw4ggc40socwkgwg4osc8wg8` | | whyrating-brand | `r80gk0ccgg0okos8cw848kkk` | `...?uuid=r80gk0ccgg0okos8cw848kkk` | | whyrating-templates | `qw80g4sog0kk8cc4wkcs8sgc` | `...?uuid=qw80g4sog0kk8cc4wkcs8sgc` | ### Quick Deploy (Next.js) ```python # 1. Create application with deploy key mcp__coolify__application( action="create_key", name="my-app", project_uuid="a8484ggc88c40w4g4k004ow0", environment_name="production", server_uuid="qk84w0goo4w48g4ggsoo0oss", git_repository="git@gitea-ho0cwgcwos88cwc48g84c0g8:alezmad/.git", git_branch="main", build_pack="nixpacks", ports_exposes="3000", private_key_uuid="akssgwowsccgwgoggs4ks8ck" ) # 2. Set FQDN ssh nuc "docker exec coolify php artisan tinker --execute=\" use App\Models\Application; \\\$app = Application::where('uuid', '')->first(); \\\$app->fqdn = 'http://.nuc.lan'; \\\$app->custom_labels = null; \\\$app->save(); \"" # 3. Set webhook secret ssh nuc "docker exec coolify php artisan tinker --execute=\" use App\Models\Application; \\\$app = Application::where('uuid', '')->first(); \\\$app->manual_webhook_secret_gitea = '9eb07a77964563378c5d66d99006e06ba3da39d232905d4b12554ff91ca39718'; \\\$app->save(); \"" # 4. Create webhook in Gitea (via browser or API): # URL: http://coolify:8080/webhooks/source/gitea/events/manual?uuid= # (Use port 8080, NOT 8000 - 8080 is the internal container port) # Secret: 9eb07a77964563378c5d66d99006e06ba3da39d232905d4b12554ff91ca39718 # Trigger: Push Events # 5. Deploy mcp__coolify__deploy(tag_or_uuid="") ``` ### Deploy Key (add to each new repo) ``` ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGHtsL3jicJTsBekYuwbKjO0EcRadYKhvLSUw/36XF7h coolify-gitea ``` Add via Gitea: Repository → Settings → Deploy Keys → **Enable Write Access** ✓ ### ✅ New Repo Auto-Deploy Checklist When creating a new repo that should auto-deploy: 1. **[ ] Add deploy key to Gitea repo** - Go to: `http://192.168.1.3:3030/alezmad//settings/keys` - Add the deploy key above with **Write Access** enabled 2. **[ ] Create Coolify application** (use `mcp__coolify__application` with `action="create_key"`) 3. **[ ] Set FQDN** via tinker command 4. **[ ] Set webhook secret** via tinker command (use shared secret above) 5. **[ ] Create Gitea webhook** - Go to: `http://192.168.1.3:3030/alezmad//settings/hooks` - Add Webhook → Gitea - **URL:** `http://coolify:8080/webhooks/source/gitea/events/manual?uuid=` - **Secret:** `9eb07a77964563378c5d66d99006e06ba3da39d232905d4b12554ff91ca39718` - **Trigger:** Push Events - **Active:** ✓ 6. **[ ] Test webhook** - Click "Test Delivery" and verify HTTP 200 response 7. **[ ] Initial deploy** - `mcp__coolify__deploy(tag_or_uuid="")` **Common mistakes:** - ❌ Using port 8000 instead of 8080 in webhook URL - ❌ Forgetting `?uuid=` in webhook URL - ❌ Not enabling Write Access on deploy key ### Quick Verification Commands ```bash # Check Gitea has [webhook] section configured ssh nuc "docker exec gitea-ho0cwgcwos88cwc48g84c0g8 cat /data/gitea/conf/app.ini | grep -A2 '\[webhook\]'" # Test Coolify is reachable from Gitea (should return HTML) ssh nuc "docker exec gitea-ho0cwgcwos88cwc48g84c0g8 wget -qO- --timeout=5 http://coolify:8080/ | head -5" # Check Gitea is on coolify network ssh nuc "docker inspect gitea-ho0cwgcwos88cwc48g84c0g8 --format '{{json .NetworkSettings.Networks}}' | jq -r 'keys[]'" ``` ### Why NOT "Gitea Source" Coolify's "Gitea Source" uses GitHub App-style OAuth with JWT - **this doesn't work with Gitea**. Use deploy keys + manual webhooks instead. ### Current Deployed Apps | App | URL | Repository | UUID | |-----|-----|------------|------| | nuc-portal | http://nuc.lan | `alezmad/nuc-portal` | `t80w0cw0oooc4g0soswos4so` | | whyrating-hub | http://whyrating.nuc.lan | `alezmad/whyrating-hub` | `vw4ggc40socwkgwg4osc8wg8` | | whyrating-brand | http://brand.nuc.lan | `alezmad/whyrating-brand` | `r80gk0ccgg0okos8cw848kkk` | | whyrating-templates | http://templates.nuc.lan | `alezmad/whyrating-templates` | `qw80g4sog0kk8cc4wkcs8sgc` | | turbostarter | https://alezmad-nuc.tail58f5ad.ts.net | `alezmad/turbostarter` | `v4gogwwc8wkk4888ksscc4k4` (service) | ### Turbostarter (Knosia) - Build & Deploy Turbostarter is deployed as a **Coolify Service** (not a standalone app) with full docker-compose infrastructure: web + pgvector + minio. **Architecture:** Tailscale Funnel (HTTPS) → Traefik (HTTP:80) → web container **FQDN (Traefik):** `http://alezmad-nuc.tail58f5ad.ts.net` (HTTP internally — Tailscale handles TLS termination) **Build & Deploy workflow:** ```bash # 1. Build image locally (ARM→AMD cross-compile) cd /Users/agutierrez/Desktop/turbostarter-export docker build --platform linux/amd64 \ --build-arg NEXT_PUBLIC_URL=https://alezmad-nuc.tail58f5ad.ts.net \ -t 192.168.1.3:3030/alezmad/turbostarter:latest . # 2. Push to Gitea registry docker push 192.168.1.3:3030/alezmad/turbostarter:latest # 3. Redeploy via Coolify (stop + start for full container recreation) mcp__coolify__control(resource="service", action="stop", uuid="v4gogwwc8wkk4888ksscc4k4") mcp__coolify__control(resource="service", action="start", uuid="v4gogwwc8wkk4888ksscc4k4") ``` **Containers:** | Container | Image | Purpose | |-----------|-------|---------| | `web-v4gogwwc8wkk4888ksscc4k4` | `localhost:3030/alezmad/turbostarter:latest` | Next.js app | | `db-v4gogwwc8wkk4888ksscc4k4` | `pgvector/pgvector:pg17` | PostgreSQL + pgvector | | `minio-v4gogwwc8wkk4888ksscc4k4` | `minio/minio:latest` | Object storage | | `minio-init-v4gogwwc8wkk4888ksscc4k4` | `minio/mc:latest` | One-time bucket init | **Database access (via SSH tunnel):** ```bash # Get DB container IP first ssh nuc "docker inspect db-v4gogwwc8wkk4888ksscc4k4 --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'" # Then tunnel to IP (not container name) ssh -L 5440::5432 nuc # Connect: postgres://turbostarter:turbostarter@localhost:5440/core ``` **Seeded users:** `me+admin@turbostarter.dev` / `Pa$$w0rd` (admin), `me+user@turbostarter.dev` / `Pa$$w0rd` **Key env vars:** - `BETTER_AUTH_TRUSTED_ORIGINS` — comma-separated list of allowed origins (CSRF protection) - `NEXT_PUBLIC_URL` — build-time arg baked into Next.js static output (must rebuild to change) - `DATABASE_URL` — internal docker network connection to pgvector ### New Site from nuc-portal Template To create a new Next.js site using nuc-portal as a base: ```bash # 1. Copy and clean cp -r /path/to/nuc-portal /path/to/new-site cd /path/to/new-site rm -rf .git .next node_modules # 2. Update package.json (name, description) # 3. Customize src/app/page.tsx # 4. Remove unused components if simplifying # 5. Initialize and push npm install && npm run build # verify it builds git init && git add -A && git commit -m "Initial commit" # Create repo in Gitea first, then: git remote add origin gitea:alezmad/.git git push -u origin main ``` Then follow the "New Repo Auto-Deploy Checklist" above. ## 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)