# 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 ### DNS Strategy: Dual Resolution (Router DNS + Mac /etc/hosts) **On the NUC's LAN (OpenWrt router):** All `.nuc.lan` domains resolve to `192.168.1.3` via dnsmasq. **On the Mac:** `/etc/hosts` maps `.nuc.lan` domains to Tailscale IP `100.113.153.45` (Mac and NUC are on different physical networks that both use `192.168.1.0/24`). **When adding a new `.nuc.lan` domain, update BOTH:** 1. OpenWrt router DNS (dnsmasq via `uci`) 2. Mac `/etc/hosts` → `100.113.153.45 newservice.nuc.lan` ### Configured Domains | Domain | Service | |--------|---------| | `nuc.lan` / `nuc.local` | NUC Portal | | `coolify.nuc.lan` | Coolify | | `gitea.nuc.lan` | Gitea | | `outline.nuc.lan` | Outline Wiki | | `files.nuc.lan` | FileBrowser | | `mail.nuc.lan` | Snappymail | | `vault.nuc.lan` | Vaultwarden | | `homepage.nuc.lan` | NUC Portal | | `brand.nuc.lan` | Whyrating Brand | | `templates.nuc.lan` | Whyrating Templates | | `whyrating.nuc.lan` | Whyrating Hub | | `whyops.nuc.lan` | WhyOps | | `arrio.nuc.lan` | Arrio | | `whatsapp.nuc.lan` | WhatsApp MCP | All resolve to `192.168.1.3`. ### Traefik Routing Config location: `/data/coolify/proxy/dynamic/nuc-services.yaml` ```yaml 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 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=\"192.168.1.3\" uci commit dhcp /etc/init.d/dnsmasq restart '" # 2. Add to Mac /etc/hosts echo "924718" | sudo -S sh -c 'echo "100.113.153.45 newservice.nuc.lan" >> /etc/hosts' echo "924718" | sudo -S dscacheutil -flushcache && sudo killall -HUP mDNSResponder # 3. Add Traefik route (if needed for port-based service) # Edit /data/coolify/proxy/dynamic/nuc-services.yaml ``` ### Always-On Tailscale **Keep Tailscale running** - direct connection on home network, mesh routing when remote, ~0% CPU idle. ## Service Management ### Coolify (Primary Service Manager) All services managed through Coolify at `http://coolify.nuc.lan` (or `http://100.113.153.45:8000`). **Prefer Coolify MCP** (`mcp__coolify__*`) over 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 ```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) ``` ### STRICT RULE: Browser MCP for Manual Configurations **ALWAYS use Browser MCP instead of asking the user to do it manually.** **Priority order:** 1. `mcp__playwriter-nuc-01__*` - Remote NUC browser (preferred) 2. `mcp__chrome-devtools-nuc-01__*` - Chrome DevTools for NUC browser 3. `mcp__playwriter-local__*` - Local browser (fallback) **NEVER say "please go to X and do Y manually" - use browser MCP instead.** ### STRICT RULE: Parallel Subtasks for Multiple Operations **ALWAYS use parallel Task agents for independent operations.** ```python # CORRECT - Parallel subtasks (fast) Task(subagent_type="general-purpose", prompt="Configure Tailscale...", description="Setup Tailscale") Task(subagent_type="general-purpose", prompt="Install WireGuard...", description="Setup WireGuard") # All run simultaneously! ``` ### STRICT RULE: MCP Research Protocol **When asked to find/recommend new MCPs**, read `docs/mcp-research-guide.md` first. Quick workflow: Search `mcp.so`, `smithery.ai`, `mcpservers.org` → Verify on GitHub (50+ stars, <6 months active) → Evaluate. ### Available MCPs for NUC Management | MCP | Purpose | |-----|---------| | `mcp__coolify__*` | Service management, deployments, env vars | | `mcp__stalwart-mail__*` | Email server management | | `mcp__email-client__*` | Read/send emails via IMAP/SMTP | | `mcp__nocodb__*` | Database operations, table management | | `mcp__ssh-manager__*` | Direct SSH commands, file transfers | | `mcp__n8n__*` | Workflow automation | | `mcp__playwriter__*` | Browser automation | | `mcp__deepgram__*` | Audio transcription (STT) and TTS | > Full MCP config details (Stalwart, Email, Remote MCPs, Browser): `docs/mcp-configs.md` ### 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(); \"" ``` ### Coolify MCP Quick Reference ```python mcp__coolify__get_infrastructure_overview() mcp__coolify__control(resource="service", action="start|stop|restart", uuid="") mcp__coolify__get_service(uuid="") mcp__coolify__service(action="update", uuid="", docker_compose_raw="") mcp__coolify__database(action="delete", uuid="", delete_volumes=True) ``` ### Docker Commands ```bash ssh nuc "docker ps -a --format '{{.Names}}\t{{.Status}}'" ssh nuc "docker logs 2>&1 | tail -50" ssh nuc "docker restart " ssh nuc "docker exec " ``` ## Services & Ports | 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-* | | WhyOps | `http://whyops.nuc.lan` | `http://100.113.153.45:3002` | whyrating-dashboard | | Palmr | `https://alezmad-nuc.tail58f5ad.ts.net:8443` | `http://100.113.153.45:3334` | palmr | | Arrio | `http://arrio.nuc.lan` | `http://100.113.153.45:3335` | web-tgksg0s8gocko4csggs0808c | | WhatsApp MCP | `http://whatsapp.nuc.lan` | `http://100.113.153.45:3100` | whatsapp-mcp | | Deepgram MCP | - | `http://100.113.153.45:8009` | deepgram-mcp | **Note:** Use Tailscale IP (`100.113.153.45`) instead of `192.168.1.3` to avoid subnet conflicts when remote. ## Port Forwarding ```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:** `/opt/homepage/config/` (services: `services.yaml`) - **Coolify Data:** `/data/coolify/` ## Important Notes 1. **After Coolify redeploy**: Containers may be in "Created" state - manually start with `docker start ` 2. **Environment Variables in Coolify**: Encrypted with Laravel. Use `encrypt()` when updating. 3. **HSTS Issues**: Use nginx proxy with `proxy_hide_header Strict-Transport-Security;` to strip. 4. **Network Discovery**: `docker inspect --format '{{.NetworkSettings.Networks}}'` ## Troubleshooting ### Coolify MCP vs Direct Docker **Always verify Coolify status with Docker** - Coolify's status can lag: ```bash ssh nuc "docker ps -a --format 'table {{.Names}}\t{{.Status}}' | grep " ``` ### Common Issues 1. **Containers stuck in "Created"**: `ssh nuc "docker start "` 2. **Service shows "running:unknown"**: No healthcheck. Add via Coolify: ```yaml healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:"] interval: 30s timeout: 10s retries: 3 start_period: 30s ``` 3. **Service dependencies not starting**: Check dependency containers are healthy first. 4. **Stale Coolify entries**: Verify container exists with `docker ps -a | grep `, then delete if missing. 5. **Embedded vs Standalone databases**: Services like Outline have their own PostgreSQL containers bundled in compose. 6. **Wrong healthcheck endpoint**: Some use `/healthz`. Verify: `docker exec wget -qO- http://127.0.0.1:/healthz` 7. **localhost resolves to IPv6 on NUC**: Always use `127.0.0.1` instead of `localhost` in Funnel/Serve targets. 8. **Funnel port not working externally**: Only ports **443, 8443, 10000** work for Funnel. 9. **API keys without UI**: Insert directly into SQLite: `docker run --rm -v :/data keinos/sqlite3 sqlite3 /data/database.sqlite ""` 10. **Can't reach NUC on LAN**: Mac and NUC on different networks both using `192.168.1.0/24`. Use Tailscale IP (`100.113.153.45`), never `192.168.1.3` from Mac. ## OpenWrt Router Router at `192.168.1.1` — OpenWrt 23.05.0, ARM Cortex-A9. **Priority Order:** SSH > OpenWrt MCP > Chrome DevTools > Manual UI ### SSH Access ```bash ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 # Or via NUC jump host: ssh nuc "ssh root@192.168.1.1 ''" ``` ### OpenWrt MCP Server - HTTP API: `http://192.168.1.1:8090` | Token: `openwrt-mcp-secret-2026` - Control: `ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "/etc/init.d/mcp-server start|stop|restart"` ### Port Forwarding ```bash ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "uci show firewall | grep redirect" 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 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 # 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 ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "ifstatus wan" # WAN status ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "cat /tmp/dhcp.leases" # Connected clients ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "ip route" # Routing table ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "logread | tail -50" # System logs ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "opkg update && opkg install " # Packages ``` ## Credentials & Authentication ### CloudBeaver | **URL** | `http://192.168.1.3:8978` | **Admin** | `cbadmin` / `CloudBeaver2026!` | **UUID** | `joo4g4k0w08k8kcosgsgswc0` | ### Vaultwarden (REQUIRES HTTPS) | **HTTPS URL** | `https://nuc-tailscale.tail58f5ad.ts.net:8443` | **Admin** | `admin@nuc.lan` / `NucVault2026!Secure` | ### Stalwart Mail Server | **Admin URL** | `http://192.168.1.3:8081` | **Creds** | `admin` / `QfKYjCJdxu` | **UUID** | `kw00kok0w0s8gcok008gk04k` | **Mail Users:** `info@whyrating.com` (username: `info`, password: `whyrating2026`) **DNS:** SPF, DKIM (Ed25519 + RSA), DMARC, MX configured for `whyrating.com` ### Outline OIDC (via Gitea) - Client ID: `249a3a1d-92d4-47d8-b4a9-81c64e1da6ab` - Auth/Token/Userinfo: `http://192.168.1.3:3030/login/oauth/authorize|access_token|userinfo` ### Gitea Users | `nedas` | `NedasNUC2026!` | Regular user | ## Gitea-Coolify Integration (Git Auto-Deploy) > Full docs: `docs/gitea-coolify-auto-deploy.md` ### Key References | Resource | Value | |----------|-------| | **Deploy Key UUID** | `akssgwowsccgwgoggs4ks8ck` | | **Gitea Container** | `gitea-ho0cwgcwos88cwc48g84c0g8` | | **Webhook Secret** | `9eb07a77964563378c5d66d99006e06ba3da39d232905d4b12554ff91ca39718` | ### CRITICAL: Gitea Webhook Allowed Hosts ```bash # Gitea's app.ini must have: [webhook] ALLOWED_HOST_LIST = coolify,10.0.1.5,192.168.1.3,localhost,host.docker.internal,external ``` ### CRITICAL: Webhook Secret Must Be Set in BOTH Places **Step 1 — Coolify:** ```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 — Gitea webhook:** - URL: `http://coolify:8080/webhooks/source/gitea/events/manual?uuid=` (port **8080**, NOT 8000) - Secret: `9eb07a77964563378c5d66d99006e06ba3da39d232905d4b12554ff91ca39718` - Trigger: Push Events ### Webhook URLs | App | UUID | |-----|------| | nuc-portal | `t80w0cw0oooc4g0soswos4so` | | whyrating-hub | `vw4ggc40socwkgwg4osc8wg8` | | whyrating-brand | `r80gk0ccgg0okos8cw848kkk` | | whyrating-templates | `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 via tinker # 3. Set webhook secret via tinker # 4. Create Gitea webhook (URL with port 8080 + secret) # 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 1. [ ] Add deploy key to Gitea repo (Write Access enabled) 2. [ ] Create Coolify application (`action="create_key"`) 3. [ ] Set FQDN via tinker 4. [ ] Set webhook secret via tinker 5. [ ] Create Gitea webhook (port 8080 + UUID + secret) 6. [ ] Test webhook (HTTP 200) 7. [ ] Initial deploy ### 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) | | arrio | http://arrio.nuc.lan | `alezmad/arrio` | `tgksg0s8gocko4csggs0808c` (service) | ### Turbostarter (Knosia) - Build & Deploy Deployed as **Coolify Service** (web + pgvector + minio). Architecture: Tailscale Funnel (HTTPS) → Traefik (HTTP:80) → web container. ```bash # Build (ARM→AMD), push to Gitea registry, redeploy 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 . docker push 192.168.1.3:3030/alezmad/turbostarter:latest mcp__coolify__control(resource="service", action="stop", uuid="v4gogwwc8wkk4888ksscc4k4") mcp__coolify__control(resource="service", action="start", uuid="v4gogwwc8wkk4888ksscc4k4") ``` **DB access:** `ssh -L 5440::5432 nuc` → `postgres://turbostarter:turbostarter@localhost:5440/core` **Seeded users:** `me+admin@turbostarter.dev` / `Pa$$w0rd`, `me+user@turbostarter.dev` / `Pa$$w0rd` ### New Site from nuc-portal Template ```bash cp -r /path/to/nuc-portal /path/to/new-site && cd /path/to/new-site rm -rf .git .next node_modules # Update package.json, customize src/app/page.tsx npm install && npm run build git init && git add -A && git commit -m "Initial commit" git remote add origin gitea:alezmad/.git && git push -u origin main ``` Then follow the Auto-Deploy Checklist above. ### Local Git SSH Config ``` Host gitea HostName 192.168.1.3 Port 22222 User git IdentityFile ~/.ssh/id_ed25519_nuc ``` ## Public Access & Security > Full architecture: `docs/architecture.md` ### Tailscale Funnel | Property | Value | |----------|-------| | **Funnel URL** | `https://nuc-tailscale.tail58f5ad.ts.net` | | **Supported ports** | **443, 8443, 10000 ONLY** | **CRITICAL:** Always use `127.0.0.1`, NOT `localhost` in Funnel/Serve targets (IPv6 issue). ```bash # Expose a service via Funnel ssh nuc "tailscale funnel --bg --https=8443 http://127.0.0.1:3000" ``` **Current Funnel allocation:** | Port | Service | Target | |------|---------|--------| | 443 | Traefik (main) | `http://192.168.1.3:80` | | 8443 | Palmr | `http://127.0.0.1:3334` | | 10000 | Palmr MinIO | `http://127.0.0.1:9379` | ### Domain Routes | Domain | Destination | Method | |--------|-------------|--------| | whyrating.com | `nuc-tailscale.tail58f5ad.ts.net` | Namecheap 301 redirect | ### Security Layers ``` Internet → Tailscale Funnel (HTTPS) → CrowdSec → Traefik → Container ``` **NOT exposed to internet:** SSH, Coolify, databases, MinIO, Authentik, router admin. ## Artifacts Folder **Location:** `.artifacts/` — stores credentials, configs, reports generated during sessions. **Naming:** `YYYY-MM-DD_HH-MM_.md` **Always save artifacts for:** API tokens, config changes, troubleshooting results, infrastructure changes, health reports. > Full conventions: see artifact template in this folder. ## Detailed Documentation (split docs) | Topic | File | |-------|------| | OpenClaw gateway | `docs/openclaw.md` | | Palmr file sharing | `docs/palmr.md` | | MinIO storage | `docs/minio.md` | | Publishing JSX artifacts | `docs/publishing-artifacts.md` | | MCP server configs | `docs/mcp-configs.md` | | Gitea auto-deploy deep-dive | `docs/gitea-coolify-auto-deploy.md` | | Security architecture | `docs/architecture.md` | | MCP research guide | `docs/mcp-research-guide.md` | | Turbostarter deployment | `docs/turbostarter-deployment.md` |