Files
nuc/CLAUDE.md
Alejandro Gutiérrez 16838f1ca1 Add webhook secret troubleshooting docs
- Document that webhook secret must be set in BOTH Coolify AND Gitea
- Add verification command to check if secret is configured
- Add whyrating-hub to webhook URL table

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 01:04:01 +00:00

32 KiB

NUC Server - Claude Code Instructions

Server Access

The NUC server is accessible via SSH:

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="<service-name>", ...)
  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)
# 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-<location>-<id>
chrome-devtools-<location>-<id>

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
# 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="<element_uid>")
mcp__chrome-devtools__fill(uid="<element_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.

# 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__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)

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.

# Basic syntax
claude mcp add --transport http <name> <url> --scope user --header "<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:

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:

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

{
  "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:

# 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();
\""

Docker Commands

# List all containers
ssh nuc "docker ps -a --format '{{.Names}}\t{{.Status}}'"

# View container logs
ssh nuc "docker logs <container_name> 2>&1 | tail -50"

# Restart a container
ssh nuc "docker restart <container_name>"

# Execute command in container
ssh nuc "docker exec <container_name> <command>"

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-*
NocoDB 8084 http://192.168.1.3:8084 nocodb-*
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:

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

  • 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 <container>

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

Troubleshooting

Coolify MCP vs Direct Docker

Always verify Coolify status with Docker - Coolify's status can lag behind actual container state:

# Coolify may show "exited" but container is actually running
ssh nuc "docker ps -a --format 'table {{.Names}}\t{{.Status}}' | grep <service>"

Common Issues

  1. Containers stuck in "Created" state: After Coolify restart/redeploy, containers may not auto-start

    ssh nuc "docker start <container_name>"
    
  2. Service shows "running:unknown": No healthcheck configured. Add one via Coolify service update:

    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost:<port>"]
      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:

    # Verify container doesn't exist
    ssh nuc "docker ps -a | grep <container_name>"
    # 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:

    ssh nuc "docker exec <container> wget -qO- http://127.0.0.1:<port>/healthz"
    
  7. Creating API keys when no UI available (e.g., n8n):

    # Stop container, insert directly into SQLite, restart
    ssh nuc "docker run --rm -v <volume>:/data keinos/sqlite3 sqlite3 /data/database.sqlite \"<INSERT_QUERY>\""
    

Coolify MCP Quick Reference

# Check infrastructure overview
mcp__coolify__get_infrastructure_overview()

# Start/stop/restart service
mcp__coolify__control(resource="service", action="start|stop|restart", uuid="<uuid>")

# Get service details (including docker_compose)
mcp__coolify__get_service(uuid="<uuid>")

# Update service config (e.g., add healthcheck)
mcp__coolify__service(action="update", uuid="<uuid>", docker_compose_raw="<yaml>")

# Delete stale database
mcp__coolify__database(action="delete", uuid="<uuid>", delete_volumes=True)

OpenWrt Router

The network is managed by an OpenWrt router at 192.168.1.1.

SSH Access

# 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 '<command>'"

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

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

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

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

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

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

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

# 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="<uid>")
mcp__chrome-devtools__fill(uid="<uid>", value="<text>")

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

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:

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

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: 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=<app-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:

# Verify Coolify has the secret
ssh nuc "docker exec coolify php artisan tinker --execute=\"
use App\\Models\\Application;
\\\$app = Application::where('uuid', '<app-uuid>')->first();
echo 'Secret: ' . (\\\$app->manual_webhook_secret_gitea ? 'SET' : 'MISSING');
\""

Local Development (Clone & Push)

SSH config is set up for direct git operations:

# Clone a repo
git clone gitea:nuc/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=<app-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)

# 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:nuc/<repo>.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', '<app-uuid>')->first();
\\\$app->fqdn = 'http://<name>.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', '<app-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=<app-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="<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

When creating a new repo that should auto-deploy:

  1. [ ] Add deploy key to Gitea repo

    • Go to: http://192.168.1.3:3030/nuc/<repo>/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/nuc/<repo>/settings/hooks
    • Add Webhook → Gitea
    • URL: http://coolify:8080/webhooks/source/gitea/events/manual?uuid=<app-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="<app-uuid>")

Common mistakes:

  • Using port 8000 instead of 8080 in webhook URL
  • Forgetting ?uuid=<app-uuid> in webhook URL
  • Not enabling Write Access on deploy key

Quick Verification Commands

# 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 nuc/nuc-portal t80w0cw0oooc4g0soswos4so
whyrating-hub http://whyrating.nuc.lan nuc/whyrating-hub vw4ggc40socwkgwg4osc8wg8
whyrating-brand http://brand.nuc.lan nuc/whyrating-brand r80gk0ccgg0okos8cw848kkk
whyrating-templates http://templates.nuc.lan nuc/whyrating-templates qw80g4sog0kk8cc4wkcs8sgc

New Site from nuc-portal Template

To create a new Next.js site using nuc-portal as a base:

# 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:nuc/<repo-name>.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:

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

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

# <Title>

**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
# 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)