Files
nuc/CLAUDE.md
Alejandro Gutiérrez f56528ddcd Slim CLAUDE.md from 65K to 21K by splitting app-specific docs
Move OpenClaw, Palmr, MinIO, JSX publishing, MCP configs, and migration
candidates into dedicated docs/ files. Keep only DevOps-essential content
inline (deployment rules, DNS, router, credentials, troubleshooting).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 02:56:08 +00:00

21 KiB

NUC Server - Claude Code Instructions

Server Access

The NUC server is accessible via SSH:

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/hosts100.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

http:
  routers:
    coolify:
      rule: Host(`coolify.nuc.lan`)
      service: coolify
  services:
    coolify:
      loadBalancer:
        servers:
          - url: http://host.docker.internal:8000

Adding a New Domain

# 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="<service-name>", ...)
  2. If type invalid: Deploy via docker-compose in Coolify using docker_compose_raw
  3. Last resort: Direct Docker commands via SSH
# 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.

# 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

# Access Coolify's Laravel tinker for direct database/service manipulation
ssh nuc "docker exec coolify php artisan tinker --execute=\"<PHP_CODE>\""

# 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

mcp__coolify__get_infrastructure_overview()
mcp__coolify__control(resource="service", action="start|stop|restart", uuid="<uuid>")
mcp__coolify__get_service(uuid="<uuid>")
mcp__coolify__service(action="update", uuid="<uuid>", docker_compose_raw="<yaml>")
mcp__coolify__database(action="delete", uuid="<uuid>", delete_volumes=True)

Docker Commands

ssh nuc "docker ps -a --format '{{.Names}}\t{{.Status}}'"
ssh nuc "docker logs <container_name> 2>&1 | tail -50"
ssh nuc "docker restart <container_name>"
ssh nuc "docker exec <container_name> <command>"

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

# Create a port forwarder for a Coolify service
ssh nuc "docker run -d --name port-fwd-<service> --network <coolify_network> -p <external_port>:<internal_port> alpine/socat tcp-listen:<internal_port>,fork,reuseaddr tcp-connect:<container_name>:<container_port>"

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 <container>
  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 <container> --format '{{.NetworkSettings.Networks}}'

Troubleshooting

Coolify MCP vs Direct Docker

Always verify Coolify status with Docker - Coolify's status can lag:

ssh nuc "docker ps -a --format 'table {{.Names}}\t{{.Status}}' | grep <service>"

Common Issues

  1. Containers stuck in "Created": ssh nuc "docker start <container_name>"
  2. Service shows "running:unknown": No healthcheck. Add via Coolify:
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost:<port>"]
      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 <name>, 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 <container> wget -qO- http://127.0.0.1:<port>/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 <volume>:/data keinos/sqlite3 sqlite3 /data/database.sqlite "<QUERY>"
  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

ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1
# Or via NUC jump host:
ssh nuc "ssh root@192.168.1.1 '<command>'"

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

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='<name>'
uci set firewall.@redirect[-1].src='wan'
uci set firewall.@redirect[-1].src_dport='<external_port>'
uci set firewall.@redirect[-1].dest='lan'
uci set firewall.@redirect[-1].dest_ip='<internal_ip>'
uci set firewall.@redirect[-1].dest_port='<internal_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

ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "
uci add firewall rule
uci set firewall.@rule[-1].name='Allow-<service>'
uci set firewall.@rule[-1].src='wan'
uci set firewall.@rule[-1].dest_port='<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)

# 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='<hostname>'
uci set dhcp.@host[-1].mac='<mac_address>'
uci set dhcp.@host[-1].ip='<ip_address>'
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='<hostname>'
uci set dhcp.@domain[-1].ip='<ip_address>'
uci commit dhcp
/etc/init.d/dnsmasq restart
"

Network Diagnostics

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 <package>"  # 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

# 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:

ssh nuc "docker exec coolify php artisan tinker --execute=\"
use App\\Models\\Application;
\\\$app = Application::where('uuid', '<app-uuid>')->first();
\\\$app->manual_webhook_secret_gitea = '9eb07a77964563378c5d66d99006e06ba3da39d232905d4b12554ff91ca39718';
\\\$app->save();
\""

Step 2 — Gitea webhook:

  • URL: http://coolify:8080/webhooks/source/gitea/events/manual?uuid=<app-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)

# 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/<repo>.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="<app-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.

# 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:<container_ip>:5432 nucpostgres://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

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/<repo>.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).

# 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_<description>.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