Expand from initial setup notes to full operational manual covering OpenClaw gateway, Palmr file sharing, MinIO storage, Deepgram MCP, Gitea auto-deploy workflows, Tailscale Funnel architecture, JSX artifact publishing, and OpenWrt router management. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1778 lines
61 KiB
Markdown
1778 lines
61 KiB
Markdown
# 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: Local IP + Tailscale Subnet Routing
|
|
|
|
All `.nuc.lan` domains resolve to the **local LAN IP** (`192.168.1.3`). This allows any device on the LAN to access services without Tailscale.
|
|
|
|
**Remote access (Tailscale)** works via two mechanisms:
|
|
1. **Split DNS**: Tailscale forwards `*.nuc.lan` queries to the OpenWrt router (`192.168.1.1`) through the tunnel
|
|
2. **Subnet routing**: The NUC advertises `192.168.1.0/24` as a Tailscale subnet route, so `192.168.1.3` is reachable from any Tailscale device remotely
|
|
|
|
### Configured Domains (OpenWrt Router DNS)
|
|
|
|
| Domain | Resolves To | Service |
|
|
|--------|-------------|---------|
|
|
| `nuc.lan` | `192.168.1.3` | NUC Portal |
|
|
| `nuc.local` | `192.168.1.3` | NUC Portal |
|
|
| `coolify.nuc.lan` | `192.168.1.3` | Coolify |
|
|
| `gitea.nuc.lan` | `192.168.1.3` | Gitea |
|
|
| `outline.nuc.lan` | `192.168.1.3` | Outline Wiki |
|
|
| `files.nuc.lan` | `192.168.1.3` | FileBrowser |
|
|
| `mail.nuc.lan` | `192.168.1.3` | Snappymail |
|
|
| `vault.nuc.lan` | `192.168.1.3` | Vaultwarden |
|
|
| `homepage.nuc.lan` | `192.168.1.3` | NUC Portal |
|
|
| `brand.nuc.lan` | `192.168.1.3` | Whyrating Brand |
|
|
| `templates.nuc.lan` | `192.168.1.3` | Whyrating Templates |
|
|
| `whyrating.nuc.lan` | `192.168.1.3` | Whyrating Hub |
|
|
| `whyops.nuc.lan` | `192.168.1.3` | WhyOps (scraping, pipelines, testing) |
|
|
| `arrio.nuc.lan` | `192.168.1.3` | Arrio (hotel check-in) |
|
|
|
|
### Traefik Routing (Dynamic Config)
|
|
|
|
Traefik routes domain-based requests to the correct backend. Config location: `/data/coolify/proxy/dynamic/nuc-services.yaml`
|
|
|
|
```yaml
|
|
# Routes for port-based services via domain names
|
|
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 (via NUC jump host)
|
|
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 Traefik route (if needed for port-based service)
|
|
# Edit /data/coolify/proxy/dynamic/nuc-services.yaml
|
|
```
|
|
|
|
### Always-On Tailscale
|
|
|
|
**Keep Tailscale running** - it's designed to be always-on:
|
|
- When on home network: Uses direct connection (no relay, same performance as local)
|
|
- When remote: Routes through Tailscale mesh
|
|
- Minimal resource usage (~0% CPU when idle)
|
|
|
|
## Service Management
|
|
|
|
### Coolify (Primary Service Manager)
|
|
All services are managed through Coolify at `http://coolify.nuc.lan` (or `http://100.113.153.45: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)
|
|
|
|
```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)
|
|
```
|
|
|
|
**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
|
|
|
|
```python
|
|
# 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.**
|
|
|
|
```python
|
|
# 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__stalwart-mail__*` | Email server management (users, domains, queue) |
|
|
| `mcp__email-client__*` | Read/send emails via IMAP/SMTP (see below) |
|
|
| `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) |
|
|
| `mcp__deepgram__*` | Audio transcription (STT) and text-to-speech (TTS) |
|
|
|
|
### Stalwart Mail MCP (Quick Guide)
|
|
|
|
**Location:** `~/mcp-servers/stalwart-mail/`
|
|
|
|
Manage the self-hosted Stalwart mail server via natural language.
|
|
|
|
**Available Tools:**
|
|
|
|
| Category | Tools |
|
|
|----------|-------|
|
|
| **Users** | `list_users`, `get_user`, `create_user`, `update_user_password`, `delete_user`, `add_email_alias` |
|
|
| **Domains** | `create_domain`, `generate_dkim` |
|
|
| **Queue** | `list_queue`, `get_queue_status`, `delete_queued_message`, `retry_queued_message` |
|
|
| **Monitoring** | `get_metrics`, `get_dmarc_reports`, `get_server_logs` |
|
|
| **DNS** | `check_dns_records`, `troubleshoot_delivery` |
|
|
| **Spam** | `train_spam`, `train_ham`, `update_spam_filter` |
|
|
|
|
**Usage Examples:**
|
|
```
|
|
"List all mail users"
|
|
"Create user sales with email sales@whyrating.com and password Secret123"
|
|
"Check the mail queue"
|
|
"Verify DNS records for whyrating.com"
|
|
"Show server metrics"
|
|
"Delete user john"
|
|
```
|
|
|
|
**Direct API Test (if MCP not responding):**
|
|
```bash
|
|
curl -s -u "admin:QfKYjCJdxu" "http://192.168.1.3:8081/api/principal" | jq .
|
|
```
|
|
|
|
**Reconfigure MCP:**
|
|
```bash
|
|
claude mcp remove stalwart-mail
|
|
claude mcp add stalwart-mail \
|
|
-e STALWART_URL=http://192.168.1.3:8081 \
|
|
-e STALWART_USER=admin \
|
|
-e STALWART_PASS=QfKYjCJdxu \
|
|
--scope user \
|
|
-- ~/mcp-servers/stalwart-mail/.venv/bin/python ~/mcp-servers/stalwart-mail/server.py
|
|
```
|
|
|
|
**⚠️ SMTP Authentication Requirements:**
|
|
1. **Password format:** Must be SHA-512 hashed (not plaintext). When creating users via API:
|
|
```python
|
|
import crypt
|
|
hashed = crypt.crypt('password', crypt.mksalt(crypt.METHOD_SHA512))
|
|
# Use hashed value in 'secrets' field
|
|
```
|
|
2. **SMTP login:** Use username only (e.g., `info`), NOT full email (`info@whyrating.com`)
|
|
3. **Port 465 (SMTPS):** Supports PLAIN/LOGIN auth with implicit TLS
|
|
4. **Port 587 (Submission):** Requires STARTTLS, only OAuth supported without TLS
|
|
|
|
**Send email via Python (from NUC):**
|
|
```python
|
|
import smtplib, ssl
|
|
from email.mime.text import MIMEText
|
|
|
|
context = ssl.create_default_context()
|
|
context.check_hostname = False
|
|
context.verify_mode = ssl.CERT_NONE
|
|
|
|
with smtplib.SMTP_SSL('localhost', 465, context=context) as server:
|
|
server.login('info', 'whyrating2026') # Username only!
|
|
server.sendmail('info@whyrating.com', 'recipient@example.com', msg.as_string())
|
|
```
|
|
|
|
### Email Client MCP (Read/Send Emails)
|
|
|
|
**Package:** [mcp-email-server](https://github.com/ai-zerolab/mcp-email-server)
|
|
|
|
Read and send emails via IMAP/SMTP directly from Claude.
|
|
|
|
**Configured for:** `info@whyrating.com` on Stalwart
|
|
|
|
**Usage Examples:**
|
|
```
|
|
"Check my inbox"
|
|
"Read the latest email"
|
|
"Send an email to john@example.com with subject Hello"
|
|
"Search emails from support@"
|
|
"List email folders"
|
|
```
|
|
|
|
**Reconfigure:**
|
|
```bash
|
|
claude mcp remove email-client
|
|
claude mcp add email-client \
|
|
-e MCP_EMAIL_SERVER_EMAIL_ADDRESS=info@whyrating.com \
|
|
-e MCP_EMAIL_SERVER_PASSWORD=whyrating2026 \
|
|
-e MCP_EMAIL_SERVER_IMAP_HOST=192.168.1.3 \
|
|
-e MCP_EMAIL_SERVER_IMAP_PORT=143 \
|
|
-e MCP_EMAIL_SERVER_SMTP_HOST=192.168.1.3 \
|
|
-e MCP_EMAIL_SERVER_SMTP_PORT=587 \
|
|
-e MCP_EMAIL_SERVER_SMTP_VERIFY_SSL=false \
|
|
-e MCP_EMAIL_SERVER_ENABLE_ATTACHMENT_DOWNLOAD=true \
|
|
--scope user \
|
|
-- uvx mcp-email-server@latest stdio
|
|
```
|
|
|
|
### 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.
|
|
|
|
```bash
|
|
# 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:**
|
|
```bash
|
|
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:
|
|
|
|
```javascript
|
|
// 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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 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
|
|
|
|
**Preferred access via domain names** (works from anywhere via Tailscale):
|
|
|
|
| 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 |
|
|
| 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
|
|
|
|
Some services use port forwarding containers (alpine/socat or nginx) to expose internal Coolify services:
|
|
```bash
|
|
# 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:
|
|
```bash
|
|
# 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
|
|
```bash
|
|
ssh nuc "docker start <container_name>"
|
|
```
|
|
|
|
2. **Service shows "running:unknown"**: No healthcheck configured. Add one via Coolify service update:
|
|
```yaml
|
|
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:
|
|
```bash
|
|
# 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:
|
|
```bash
|
|
ssh nuc "docker exec <container> wget -qO- http://127.0.0.1:<port>/healthz"
|
|
```
|
|
|
|
7. **localhost resolves to IPv6 on NUC**: Docker containers bind to IPv4 only. Always use `127.0.0.1` instead of `localhost` in Tailscale Funnel/Serve targets, curl commands inside SSH, etc.
|
|
|
|
8. **Tailscale Funnel port not working externally**: Funnel ONLY supports ports **443, 8443, and 10000**. Other ports will show as "Funnel on" in status but won't route internet traffic.
|
|
|
|
9. **Creating API keys when no UI available** (e.g., n8n):
|
|
```bash
|
|
# 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
|
|
|
|
```python
|
|
# 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
|
|
```bash
|
|
# 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`
|
|
|
|
```bash
|
|
# 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:**
|
|
```bash
|
|
# 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:**
|
|
```bash
|
|
# 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):**
|
|
```bash
|
|
# 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:**
|
|
```bash
|
|
# 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):**
|
|
```bash
|
|
# 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:
|
|
```python
|
|
# 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`
|
|
|
|
## ⚠️ Critical Credentials & Access
|
|
|
|
### CloudBeaver (Database Manager)
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| **URL** | `http://192.168.1.3:8978` |
|
|
| **Admin User** | `cbadmin` |
|
|
| **Admin Password** | `CloudBeaver2026!` |
|
|
| **Service UUID** | `joo4g4k0w08k8kcosgsgswc0` |
|
|
|
|
**Pre-configured connections:** 9 databases across 3 folders. Turbostarter DB is now in service `v4gogwwc8wkk4888ksscc4k4` (container: `db-v4gogwwc8wkk4888ksscc4k4`).
|
|
Connected to 7 Docker networks for direct container-to-container access.
|
|
|
|
### Vaultwarden (Password Manager)
|
|
|
|
**⚠️ CRITICAL: Vaultwarden REQUIRES HTTPS** - The Web Crypto API needs a secure context for client-side encryption. HTTP access will NOT work (blank page/loading forever).
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| **HTTPS URL** | `https://nuc-tailscale.tail58f5ad.ts.net:8443` |
|
|
| **HTTP URL** | `http://192.168.1.3:8222` (won't load - HTTPS required) |
|
|
| **Admin Email** | `admin@nuc.lan` |
|
|
| **Admin Password** | `NucVault2026!Secure` |
|
|
|
|
**Access via Tailscale Funnel:**
|
|
```bash
|
|
# Vaultwarden is exposed on port 8443 via Tailscale Funnel
|
|
open "https://nuc-tailscale.tail58f5ad.ts.net:8443"
|
|
```
|
|
|
|
### Stalwart Mail Server
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| **Admin URL** | `http://192.168.1.3:8081` |
|
|
| **Username** | `admin` |
|
|
| **Password** | `QfKYjCJdxu` |
|
|
| **Webmail (Snappymail)** | `http://192.168.1.3:8082` |
|
|
| **Service UUID** | `kw00kok0w0s8gcok008gk04k` |
|
|
| **MCP Server** | `mcp__stalwart-mail__*` (see Quick Guide above) |
|
|
|
|
**Mail Users:**
|
|
| Email | Username | Password |
|
|
|-------|----------|----------|
|
|
| `info@whyrating.com` | `info` | `whyrating2026` |
|
|
|
|
**DNS Records Configured:** SPF, DKIM (Ed25519 + RSA), DMARC, MX for `whyrating.com`
|
|
|
|
**Container Status:** Running
|
|
|
|
### Gitea Users (for Outline OIDC)
|
|
|
|
| Username | Password | Notes |
|
|
|----------|----------|-------|
|
|
| `nedas` | `NedasNUC2026!` | Regular user account |
|
|
|
|
## 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:
|
|
|
|
```bash
|
|
# 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):
|
|
```bash
|
|
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:
|
|
```bash
|
|
# 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:
|
|
|
|
```bash
|
|
# Clone a repo
|
|
git clone gitea:alezmad/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)
|
|
|
|
```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/<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/alezmad/<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/alezmad/<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
|
|
|
|
```bash
|
|
# 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 | `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
|
|
|
|
Turbostarter is deployed as a **Coolify Service** (not a standalone app) with full docker-compose infrastructure: web + pgvector + minio.
|
|
|
|
**Architecture:** Tailscale Funnel (HTTPS) → Traefik (HTTP:80) → web container
|
|
|
|
**FQDN (Traefik):** `http://alezmad-nuc.tail58f5ad.ts.net` (HTTP internally — Tailscale handles TLS termination)
|
|
|
|
**Build & Deploy workflow:**
|
|
```bash
|
|
# 1. Build image locally (ARM→AMD cross-compile)
|
|
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 .
|
|
|
|
# 2. Push to Gitea registry
|
|
docker push 192.168.1.3:3030/alezmad/turbostarter:latest
|
|
|
|
# 3. Redeploy via Coolify (stop + start for full container recreation)
|
|
mcp__coolify__control(resource="service", action="stop", uuid="v4gogwwc8wkk4888ksscc4k4")
|
|
mcp__coolify__control(resource="service", action="start", uuid="v4gogwwc8wkk4888ksscc4k4")
|
|
```
|
|
|
|
**Containers:**
|
|
| Container | Image | Purpose |
|
|
|-----------|-------|---------|
|
|
| `web-v4gogwwc8wkk4888ksscc4k4` | `localhost:3030/alezmad/turbostarter:latest` | Next.js app |
|
|
| `db-v4gogwwc8wkk4888ksscc4k4` | `pgvector/pgvector:pg17` | PostgreSQL + pgvector |
|
|
| `minio-v4gogwwc8wkk4888ksscc4k4` | `minio/minio:latest` | Object storage |
|
|
| `minio-init-v4gogwwc8wkk4888ksscc4k4` | `minio/mc:latest` | One-time bucket init |
|
|
|
|
**Database access (via SSH tunnel):**
|
|
```bash
|
|
# Get DB container IP first
|
|
ssh nuc "docker inspect db-v4gogwwc8wkk4888ksscc4k4 --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'"
|
|
# Then tunnel to IP (not container name)
|
|
ssh -L 5440:<container_ip>:5432 nuc
|
|
# Connect: postgres://turbostarter:turbostarter@localhost:5440/core
|
|
```
|
|
|
|
**Seeded users:** `me+admin@turbostarter.dev` / `Pa$$w0rd` (admin), `me+user@turbostarter.dev` / `Pa$$w0rd`
|
|
|
|
**Key env vars:**
|
|
- `BETTER_AUTH_TRUSTED_ORIGINS` — comma-separated list of allowed origins (CSRF protection)
|
|
- `NEXT_PUBLIC_URL` — build-time arg baked into Next.js static output (must rebuild to change)
|
|
- `DATABASE_URL` — internal docker network connection to pgvector
|
|
|
|
### New Site from nuc-portal Template
|
|
|
|
To create a new Next.js site using nuc-portal as a base:
|
|
|
|
```bash
|
|
# 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:alezmad/<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"` |
|
|
|
|
**⚠️ CRITICAL: Funnel only supports ports 443, 8443, and 10000.** Other ports will appear to work locally (`tailscale funnel status` shows them) but traffic will NOT route from the public internet. Always use one of these three ports.
|
|
|
|
**⚠️ CRITICAL: Always use `127.0.0.1`, NOT `localhost`** in Funnel/Serve targets. On the NUC, `localhost` resolves to IPv6 (`::1`) but Docker containers bind to IPv4 only, causing connection resets.
|
|
|
|
**Start Funnel for a service:**
|
|
```bash
|
|
# Expose port 3000 via Funnel (use 127.0.0.1, NOT localhost!)
|
|
ssh nuc "printf '<sudo_pass>\n' | sudo -S 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 (file sharing) | `http://127.0.0.1:3334` |
|
|
| 10000 | Palmr MinIO (uploads) | `http://127.0.0.1:9379` |
|
|
|
|
### 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):
|
|
```python
|
|
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
|
|
```markdown
|
|
# <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>
|
|
```
|
|
|
|
## Publishing JSX/React Artifacts Online
|
|
|
|
Single-file React components (JSX) can be published as standalone web pages via the NUC's artifacts infrastructure.
|
|
|
|
### How It Works
|
|
|
|
```
|
|
Public Internet → Tailscale Funnel (:443) → Traefik → artifacts-web (nginx) → /opt/artifacts/
|
|
```
|
|
|
|
- **Funnel URL:** `https://alezmad-nuc.tail58f5ad.ts.net/artifacts/<name>/`
|
|
- **LAN URL:** `https://artifacts.nuc.lan/<name>/`
|
|
- **Nginx container:** `artifacts-web` (image: `nginx:alpine`, read-only mount of `/opt/artifacts`)
|
|
- **Traefik public route:** `Host(alezmad-nuc.tail58f5ad.ts.net) && PathPrefix(/artifacts)` → strips `/artifacts` → `artifacts-web:80`
|
|
- **Config file:** `/traefik/dynamic/nuc-services-public.yaml` (inside `coolify-proxy` container)
|
|
|
|
### Quick Publish Steps
|
|
|
|
```bash
|
|
# 1. Build self-contained HTML from JSX
|
|
# - Replace `import { useState, ... } from "react"` with `const { useState, ... } = React;`
|
|
# - Remove `export default ComponentName;`
|
|
# - Wrap in HTML with React 18 CDN + Babel standalone
|
|
# - Add `ReactDOM.createRoot(root).render(<Component />)` at the end
|
|
|
|
# 2. Copy to NUC artifacts directory
|
|
ssh nuc "echo '7vXHpSTD.' | sudo -S mkdir -p /opt/artifacts/<name>"
|
|
scp /tmp/build/index.html nuc:~/tmp-artifact.html
|
|
ssh nuc "echo '7vXHpSTD.' | sudo -S mv ~/tmp-artifact.html /opt/artifacts/<name>/index.html"
|
|
ssh nuc "echo '7vXHpSTD.' | sudo -S chmod 644 /opt/artifacts/<name>/index.html"
|
|
|
|
# 3. Done! No server restart needed — nginx serves it immediately.
|
|
```
|
|
|
|
### HTML Template for JSX Files
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>TITLE</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
|
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
|
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { background: #08090d; overflow-x: hidden; }
|
|
/* Add any @keyframes or global styles here */
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="root"></div>
|
|
<script type="text/babel">
|
|
const { useState, useEffect, useRef, useCallback } = React;
|
|
|
|
// ... paste JSX component code here (without import/export lines) ...
|
|
|
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
root.render(React.createElement(ComponentName));
|
|
</script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
### Build Script (for large JSX files)
|
|
|
|
```bash
|
|
# Automated: strips import/export, wraps in HTML
|
|
cat > /tmp/build.html << 'HEADER'
|
|
<!DOCTYPE html>
|
|
<html><head>
|
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
|
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
</head><body><div id="root"></div>
|
|
<script type="text/babel">
|
|
const { useState, useEffect, useRef, useCallback, useMemo, useReducer } = React;
|
|
HEADER
|
|
|
|
# Strip first line (import) and last line (export), append body
|
|
sed -n '2,$p' source.jsx | sed '$d' >> /tmp/build.html
|
|
|
|
# Add render footer (replace COMPONENT_NAME)
|
|
cat >> /tmp/build.html << 'FOOTER'
|
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
root.render(React.createElement(COMPONENT_NAME));
|
|
</script></body></html>
|
|
FOOTER
|
|
```
|
|
|
|
### Currently Published
|
|
|
|
| Path | Source | Public URL |
|
|
|------|--------|------------|
|
|
| `/opt/artifacts/checkin/` | `arrio/.scratch/checkin_demo_v1.jsx` | `https://alezmad-nuc.tail58f5ad.ts.net/artifacts/checkin/` |
|
|
|
|
## 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
|
|
|
|
## Palmr (File Sharing - Dropbox Alternative)
|
|
|
|
Self-hosted file sharing platform (like WeTransfer/Dropbox) for sending and receiving files.
|
|
|
|
### Access
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| **Public URL** | `https://alezmad-nuc.tail58f5ad.ts.net:8443` |
|
|
| **Local URL** | `http://192.168.1.3:3334` |
|
|
| **Login page** | `/login` |
|
|
| **Container** | `palmr` |
|
|
| **Image** | `kyantech/palmr:latest` |
|
|
| **Network** | `coolify` (IP: `10.0.1.5`) |
|
|
| **Sudo pass** | `7vXHpSTD.` |
|
|
|
|
### Users
|
|
|
|
| Username | Email | Password | Admin |
|
|
|----------|-------|----------|-------|
|
|
| `alezmad` | `agutierrez@mineryreport.com` | *(set via UI)* | Yes |
|
|
| `michi` | `michi@nuc.lan` | `FlexiCar2025.` | No |
|
|
|
|
### Architecture
|
|
|
|
```
|
|
External Browser → Funnel :8443 → 127.0.0.1:3334 → container:5487 (Next.js frontend)
|
|
Funnel :10000 → 127.0.0.1:9379 → container:9379 (MinIO uploads)
|
|
↑ via port-fwd-palmr-minio
|
|
```
|
|
|
|
Palmr uses **presigned URLs** for file uploads — the browser uploads directly to MinIO, NOT through the backend. This means MinIO MUST be reachable from the client's browser.
|
|
|
|
### Key Environment Variables
|
|
|
|
| Variable | Value | Purpose |
|
|
|----------|-------|---------|
|
|
| `STORAGE_URL` | `https://alezmad-nuc.tail58f5ad.ts.net:10000` | Public MinIO URL for presigned upload URLs |
|
|
| `API_BASE_URL` | `http://127.0.0.1:3333` | Internal backend API (Next.js → Fastify) |
|
|
|
|
### Volume Mounts
|
|
|
|
| Volume | Mount Point | Contents |
|
|
|--------|-------------|----------|
|
|
| `5273abd0c...` (anonymous) | `/app/server` | SQLite DB, MinIO data, credentials |
|
|
| `palmr_uploads` | `/app/uploads` | Uploaded files |
|
|
|
|
### Container Recreation
|
|
|
|
```bash
|
|
ssh nuc "docker stop palmr && docker rm palmr && docker run -d \
|
|
--name palmr \
|
|
--network coolify \
|
|
--restart unless-stopped \
|
|
-p 3334:5487 \
|
|
-v 5273abd0c536116056362397cdb568d2eab066b8289412dd91ecce58c174df68:/app/server \
|
|
-v palmr_uploads:/app/uploads \
|
|
-e STORAGE_URL=https://alezmad-nuc.tail58f5ad.ts.net:10000 \
|
|
kyantech/palmr:latest"
|
|
```
|
|
|
|
### Port Forwarder (MinIO)
|
|
|
|
MinIO runs inside the Palmr container on port 9379 (not exposed). A socat forwarder bridges it:
|
|
|
|
```bash
|
|
ssh nuc "docker run -d --name port-fwd-palmr-minio --network coolify -p 9379:9379 \
|
|
alpine/socat tcp-listen:9379,fork,reuseaddr tcp-connect:palmr:9379"
|
|
```
|
|
|
|
### Database Access
|
|
|
|
Palmr uses SQLite at `/app/server/prisma/palmr.db`. Passwords are **bcrypt** hashed.
|
|
|
|
```bash
|
|
# Copy DB locally for queries
|
|
ssh nuc "docker cp palmr:/app/server/prisma/palmr.db /tmp/palmr.db"
|
|
scp nuc:/tmp/palmr.db /tmp/palmr.db
|
|
sqlite3 /tmp/palmr.db "SELECT username, email, isAdmin FROM users;"
|
|
|
|
# Generate bcrypt hash inside container
|
|
ssh nuc "docker exec palmr sh -c 'cd /app/palmr-app && node -e \"const bcrypt = require(\\\"bcryptjs\\\"); console.log(bcrypt.hashSync(\\\"PASSWORD\\\", 10));\"'"
|
|
```
|
|
|
|
### Troubleshooting
|
|
|
|
1. **Upload stuck on loader**: Check `docker logs palmr` for `STORAGE_URL` errors. MinIO must be reachable from the client browser via the presigned URL.
|
|
|
|
2. **Blank page externally**: Verify Funnel is on a supported port (443/8443/10000) and target uses `127.0.0.1` not `localhost`.
|
|
|
|
3. **JWT token invalid after restart**: Expected — users must log in again after container recreation (JWT secret regenerated).
|
|
|
|
4. **Config limits**: File size and storage limits are in `app_configs` table (values in bytes). Update via SQLite, copy DB back, restart.
|
|
|
|
## MinIO Object Storage (Mac ↔ NUC File Transfer)
|
|
|
|
### Architecture
|
|
|
|
MinIO runs on the NUC as S3-compatible object storage. Used for Mac backups, file transfers, and app storage.
|
|
|
|
```
|
|
Mac (mc CLI) → Tailscale → 100.113.153.45:9000 → port-fwd (socat) → minio-xwowg8kswwsocssgocs8ss40:9000
|
|
```
|
|
|
|
### Access
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| **API URL** | `http://100.113.153.45:9000` |
|
|
| **Console URL** | `http://100.113.153.45:9001` |
|
|
| **Access Key** | `minioadmin` |
|
|
| **Secret Key** | `minioadmin` |
|
|
| **Port Forwarder** | `minio-port-fwd` (socat, maps host :9000/:9001 → container) |
|
|
| **Main Container** | `minio-xwowg8kswwsocssgocs8ss40` |
|
|
|
|
### Buckets
|
|
|
|
| Bucket | Purpose |
|
|
|--------|---------|
|
|
| `mac-backups` | Database dumps, tar archives, important backups |
|
|
| `mac-downloads` | Files moved from ~/Downloads for archival |
|
|
| `mac-projects` | Archived project files |
|
|
| `nuc-portal-previews` | NUC Portal screenshot previews |
|
|
| `whyrating` | WhyRating app assets |
|
|
|
|
### Mac CLI (mc)
|
|
|
|
Installed at `/opt/homebrew/bin/mc`, alias `nuc` pre-configured.
|
|
|
|
```bash
|
|
# List buckets
|
|
mc ls nuc/
|
|
|
|
# Upload file
|
|
mc cp file.tar nuc/mac-backups/
|
|
|
|
# Download file
|
|
mc cp nuc/mac-backups/file.tar ./
|
|
|
|
# Sync a directory
|
|
mc mirror ~/path/to/dir nuc/mac-projects/dirname/
|
|
|
|
# Check bucket size
|
|
mc du nuc/mac-backups
|
|
```
|
|
|
|
### nuc-sync CLI Tool
|
|
|
|
Wrapper script at `~/.local/bin/nuc-sync` for daily operations:
|
|
|
|
```bash
|
|
nuc-sync status # Check NUC + MinIO connectivity
|
|
nuc-sync upload file.tar # Upload to mac-backups bucket
|
|
nuc-sync upload file.sql mac-downloads # Upload to specific bucket
|
|
nuc-sync push ~/Desktop/project # rsync to NUC (excludes node_modules, .next, .git)
|
|
nuc-sync pull /opt/backups/mac/x . # rsync from NUC
|
|
nuc-sync list # List all buckets
|
|
nuc-sync list mac-backups # List bucket contents
|
|
```
|
|
|
|
### MinIO MCP Server
|
|
|
|
MCP server for Claude Code integration: `minio-nuc`
|
|
- **Source:** `~/mcp-servers/minio-mcp-server/`
|
|
- **GitHub:** `https://github.com/Rafitis/minio-mcp-server`
|
|
- **Scope:** user (available in all projects)
|
|
- **Tools:** list buckets, upload/download objects, create/delete buckets
|
|
|
|
### rsync Backup Directory
|
|
|
|
```bash
|
|
# NUC backup path (owned by alezmad)
|
|
/opt/backups/mac/
|
|
```
|
|
|
|
### Troubleshooting
|
|
|
|
1. **MinIO unreachable (connection refused)**: The port forwarder may be down:
|
|
```bash
|
|
ssh nuc "docker start minio-port-fwd"
|
|
```
|
|
|
|
2. **mc alias not working**: Reconfigure:
|
|
```bash
|
|
/opt/homebrew/bin/mc alias set nuc http://100.113.153.45:9000 minioadmin minioadmin
|
|
```
|
|
|
|
3. **Console not loading**: Port 9001 needs the port-fwd container running. Check with:
|
|
```bash
|
|
ssh nuc "docker ps | grep minio-port-fwd"
|
|
```
|
|
|
|
## 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
|
|
|
|
```bash
|
|
# 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)
|
|
|
|
## OpenClaw (AI Assistant Gateway)
|
|
|
|
Self-hosted AI assistant gateway running on the NUC via Docker Compose. Connects to messaging platforms (WhatsApp, Telegram, Discord, etc.) and routes messages through Claude.
|
|
|
|
### Access
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| **Control UI (HTTPS)** | `https://alezmad-nuc.tail58f5ad.ts.net:8443` |
|
|
| **Gateway WS** | `ws://192.168.1.3:18789` |
|
|
| **Gateway Token** | `3547c3f2b7b4a33eb077cf804bcca446057f81ba1578b2045dbb3aa4e04346ee` |
|
|
| **Model** | `anthropic/claude-sonnet-4-5-20250929` |
|
|
| **Repo on NUC** | `~/openclaw/` |
|
|
| **Config** | `~/.openclaw/openclaw.json` |
|
|
| **Version** | `2026.2.10` |
|
|
|
|
**⚠️ HTTPS Required:** The Control UI requires a secure context (HTTPS or localhost). Access via Tailscale Serve on port 8443.
|
|
|
|
### Tailscale Serve (HTTPS access)
|
|
|
|
The gateway is exposed via Tailscale Serve (not Funnel - tailnet only, not public):
|
|
|
|
```bash
|
|
# Start HTTPS proxy (requires sudo, must run from NUC terminal)
|
|
ssh nuc
|
|
sudo tailscale serve --bg --https=8443 http://localhost:18789
|
|
# Password: 7vXHpSTD
|
|
```
|
|
|
|
**⚠️ The `--bg` flag makes it persistent.** Without it, Ctrl+C stops the proxy.
|
|
|
|
### Docker Compose Management
|
|
|
|
```bash
|
|
# Start gateway
|
|
ssh nuc "cd ~/openclaw && docker compose up -d openclaw-gateway"
|
|
|
|
# Restart gateway
|
|
ssh nuc "cd ~/openclaw && docker compose restart openclaw-gateway"
|
|
|
|
# View logs
|
|
ssh nuc "docker logs openclaw-openclaw-gateway-1 2>&1 | tail -30"
|
|
|
|
# Run CLI commands (use docker exec for commands that need gateway connection)
|
|
ssh nuc "docker exec openclaw-openclaw-gateway-1 node dist/index.js <command> --token 3547c3f2b7b4a33eb077cf804bcca446057f81ba1578b2045dbb3aa4e04346ee --url ws://127.0.0.1:18789"
|
|
|
|
# Run CLI commands that don't need gateway (use docker compose run)
|
|
ssh nuc "cd ~/openclaw && script -qc 'docker compose run --rm openclaw-cli <command>' /dev/null"
|
|
```
|
|
|
|
### Device Pairing
|
|
|
|
When the Control UI shows "pairing required", approve the pending device:
|
|
|
|
```bash
|
|
# List pending devices
|
|
ssh nuc "docker exec openclaw-openclaw-gateway-1 node dist/index.js devices list --token 3547c3f2b7b4a33eb077cf804bcca446057f81ba1578b2045dbb3aa4e04346ee --url ws://127.0.0.1:18789"
|
|
|
|
# Approve a device (use requestId from the list)
|
|
ssh nuc "docker exec openclaw-openclaw-gateway-1 node dist/index.js devices approve <requestId> --token 3547c3f2b7b4a33eb077cf804bcca446057f81ba1578b2045dbb3aa4e04346ee --url ws://127.0.0.1:18789"
|
|
```
|
|
|
|
**Dashboard URL with embedded token (auto-authenticates):**
|
|
```
|
|
https://alezmad-nuc.tail58f5ad.ts.net:8443/#token=3547c3f2b7b4a33eb077cf804bcca446057f81ba1578b2045dbb3aa4e04346ee
|
|
```
|
|
|
|
### Channel Plugins
|
|
|
|
Channels are plugins that must be enabled before use. 35 available, 4 loaded by default.
|
|
|
|
```bash
|
|
# List all plugins
|
|
ssh nuc "script -qc 'cd ~/openclaw && docker compose run --rm openclaw-cli plugins list' /dev/null"
|
|
|
|
# Enable a channel plugin
|
|
ssh nuc "script -qc 'cd ~/openclaw && docker compose run --rm openclaw-cli plugins enable <plugin-id>' /dev/null"
|
|
|
|
# Then restart gateway
|
|
ssh nuc "cd ~/openclaw && docker compose restart openclaw-gateway"
|
|
```
|
|
|
|
**Currently enabled channels:** WhatsApp (linked and active)
|
|
|
|
**Available channel plugins:**
|
|
| Plugin ID | Channel |
|
|
|-----------|---------|
|
|
| `whatsapp` | WhatsApp |
|
|
| `telegram` | Telegram |
|
|
| `discord` | Discord |
|
|
| `slack` | Slack |
|
|
| `signal` | Signal |
|
|
| `matrix` | Matrix |
|
|
| `msteams` | Microsoft Teams |
|
|
| `googlechat` | Google Chat |
|
|
| `imessage` | iMessage |
|
|
| `irc` | IRC |
|
|
|
|
### Setting Up Channels That Require QR Codes (WhatsApp, etc.)
|
|
|
|
**⚠️ The CLI needs a TTY for QR display.** Cannot run directly via `ssh nuc "command"`. Use `script` to fake a TTY and capture output:
|
|
|
|
```bash
|
|
# Step 1: Enable the plugin
|
|
ssh nuc "script -qc 'cd ~/openclaw && docker compose run --rm openclaw-cli plugins enable whatsapp' /dev/null"
|
|
|
|
# Step 2: Restart gateway
|
|
ssh nuc "cd ~/openclaw && docker compose restart openclaw-gateway"
|
|
|
|
# Step 3: Run login with TTY capture (captures QR to file)
|
|
ssh nuc "script -q /tmp/openclaw-qr.txt -c 'cd ~/openclaw && docker compose run --rm openclaw-cli channels login'"
|
|
|
|
# Step 4: If QR needs to be viewed remotely, copy and render as image
|
|
scp nuc:/tmp/openclaw-qr.txt /tmp/
|
|
# Then use Python + Pillow to convert Unicode block chars to PNG
|
|
```
|
|
|
|
**QR to PNG conversion (run locally on Mac):**
|
|
```python
|
|
from PIL import Image
|
|
import re
|
|
|
|
with open("/tmp/openclaw-qr.txt", "rb") as f:
|
|
content = f.read().decode("utf-8", errors="replace")
|
|
|
|
lines = content.split("\n")
|
|
qr_lines = []
|
|
for l in lines:
|
|
clean = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", l).replace(chr(0), "")
|
|
if any(c in clean for c in ["\u2584", "\u2588", "\u2580"]):
|
|
qr_lines.append(clean)
|
|
|
|
scale = 10
|
|
width = max(len(l) for l in qr_lines)
|
|
height = len(qr_lines) * 2
|
|
img = Image.new("RGB", (width * scale, height * scale), "white")
|
|
|
|
for row_idx, line in enumerate(qr_lines):
|
|
for col_idx, ch in enumerate(line):
|
|
top_black = ch in ["\u2588", "\u2580"]
|
|
bot_black = ch in ["\u2588", "\u2584"]
|
|
for dy in range(scale):
|
|
for dx in range(scale):
|
|
if top_black:
|
|
img.putpixel((col_idx*scale+dx, row_idx*2*scale+dy), (0,0,0))
|
|
if bot_black:
|
|
img.putpixel((col_idx*scale+dx, (row_idx*2+1)*scale+dy), (0,0,0))
|
|
|
|
img.save("/tmp/qr.png")
|
|
```
|
|
|
|
**Alternative: User scans directly** — If the user has SSH terminal access, they can run `channels login` interactively and scan the QR from their terminal.
|
|
|
|
### Anthropic Authentication
|
|
|
|
Uses a Claude Code OAuth token (valid 1 year). Set in both config and docker-compose env:
|
|
|
|
**Config (`~/.openclaw/openclaw.json`):**
|
|
```json
|
|
{
|
|
"env": {
|
|
"ANTHROPIC_API_KEY": "sk-ant-oat01-..."
|
|
}
|
|
}
|
|
```
|
|
|
|
**Docker env (`~/openclaw/.env`):**
|
|
```
|
|
ANTHROPIC_API_KEY=sk-ant-oat01-...
|
|
```
|
|
|
|
**To regenerate token (run on Mac where `claude` CLI is installed):**
|
|
```bash
|
|
claude setup-token
|
|
# Copy the output token and update both config + .env on NUC
|
|
```
|
|
|
|
### Config Schema (current)
|
|
|
|
```json
|
|
{
|
|
"agents": {
|
|
"defaults": {
|
|
"model": {
|
|
"primary": "anthropic/claude-sonnet-4-5-20250929"
|
|
}
|
|
}
|
|
},
|
|
"gateway": {
|
|
"port": 18789,
|
|
"mode": "local",
|
|
"bind": "lan",
|
|
"auth": {
|
|
"mode": "token"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**⚠️ Config gotchas:**
|
|
- `agent.model` is **legacy** — use `agents.defaults.model.primary` instead
|
|
- Run `docker exec openclaw-openclaw-gateway-1 node dist/index.js doctor --fix` to migrate legacy keys
|
|
- The `gateway.pairing` key does NOT exist — device pairing is managed via the `devices` CLI, not config
|
|
|
|
### Troubleshooting
|
|
|
|
1. **"control ui requires HTTPS or localhost"**: Access via `https://alezmad-nuc.tail58f5ad.ts.net:8443` (Tailscale Serve), NOT `http://192.168.1.3:18789`
|
|
|
|
2. **"pairing required"**: Approve the device via `devices approve` command (see Device Pairing section above)
|
|
|
|
3. **"unauthorized: gateway token missing"**: Use the dashboard URL with `#token=...` hash to auto-authenticate
|
|
|
|
4. **CLI commands fail with "gateway closed"**: Use `docker exec` into the running gateway container instead of `docker compose run` (the CLI container can't reach the gateway on its internal Docker IP)
|
|
|
|
5. **Config "invalid" after edit**: Run `doctor --fix` inside the gateway container to clean up
|
|
|
|
6. **Channel "unsupported"**: Enable the plugin first with `plugins enable <id>`, then restart gateway
|
|
|
|
7. **CLAUDE_AI_SESSION_KEY warnings**: Harmless — these are for Claude web session auth which isn't used when using API key
|