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>
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) or100.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:
- OpenWrt router DNS (dnsmasq via
uci) - 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
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:
- 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
# 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:
mcp__playwriter-nuc-01__*- Remote NUC browser (preferred)mcp__chrome-devtools-nuc-01__*- Chrome DevTools for NUC browsermcp__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
- After Coolify redeploy: Containers may be in "Created" state - manually start with
docker start <container> - Environment Variables in Coolify: Encrypted with Laravel. Use
encrypt()when updating. - HSTS Issues: Use nginx proxy with
proxy_hide_header Strict-Transport-Security;to strip. - 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
- Containers stuck in "Created":
ssh nuc "docker start <container_name>" - 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 - Service dependencies not starting: Check dependency containers are healthy first.
- Stale Coolify entries: Verify container exists with
docker ps -a | grep <name>, then delete if missing. - Embedded vs Standalone databases: Services like Outline have their own PostgreSQL containers bundled in compose.
- Wrong healthcheck endpoint: Some use
/healthz. Verify:docker exec <container> wget -qO- http://127.0.0.1:<port>/healthz - localhost resolves to IPv6 on NUC: Always use
127.0.0.1instead oflocalhostin Funnel/Serve targets. - Funnel port not working externally: Only ports 443, 8443, 10000 work for Funnel.
- API keys without UI: Insert directly into SQLite:
docker run --rm -v <volume>:/data keinos/sqlite3 sqlite3 /data/database.sqlite "<QUERY>" - 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), never192.168.1.3from 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
- Add deploy key to Gitea repo (Write Access enabled)
- Create Coolify application (
action="create_key") - Set FQDN via tinker
- Set webhook secret via tinker
- Create Gitea webhook (port 8080 + UUID + secret)
- Test webhook (HTTP 200)
- 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 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
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 |