- CLAUDE.md: Server instructions and service reference - docs/: Persistent documentation (architecture, guides) - .artifacts/: Session-generated notes - playwriter-browser/: Remote browser container config Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
23 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:
- First: Try
mcp__coolify__service(action="create", type="<service-name>", ...) - If type invalid: Deploy via docker-compose in Coolify using
docker_compose_raw - 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:
mcp__playwriter-nuc-01__*- Remote NUC browser (preferred, no local resources)mcp__chrome-devtools-nuc-01__*- Chrome DevTools for NUC browsermcp__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
Tasktool calls in a single message - Each task gets its own agent with full context
- Results are collected when all complete
Available MCPs for NUC Management
| MCP | Purpose |
|---|---|
mcp__coolify__* |
Service management, deployments, env vars |
mcp__ssh-manager__* |
Direct SSH commands, file transfers |
mcp__n8n__* |
Workflow automation (if configured) |
mcp__playwriter__* |
Browser automation fallback (see below) |
⚠️ STRICT RULE: MCP Research Protocol
When asked to find/recommend new MCPs, read the detailed guide at docs/mcp-research-guide.md which contains:
- MCP directories (MCP.so, Smithery, MCPHub, etc.)
- GitHub verification resources
- Evaluation criteria checklist
- Research workflow
- Category quick reference
Quick workflow:
- Search directories:
mcp.so,smithery.ai,mcpservers.org - Verify on GitHub: stars, last commit, issues
- 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-* |
| 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
-
After Coolify redeploy: Containers may be in "Created" state - manually start with
docker start <container> -
Environment Variables in Coolify: Are encrypted with Laravel encryption. Use
encrypt()when updating. -
HSTS Issues: Some services send HSTS headers. Use nginx proxy with
proxy_hide_header Strict-Transport-Security;to strip them. -
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
-
Containers stuck in "Created" state: After Coolify restart/redeploy, containers may not auto-start
ssh nuc "docker start <container_name>" -
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 -
Service dependencies not starting: Services with
depends_on: condition: service_healthywon't start until dependencies are healthy. Check dependency containers first. -
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 -
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. -
Wrong healthcheck endpoint: Some services use
/healthzinstead of/. Verify with:ssh nuc "docker exec <container> wget -qO- http://127.0.0.1:<port>/healthz" -
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
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
-
Check availability (if needed):
mcp__namecheap__namecheap_check_domain_availability(domains=["example.com"]) -
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)
-
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.md2026-02-01_14-30_n8n-mcp-setup.md2026-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 |
Recommended Coolify Deployments
# 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)