Initial commit - NUC server configuration and docs

- 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>
This commit is contained in:
Alejandro Gutiérrez
2026-02-01 20:49:20 +00:00
commit 390eda1595
25 changed files with 3664 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
# Task Runners Health Check Fix
**Date:** 2026-02-01 13:20
**Context:** Fixed unhealthy status on n8n task-runners container
## Problem
The `task-runners-uk0o04o0g84s4sc80kkoooc0` container was showing as unhealthy with 60,395+ consecutive failures.
**Root Cause:** Health check was configured to hit `/` but the actual health endpoint is `/healthz`
| Configured | Actual Endpoint |
|------------|-----------------|
| `http://127.0.0.1:5680/` | Returns 404 |
| `http://127.0.0.1:5680/healthz` | Returns `{"status":"ok"}` |
## Solution
Updated Coolify's docker-compose configuration for the n8n service:
```bash
docker exec coolify php artisan tinker --execute="
use App\Models\Service;
\$service = Service::find(4);
\$compose = \$service->docker_compose_raw;
\$compose = str_replace(
\"'wget -qO- http://127.0.0.1:5680/'\",
\"'wget -qO- http://127.0.0.1:5680/healthz'\",
\$compose
);
\$service->docker_compose_raw = \$compose;
\$service->save();
"
```
Then restarted the service:
```bash
docker exec coolify php artisan tinker --execute="
use App\Models\Service;
use App\Actions\Service\StopService;
use App\Actions\Service\StartService;
\$service = Service::find(4);
StopService::run(\$service);
StartService::run(\$service);
"
```
## Result
- Container now shows as **healthy**
- Health check correctly hitting `/healthz` endpoint
## Related
- n8n Service ID: 4
- n8n Service UUID: uk0o04o0g84s4sc80kkoooc0
- Coolify: http://192.168.1.3:8000

View File

@@ -0,0 +1,132 @@
# LiquidGym & Knosia Migration to NUC
**Date:** 2026-02-01 16:45
**Context:** Migrated LiquidGym MySQL and Knosia PostgreSQL from local Docker to NUC Coolify
## NUC MySQL Connection
| Property | Value |
|----------|-------|
| Host | 192.168.1.3 |
| Port | 3306 |
| Database | liquidgym |
| User | liquidgym |
| Password | liquidgym_nuc_2026 |
| Root Password | liquidgym_root_nuc_2026 |
| Coolify UUID | hgwcgs4oswwc8scg080scoo4 |
| External URL | mysql://liquidgym:liquidgym_nuc_2026@192.168.1.3:3306/liquidgym |
## Local Volumes (Safe to Delete)
These local volumes contained LiquidGym MySQL data and are now migrated:
- `infra_mysql_data` - Active data (migrated)
- `infra_mysql_logs` - Logs
- `liquidgym_mysql_data` - Stale duplicate
- `liquidgym_mysql_logs` - Stale duplicate
## Project Updates
The liquidgym project at `/Users/agutierrez/Desktop/liquidgym` has been updated:
- Added `.env.nuc` with NUC MySQL configuration
- Local MySQL in docker-compose.yml can still be used for isolated testing
## Usage
```bash
# Use NUC MySQL (remote)
cd ~/Desktop/liquidgym/infra
cp .env.nuc .env
docker compose up cloudbeaver -d
# Use local MySQL (isolated)
docker compose --profile tier1 up mysql -d
```
---
## Knosia (LiquidRender) PostgreSQL
| Property | Value |
|----------|-------|
| Host | 192.168.1.3 |
| Port | 5442 |
| Database | knosia |
| User | knosia |
| Password | knosia_nuc_2026 |
| Coolify UUID | ik80skko0008w4000c4w40os |
| pgvector | 0.8.1 enabled |
| External URL | postgresql://knosia:knosia_nuc_2026@192.168.1.3:5442/knosia |
## NUC MinIO for Knosia
| Property | Value |
|----------|-------|
| Endpoint | http://192.168.1.3:9000 |
| Bucket | knosia |
| Access Key | Dt6yvE0MTH7N4GBz |
| Secret Key | iz6Fl5aAixTgyzRcRLZrjmCM02CBCdmZ |
| Coolify Service | dg4wkgg8skcssww0040sgk80 |
## Project Updates
- LiquidGym: `/Desktop/liquidgym/infra/.env.nuc`
- LiquidRender (Knosia): `/Desktop/liquidrender/.env.nuc`
---
## LiquidGym PostgreSQL (Test Datasets)
| Property | Value |
|----------|-------|
| Host | 192.168.1.3 |
| Port | 5433 |
| User | postgres |
| Password | postgres |
| Coolify UUID | x4kk8g4k8w4g0cw480w84g4g |
**Databases migrated (~520MB total):**
| Database | Size | Description |
|----------|------|-------------|
| employees | 334 MB | HR dataset |
| adventureworks | 114 MB | Sales/HR sample |
| lego | 43 MB | LEGO sets |
| pagila | 17 MB | DVD rental |
| netflix | 11 MB | Netflix titles |
| chinook | 10 MB | Music store |
| northwind | 8 MB | Classic sample |
---
## Google Reviews Scraper PostgreSQL
| Property | Value |
|----------|-------|
| Host | 192.168.1.3 |
| Port | 5437 |
| Database | scraper |
| User | scraper |
| Password | scraper_nuc_2026 |
| Coolify UUID | g4s8w4csk8s8ocswg48kkogo |
**Data migrated:**
| Table | Rows |
|-------|------|
| jobs | 21 |
| gbp_categories | 4,141 |
| canary_results | 25 |
**Project files:**
- `.env.nuc` - NUC database config
- `docker-compose.nuc.yml` - Override to use NUC DB
**Usage:**
```bash
cd ~/Desktop/google-reviews-scraper-pro
cp .env.nuc .env
docker compose -f docker-compose.production.yml -f docker-compose.nuc.yml up -d
```
## Related
- CloudBeaver on NUC: http://192.168.1.3:8087
- MinIO Console: http://192.168.1.3:9001
- Coolify Dashboard: http://192.168.1.3:8000

View File

@@ -0,0 +1,124 @@
# LiquidGym Database Engines Reference
**Date:** 2026-02-01 17:15
**Context:** Reference guide for LiquidGym's multi-engine SQL testing infrastructure
## Overview
LiquidGym is a multi-database testing environment designed to verify that analytical queries work identically across different database engines. This ensures engine-agnostic query generation.
## Engine Tiers
### Core (Always Started)
| Engine | Image | Port | Purpose |
|--------|-------|------|---------|
| PostgreSQL 16 | `postgres:16` | 5433 | Primary test database with sample datasets |
| CloudBeaver | `dbeaver/cloudbeaver` | 8978 | Web-based database UI |
### Tier 1: Essential Engines
Different SQL dialects for cross-engine testing.
| Engine | Image | Port | Description |
|--------|-------|------|-------------|
| **ClickHouse** | `clickhouse/clickhouse-server` | 8123 (HTTP), 9000 (Native) | Column-oriented OLAP database. Extremely fast for analytics on billions of rows. Used by Cloudflare, Uber, eBay. Best for: logs, metrics, time-series analytics. |
| **MySQL 8** | `mysql:8` | 3306 | World's most popular open-source RDBMS. Tests MySQL-specific SQL dialect. |
### Tier 2: Distributed & Specialized
| Engine | Image | Port | Description |
|--------|-------|------|-------------|
| **Trino** | `trinodb/trino` | 8084 | Distributed SQL query engine. Queries data across multiple sources (Postgres, S3, Kafka) with single SQL. No storage - just a query layer. |
| **StarRocks** | `starrocks/allin1-ubuntu` | 9030 (MySQL), 8030 (HTTP) | MPP analytics database. Sub-second queries on large datasets. Powers BI dashboards. Fork of Apache Doris with performance improvements. |
| **TimescaleDB** | `timescale/timescaledb:latest-pg16` | 5434 | PostgreSQL extension for time-series data. Auto-partitions by time. Perfect for IoT, metrics, events. Familiar Postgres SQL. |
### Tier 3: Advanced/Specialized
| Engine | Image | Port | Description |
|--------|-------|------|-------------|
| **Apache Doris** | `apache/doris:doris-all-in-one-2.1.0` | 9031 (MySQL), 8031 (HTTP) | Real-time analytical database. MySQL-compatible. Good for real-time dashboards and ad-hoc queries. |
| **Apache Druid** | `apache/druid:26.0.0` | 8888 | Real-time OLAP for sub-second slice-and-dice analytics. Powers Airbnb, Netflix, Alibaba dashboards. Best for: high-concurrency, low-latency queries. |
| **Apache Spark** | `apache/spark:3.5.0` | 7077 (Master), 8085 (UI) | Distributed compute engine for big data. ML pipelines, ETL, batch processing. Overkill for small datasets. |
## Observability Stack
| Tool | Image | Port | Description |
|------|-------|------|-------------|
| **Grafana** | `grafana/grafana` | 3005 | Visualization & dashboards. Query any data source, create alerts. Login: admin/liquidgym |
| **Prometheus** | `prom/prometheus` | 9090 | Metrics collection & alerting. Scrapes metrics from all engines. |
| **Redis** | `redis:7-alpine` | 6379 | In-memory cache. Used for session storage, caching query results. |
## Usage
```bash
cd ~/Desktop/liquidgym/infra
# Start core only (Postgres + CloudBeaver)
docker compose up -d
# Start with Tier 1 engines (+ ClickHouse, MySQL)
docker compose --profile tier1 up -d
# Start with Tier 2 engines (+ Trino, StarRocks, TimescaleDB)
docker compose --profile tier2 up -d
# Start with Tier 3 engines (+ Doris, Spark)
docker compose --profile tier3 up -d
# Start observability stack (+ Prometheus, Grafana, Redis)
docker compose --profile observability up -d
# Start everything
docker compose --profile all up -d
# Load sample datasets
docker compose --profile loader up
```
## Sample Datasets
| Dataset | Description | Tables |
|---------|-------------|--------|
| **Northwind** | Classic MS Access sample - orders, products, customers | 14 |
| **Pagila** | DVD rental store (PostgreSQL port of Sakila) | 29 |
| **Chinook** | Digital media store - artists, albums, tracks | 11 |
| **AdventureWorks** | Microsoft sample - sales, HR, production | 68 |
| **Employees** | Large HR dataset with 300K+ employee records | 6 |
| **LEGO** | LEGO sets, parts, themes, colors | 8 |
| **Netflix** | Netflix titles catalog | 1 |
## When to Use Each Engine
| Use Case | Recommended Engine |
|----------|-------------------|
| General OLTP | PostgreSQL, MySQL |
| Analytics on large datasets | ClickHouse, StarRocks |
| Time-series / IoT | TimescaleDB |
| Real-time dashboards | Druid, Doris |
| Query across multiple DBs | Trino |
| Big data / ML pipelines | Spark |
| Caching | Redis |
## Resource Requirements
| Profile | RAM | CPU | Disk |
|---------|-----|-----|------|
| Core | 1GB | 1 | 1GB |
| + Tier 1 | 6GB | 2 | 3GB |
| + Tier 2 | 10GB | 4 | 5GB |
| + Tier 3 | 16GB+ | 6+ | 10GB+ |
| + Observability | +2GB | +1 | +1GB |
## NUC Migration Status
The following have been migrated to NUC and no longer need local volumes:
| Service | NUC Location | Status |
|---------|--------------|--------|
| PostgreSQL (datasets) | 192.168.1.3:5433 | Migrated |
| MySQL | 192.168.1.3:3306 | Migrated |
Tier 1-3 engines remain local-only for development testing.
## Related
- LiquidGym project: `~/Desktop/liquidgym/infra/`
- Docker Compose: `~/Desktop/liquidgym/infra/docker-compose.yml`
- Datasets: `~/Desktop/liquidgym/infra/datasets/`

27
.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# Ignore sensitive files
*.key
*.pem
secrets/
.env
# Sensitive artifacts (tokens, credentials)
.artifacts/*-token*.md
.artifacts/*-api-token*.md
.artifacts/wireguard*.conf
# Claude session files
.claude/
# OS files
.DS_Store
Thumbs.db
# Editor files
*.swp
*.swo
*~
# Temporary files
*.tmp
*.log
tmp/

28
.mcp.json Normal file
View File

@@ -0,0 +1,28 @@
{
"mcpServers": {
"coolify": {
"command": "npx",
"args": ["-y", "@masonator/coolify-mcp@latest"],
"env": {
"COOLIFY_BASE_URL": "http://192.168.1.3:8000",
"COOLIFY_ACCESS_TOKEN": "8|UZt8zhHTMDJofF0OmONSegWoQTDkXpLv1TWDnM1X"
}
},
"playwriter-local": {
"command": "npx",
"args": ["playwriter"]
},
"playwriter-nuc-01": {
"command": "npx",
"args": ["-y", "playwriter", "--host", "192.168.1.3", "--token", "nuc-browser-token"]
},
"browsermcp": {
"command": "npx",
"args": ["@browsermcp/mcp@latest"]
},
"chrome-devtools-nuc-01": {
"command": "npx",
"args": ["-y", "chrome-devtools-mcp@latest", "--wsEndpoint", "ws://192.168.1.3:19988/cdp"]
}
}
}

696
CLAUDE.md Normal file
View File

@@ -0,0 +1,696 @@
# NUC Server - Claude Code Instructions
## Server Access
The NUC server is accessible via SSH:
```bash
ssh nuc
```
**Connection Details:**
- Hostname: `192.168.1.3`
- User: `alezmad`
- SSH Key: `~/.ssh/id_ed25519_nuc`
## Service Management
### Coolify (Primary Service Manager)
All services are managed through Coolify at `http://192.168.1.3:8000`
**Prefer using Coolify MCP** (`mcp__coolify__*`) for service management - it's faster and more reliable than SSH commands.
### ⚠️ STRICT RULE: Container Deployment Priority
**ALWAYS attempt Coolify first when adding any container/service:**
1. **First:** Try `mcp__coolify__service(action="create", type="<service-name>", ...)`
2. **If type invalid:** Deploy via docker-compose in Coolify using `docker_compose_raw`
3. **Last resort:** Direct Docker commands via SSH (only if Coolify can't handle it)
```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__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:**
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
| 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:
```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. **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`
## 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:**
```bash
# Expose port 3000 via Funnel
ssh nuc "tailscale funnel 3000"
# Or with background (use screen/tmux)
ssh nuc "screen -dmS funnel tailscale funnel 3000"
```
### Current Domain Routes
| Domain | Destination | Method |
|--------|-------------|--------|
| whyrating.com | `nuc-tailscale.tail58f5ad.ts.net` | Namecheap 301 redirect |
### Adding a New Domain
1. **Check availability** (if needed):
```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>
```
## 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
```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)

72
NEXT_STEPS.md Normal file
View File

@@ -0,0 +1,72 @@
# Next Steps
## Immediate Tasks
### 1. Documentation Migration
- [ ] Create documentation structure in Outline (http://192.168.1.3:3080)
- [ ] Migrate guidelines from Homepage to Outline
- [ ] Remove guidelines from Homepage after migration
### 2. Cleanup
- [ ] Revoke duplicate "Claude MCP Server" API token in Coolify (Settings → Keys & Tokens → API Tokens)
## Configuration Tasks
### 3. Test Coolify MCP
- [ ] Start a Claude Code session from `~/Desktop/nuc`
- [ ] Verify Coolify MCP connects with the API token
- [ ] Test listing services, deployments, and servers
### 4. SSH Setup (if not done)
```bash
cd ~/Desktop/nuc
./setup-ssh.sh
```
## Future Improvements
### Infrastructure
- [ ] Configure Authentik as central identity provider
- [ ] Set up automated backups verification in Kopia
- [ ] Add more services to Uptime Kuma monitoring
- [ ] Configure ntfy notifications for critical alerts
### Security
- [ ] Enable HTTPS with Let's Encrypt certificates
- [ ] Set up Vaultwarden backup strategy
- [ ] Review and restrict Coolify API token IP allowlist
- [ ] Audit exposed ports and services
### Automation
- [ ] Create n8n workflows for common tasks
- [ ] Set up automated health checks
- [ ] Configure Coolify webhooks for deployment notifications
### Documentation
- [ ] Document backup/restore procedures in Outline
- [ ] Create runbooks for common incidents
- [ ] Document service dependencies and startup order
- [ ] Add network diagram to documentation
## Service-Specific Tasks
### Outline
- [ ] Configure S3 storage with MinIO for attachments
- [ ] Set up collections and permissions structure
- [ ] Import existing documentation
### Gitea
- [ ] Configure repository mirroring (if needed)
- [ ] Set up CI/CD with Gitea Actions
- [ ] Configure webhook integrations
### n8n
- [ ] Create workflow templates
- [ ] Set up credentials for external services
- [ ] Configure error notifications
## Notes
- Coolify API Token: Configured in `.claude/settings.json`
- All services accessible at `192.168.1.3` on their respective ports
- SSH access: `ssh nuc` (after running setup-ssh.sh)

151
README.md Normal file
View File

@@ -0,0 +1,151 @@
# NUC Home Server
Personal home server running on Intel NUC at `192.168.1.3`
## Quick Access
| Service | URL | Description |
|---------|-----|-------------|
| **Homepage** | http://192.168.1.3:3000 | Dashboard with all services |
| **Coolify** | http://192.168.1.3:8000 | Service deployment & management |
| **Gitea** | http://192.168.1.3:3030 | Git repository hosting |
| **Outline** | http://192.168.1.3:3080 | Documentation wiki (login via Gitea) |
| **n8n** | http://192.168.1.3:5678 | Workflow automation |
| **Vaultwarden** | http://192.168.1.3:8222 | Password manager (Bitwarden compatible) |
| **Ntfy** | http://192.168.1.3:8333 | Push notifications |
| **Uptime Kuma** | http://192.168.1.3:3001 | Service monitoring |
## SSH Access
```bash
# Connect to the server
ssh nuc
# Or explicitly
ssh alezmad@192.168.1.3
```
Make sure your SSH config includes:
```
Host nuc
HostName 192.168.1.3
User alezmad
IdentityFile ~/.ssh/id_ed25519_nuc
```
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ NUC Server │
│ 192.168.1.3 │
├─────────────────────────────────────────────────────────────┤
│ Coolify (PaaS) │
│ ├── Traefik (Reverse Proxy) :80/:443 │
│ ├── Services: │
│ │ ├── Homepage (Dashboard) │
│ │ ├── Gitea (Git + OAuth2 Provider) │
│ │ ├── Outline (Wiki/Docs) │
│ │ ├── n8n (Automation) │
│ │ ├── Vaultwarden (Passwords) │
│ │ ├── Ntfy (Notifications) │
│ │ ├── MinIO (S3 Storage) │
│ │ ├── FileBrowser │
│ │ └── Authentik (Identity - partially configured) │
│ └── Databases (PostgreSQL, Redis per service) │
├─────────────────────────────────────────────────────────────┤
│ Standalone Containers: │
│ ├── Uptime Kuma (Monitoring) │
│ ├── Dozzle (Log Viewer) │
│ ├── Adminer (DB Admin) │
│ └── Kopia (Backups) │
└─────────────────────────────────────────────────────────────┘
```
## Service Details
### Coolify
- **URL:** http://192.168.1.3:8000
- **Purpose:** Self-hosted PaaS for deploying and managing all services
- **Login:** Admin account configured during setup
### Gitea
- **URL:** http://192.168.1.3:3030
- **SSH:** Port 22222
- **Purpose:** Git repository hosting, also serves as OAuth2 provider for Outline
- **Admin:** alezmad
### Outline
- **URL:** http://192.168.1.3:3080
- **Purpose:** Team documentation and wiki
- **Auth:** Login via Gitea (OIDC)
- **Note:** Uses nginx proxy to strip HSTS headers
### n8n
- **URL:** http://192.168.1.3:5678
- **Purpose:** Workflow automation (like Zapier)
- **Runners:** Has dedicated task runners container
### Vaultwarden
- **URL:** http://192.168.1.3:8222
- **Purpose:** Self-hosted Bitwarden-compatible password manager
- **Clients:** Use any Bitwarden client, point to this URL
### Ntfy
- **URL:** http://192.168.1.3:8333
- **Purpose:** Push notifications to mobile/desktop
- **Mobile App:** Available on F-Droid and Play Store
### MinIO
- **Console:** http://192.168.1.3:9001
- **API:** http://192.168.1.3:9000
- **Purpose:** S3-compatible object storage
### Monitoring & Tools
- **Uptime Kuma:** http://192.168.1.3:3001 - Service health monitoring
- **Dozzle:** http://192.168.1.3:9999 - Real-time Docker log viewer
- **Adminer:** http://192.168.1.3:8088 - Database management UI
- **Kopia:** http://192.168.1.3:51515 - Backup management
## Common Tasks
### View Service Logs
```bash
ssh nuc "docker logs <container_name> -f --tail 100"
```
### Restart a Service
```bash
ssh nuc "docker restart <container_name>"
```
### Check All Services
```bash
ssh nuc "docker ps --format 'table {{.Names}}\t{{.Status}}'"
```
### Backup Consideration
- Kopia handles automated backups
- Access at http://192.168.1.3:51515 to manage snapshots
## Troubleshooting
### Service Not Accessible
1. Check if container is running: `ssh nuc "docker ps | grep <service>"`
2. Check logs: `ssh nuc "docker logs <container> --tail 50"`
3. Check port forwarding container if applicable
### After Coolify Redeploy
Containers may be in "Created" state. Start manually:
```bash
ssh nuc "docker start <container_name>"
```
### HTTPS/SSL Issues
Some browsers cache HSTS. Clear at `chrome://net-internals/#hsts`
## Files in This Directory
- `CLAUDE.md` - Instructions for Claude Code AI assistant
- `README.md` - This file (human documentation)
- `.claude/settings.json` - MCP server configuration for Claude Code

171
commands.md Normal file
View File

@@ -0,0 +1,171 @@
# Quick Reference Commands
## SSH Connection
```bash
ssh nuc
```
## Docker Commands
### List Running Containers
```bash
ssh nuc "docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'"
```
### View Logs
```bash
# Last 100 lines
ssh nuc "docker logs <container> --tail 100"
# Follow logs
ssh nuc "docker logs <container> -f"
# With timestamps
ssh nuc "docker logs <container> --tail 50 -t"
```
### Container Management
```bash
# Restart
ssh nuc "docker restart <container>"
# Stop
ssh nuc "docker stop <container>"
# Start
ssh nuc "docker start <container>"
# Remove
ssh nuc "docker rm <container>"
```
### Execute Commands in Container
```bash
ssh nuc "docker exec <container> <command>"
# Interactive shell
ssh nuc -t "docker exec -it <container> /bin/sh"
```
### Network Inspection
```bash
# Find container's network
ssh nuc "docker inspect <container> --format '{{range .NetworkSettings.Networks}}{{.NetworkID}}{{end}}'"
# List networks
ssh nuc "docker network ls"
```
## Coolify Management
### Restart a Service
```bash
ssh nuc "docker exec coolify php artisan tinker --execute=\"
use App\\Actions\\Service\\StartService;
use App\\Models\\Service;
\\\$service = Service::find(<SERVICE_ID>);
StartService::run(\\\$service);
\""
```
### List Services
```bash
ssh nuc "docker exec coolify php artisan tinker --execute=\"
use App\\Models\\Service;
Service::all(['id','name','uuid'])->toJson();
\""
```
### Update Environment Variable
```bash
ssh nuc "docker exec coolify php artisan tinker --execute=\"
use App\\Models\\EnvironmentVariable;
\\\$var = EnvironmentVariable::where('key', 'VAR_NAME')
->where('resourceable_id', <SERVICE_ID>)
->where('resourceable_type', 'App\\\\\\\\Models\\\\\\\\Service')
->first();
\\\$var->value = encrypt('new_value');
\\\$var->save();
echo 'Updated';
\""
```
## Port Forwarding
### Create socat forwarder
```bash
ssh nuc "docker run -d \
--name port-fwd-<name> \
--network <network_id> \
-p <external_port>:<internal_port> \
alpine/socat \
tcp-listen:<internal_port>,fork,reuseaddr \
tcp-connect:<container>:<container_port>"
```
### Create nginx forwarder (with HSTS stripping)
```bash
ssh nuc "docker run -d \
--name port-fwd-<name> \
--network <network_id> \
-p <external_port>:<proxy_port> \
nginx:alpine \
sh -c 'echo \"server { listen <proxy_port>; location / { proxy_pass http://<container>:<port>; proxy_set_header Host \\\$host; proxy_hide_header Strict-Transport-Security; } }\" > /etc/nginx/conf.d/default.conf && nginx -g \"daemon off;\"'"
```
## Service URLs
| Service | URL |
|---------|-----|
| Homepage | http://192.168.1.3:3000 |
| Coolify | http://192.168.1.3:8000 |
| Gitea | http://192.168.1.3:3030 |
| Outline | http://192.168.1.3:3080 |
| n8n | http://192.168.1.3:5678 |
| Vaultwarden | http://192.168.1.3:8222 |
| Ntfy | http://192.168.1.3:8333 |
| MinIO | http://192.168.1.3:9001 |
| Uptime Kuma | http://192.168.1.3:3001 |
| Dozzle | http://192.168.1.3:9999 |
| Adminer | http://192.168.1.3:8088 |
| Kopia | http://192.168.1.3:51515 |
| FileBrowser | http://192.168.1.3:8085 |
## Backup & Recovery
### Kopia Web UI
http://192.168.1.3:51515
### Manual Backup Check
```bash
ssh nuc "docker exec kopia kopia snapshot list"
```
## Troubleshooting
### Check disk space
```bash
ssh nuc "df -h"
```
### Check memory
```bash
ssh nuc "free -h"
```
### Check Docker disk usage
```bash
ssh nuc "docker system df"
```
### Clean up Docker
```bash
# Remove unused images
ssh nuc "docker image prune -a"
# Remove unused volumes
ssh nuc "docker volume prune"
# Full cleanup
ssh nuc "docker system prune -a"
```

279
docs/architecture.md Normal file
View File

@@ -0,0 +1,279 @@
# NUC Infrastructure Architecture
**Date:** 2026-02-01
**Context:** Secure self-hosted infrastructure with Tailscale Funnel for public access and Tailscale mesh for private admin access. Designed to bypass Spanish ISP blocks and handle dynamic IPs.
---
## Architecture Diagram
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ INTERNET │
└─────────────────────────────────────────────────────────────────────────────┘
│ │
│ Public Traffic │ Admin Traffic
▼ ▼
┌───────────────────────────────┐ ┌─────────────────────────────────────┐
│ whyrating.com │ │ TAILSCALE MESH │
│ │ │ │ (encrypted, private) │
│ ▼ │ │ │
│ ┌─────────────────────┐ │ │ ┌───────────┐ ┌───────────┐ │
│ │ Namecheap DNS │ │ │ │ Your Mac │◄──►│ NUC │ │
│ │ (301 redirect) │ │ │ │100.x.x.2 │ │100.x.x.1 │ │
│ └──────────┬──────────┘ │ │ └───────────┘ └───────────┘ │
│ │ │ │ ▲ ▲ │
│ ▼ │ │ │ Anywhere │ │
│ ┌─────────────────────┐ │ │ │ in world │ │
│ │ Tailscale Funnel │ │ └─────────┴────────────────┴─────────┘
│ │ nuc-tailscale.ts.net│◄─────┼────────────────────────┘
│ └──────────┬──────────┘ │
│ │ HTTPS/443 │
└─────────────┼─────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ NUC SERVER │
│ 192.168.1.3 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ SECURITY LAYER │ │
│ │ ┌─────────────┐ │ │
│ │ │ CrowdSec │ ← Blocks malicious IPs, DDoS protection │ │
│ │ │ :8083 │ │ │
│ │ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ COOLIFY (Docker Orchestrator) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ TRAEFIK (Reverse Proxy) │ │ │
│ │ │ Routes by domain │ │ │
│ │ └───────────┬─────────────────┬─────────────────┬─────────────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ PUBLIC WEBSITES │ │ │
│ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │
│ │ │ │Homepage │ │ App A │ │ App B │ │ App C │ │ │ │
│ │ │ │ :3000 │ │ :3001 │ │ :3002 │ │ :3003 │ │ │ │
│ │ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ │
│ │ │ (internal ports only, not exposed) │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ PRIVATE SERVICES (Tailscale only) │ │ │
│ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │
│ │ │ │Coolify │ │ Gitea │ │ MinIO │ │Postgres │ │ │ │
│ │ │ │ :8000 │ │ :3030 │ │ :9001 │ │ :5432 │ │ │ │
│ │ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │
│ │ │ │Authentik│ │ n8n │ │Vaultwrdn│ │ Outline │ │ │ │
│ │ │ │ :9090 │ │ :5678 │ │ :8222 │ │ :3080 │ │ │ │
│ │ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## Traffic Flows
### Public Website Access
```
User → whyrating.com → Namecheap 301 → nuc-tailscale.ts.net → Tailscale Funnel
→ CrowdSec → Traefik → Container (internal port)
```
### Admin Access (Remote)
```
Your Mac → Tailscale mesh (encrypted) → NUC Tailscale IP (100.x.x.x)
→ Direct access to any port (8000, 22, etc.)
```
### Admin Access (Home Network)
```
Your Mac → Local network → 192.168.1.3 → Any port
```
---
## Component Summary
| Component | Port | Access | Purpose |
|-----------|------|--------|---------|
| **Tailscale Funnel** | 443 | Public | Single internet entry point |
| **CrowdSec** | 8083 | Private | DDoS/attack protection |
| **Traefik** | 80/443 | Internal | Routes domains to containers |
| **Homepage** | 3000 | Via Funnel | Public dashboard |
| **Coolify** | 8000 | Tailscale only | Container management |
| **Databases** | various | Tailscale only | Data storage |
---
## Security Layers
```
┌─────────────────────────────────────────────────────────┐
│ Layer 1: TAILSCALE FUNNEL │
│ • Only entry point from internet │
│ • HTTPS termination │
│ • No open ports on router │
├─────────────────────────────────────────────────────────┤
│ Layer 2: CROWDSEC │
│ • Crowdsourced threat intelligence │
│ • Blocks known malicious IPs │
│ • DDoS mitigation │
├─────────────────────────────────────────────────────────┤
│ Layer 3: TRAEFIK │
│ • Domain-based routing │
│ • Only forwards to valid services │
│ • Rate limiting (configurable) │
├─────────────────────────────────────────────────────────┤
│ Layer 4: DOCKER NETWORK ISOLATION │
│ • Containers can't access each other unless configured │
│ • Databases on separate network from public apps │
├─────────────────────────────────────────────────────────┤
│ Layer 5: TAILSCALE MESH (Admin) │
│ • All admin traffic encrypted │
│ • No admin ports exposed to internet │
│ • Device authentication required │
└─────────────────────────────────────────────────────────┘
```
---
## Dynamic IP Handling
```
ISP Changes IP
┌─────────────┐ auto-update ┌──────────────────────┐
│ NUC detects │ ────────────────► │ Tailscale Coord │
│ new IP │ │ Server │
└─────────────┘ └──────────┬───────────┘
┌──────────────────────────────────────┘
│ broadcasts new location
┌─────────────────────────────────────────────────────────┐
│ UNCHANGED │
│ • Tailscale IP: 100.x.x.x (stable) │
│ • Funnel URL: nuc-tailscale.tail58f5ad.ts.net (stable)│
│ • whyrating.com redirect (stable) │
│ • All tunnels auto-reconnect (~10-30 sec) │
└─────────────────────────────────────────────────────────┘
```
**Key Point:** Your ISP can change your public IP anytime - Tailscale handles this automatically. No DDNS needed.
---
## Access Reference
| From | To | Method |
|------|----|--------|
| **Public users** | Websites | `whyrating.com` or `.ts.net` URL |
| **You (remote)** | NUC admin | `ssh nuc-tailscale` or `http://100.x.x.x:8000` |
| **You (home)** | NUC admin | `ssh nuc` or `http://192.168.1.3:8000` |
| **You (remote)** | Router | SSH jump: `ssh -J nuc-tailscale root@192.168.1.1` |
---
## URLs
| Service | Public URL | Private URL (Tailscale) |
|---------|------------|-------------------------|
| Main site | `whyrating.com` | - |
| Direct Funnel | `nuc-tailscale.tail58f5ad.ts.net` | - |
| Coolify | - | `http://nuc-tailscale:8000` |
| Homepage | Via Funnel :3000 | `http://nuc-tailscale:3000` |
| Gitea | - | `http://nuc-tailscale:3030` |
---
## What's NOT Exposed to Internet
- SSH (22)
- Coolify (8000)
- Databases (5432, 3306)
- MinIO (9000/9001)
- Authentik (9090)
- Router admin (192.168.1.1)
- Any direct ports on router
---
## Coolify Deployment Best Practices
### For Public Apps:
```yaml
services:
myapp:
image: myapp:latest
# NO ports: section - Traefik routes internally
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`myapp.whyrating.com`)"
networks:
- coolify
```
### For Private Apps:
```yaml
services:
mydb:
image: postgres:16
# No traefik labels
# No exposed ports
networks:
- internal # Separate from coolify network
```
---
## Quick Commands
```bash
# Check Tailscale status
tailscale status
# Check Funnel status
ssh nuc "tailscale funnel status"
# Access Coolify remotely
open http://nuc-tailscale:8000
# SSH to NUC from anywhere
ssh nuc-tailscale
# Check CrowdSec decisions
ssh nuc "docker exec crowdsec-* cscli decisions list"
```
---
## Related Documents
- `.artifacts/2026-02-01_19-11_domain-pre-purchase-check-guide.md` - Domain checking before purchase
- `.artifacts/domain-check.sh` - Script to check domains for blocks
- `CLAUDE.md` - Full NUC server documentation
---
## Why This Architecture?
1. **Spanish ISP Blocks** - Cloudflare shared IPs are blocked during LaLiga matches. Tailscale Funnel uses different infrastructure.
2. **Dynamic IP** - No need for DDNS or port forwarding. Tailscale handles IP changes automatically.
3. **Security** - Zero ports exposed on router. All admin via encrypted Tailscale mesh.
4. **Simplicity** - Single entry point (Funnel), single orchestrator (Coolify), single security layer (CrowdSec).

162
docs/backup-strategy.md Normal file
View File

@@ -0,0 +1,162 @@
# NUC Backup Strategy
**Date:** 2026-02-01 17:30
**Context:** 3-layer backup strategy for NUC disaster recovery
---
## Layer 1: Daily Automated Backup (Kopia → MinIO)
**What:** All Docker volumes + Coolify configs
**Where:** NUC MinIO (local) + optionally offsite
**Recovery time:** ~15 min
### Configure Kopia
```bash
# Connect to Kopia container
ssh nuc "docker exec -it kopia /bin/sh"
# Add Docker volumes path
kopia policy set /var/lib/docker/volumes --add-include "*.sql" --add-include "*.db"
kopia snapshot create /var/lib/docker/volumes
# Add Coolify data
kopia snapshot create /data/coolify
# Set daily schedule
kopia policy set --global --snapshot-interval 24h
```
### Critical paths to backup
| Path | Contents |
|------|----------|
| `/data/coolify/` | Coolify configs, SSH keys, DB |
| `/var/lib/docker/volumes/` | All service data |
| `/opt/homepage/config/` | Homepage dashboard config |
### Manual backup command
```bash
ssh nuc "docker exec kopia kopia snapshot create /var/lib/docker/volumes /data/coolify"
```
### Verify backups
```bash
ssh nuc "docker exec kopia kopia snapshot list --all"
```
---
## Layer 2: Clonezilla Full Disk Image
**What:** Complete disk clone (OS + everything)
**Where:** External USB drive
**Recovery time:** ~30 min
**When:** Monthly or before hardware changes
### Create image
1. Boot NUC from Clonezilla USB
2. Select: `device-image``savedisk`
3. Choose external USB as destination
4. Select NUC internal disk as source
5. Use options: `-z1p` (parallel compression), `-fsck-src-part` (check filesystem)
### Recommended naming
```
nuc-full-YYYY-MM-DD.img
```
### Storage requirements
| NUC disk used | Compressed image |
|---------------|------------------|
| 50GB | ~20GB |
| 100GB | ~40GB |
| 250GB | ~80GB |
---
## Layer 3: Coolify Services Export
**What:** List of all services + configs for manual rebuild
**Where:** This repo + offsite
**Recovery time:** 1-2 hours (fresh install)
### Export current services
```bash
# Get all services with their configs
ssh nuc "docker exec coolify php artisan tinker --execute=\"
use App\Models\Service;
Service::all()->map(fn(\\\$s) => [
'name' => \\\$s->name,
'uuid' => \\\$s->uuid,
'type' => \\\$s->service_type,
'compose' => \\\$s->docker_compose_raw
])->toJson(JSON_PRETTY_PRINT);
\""
```
### Current NUC Services (as of 2026-02-01)
| Service | Port | Coolify UUID |
|---------|------|--------------|
| Homepage | 3000 | eo0g84scsss4osk0skk040ck |
| Coolify | 8000 | (system) |
| Gitea | 3030 | ho0cwgcwos88cwc48g84c0g8 |
| Outline | 3080 | pccg80wks4c084008owokkkg |
| n8n | 5678 | uk0o04o0g84s4sc80kkoooc0 |
| Vaultwarden | 8222 | h40w0ss4kgs0c8cgc0sc8k48 |
| Ntfy | 8333 | xgkkg8gkgg048g8gkc8ck4os |
| MinIO | 9000/9001 | dg4wkgg8skcssww0040sgk80 |
| Authentik | 9090 | e8owcw0s4wcswc4w4css0sws |
| FileBrowser | 8085 | o4swwwsowwg88coo0ws4cg48 |
| CloudBeaver | 8087 | joo4g4k0w08k8kcosgsgswc0 |
| Uptime Kuma | 3001 | s4ko04w88k048sw8o4swsoww |
| Dozzle | 9999 | vgko8w4kkkc8k0g4sggs4ks8 |
| Tailscale | - | posgwooww0s0c0okssooc4gw |
### Databases
| Database | Port | UUID |
|----------|------|------|
| LiquidGym MySQL | 3306 | hgwcgs4oswwc8scg080scoo4 |
| LiquidGym Postgres | 5433 | x4kk8g4k8w4g0cw480w84g4g |
| Knosia Postgres | 5442 | ik80skko0008w4000c4w40os |
---
## Recovery Priority
If total failure, restore in this order:
1. **Coolify** (manages everything else)
2. **Databases** (apps depend on them)
3. **Authentication** (Authentik, Vaultwarden)
4. **Core services** (Gitea, n8n, Outline)
5. **Monitoring** (Uptime Kuma, Dozzle)
---
## Offsite Backup (Optional)
For offsite, sync Kopia repository to cloud:
```bash
# Sync to Backblaze B2 (example)
kopia repository sync-to b2 --bucket=nuc-backup --key-id=XXX --key=YYY
```
Or rsync the Clonezilla image to another location.
---
## Related
- Hard Disk Change Guide: `2026-02-01_17-30_nuc-hard-disk-change.md`
- LiquidGym Migration: `2026-02-01_16-45_liquidgym-mysql-migration.md`

301
docs/domain-check-guide.md Normal file
View File

@@ -0,0 +1,301 @@
# Domain Pre-Purchase Check Guide
**Date:** 2026-02-01
**Context:** After whymyrating.com was blocked by Spanish ISPs due to LaLiga's Cloudflare IP blocking, this guide documents how to check a domain before purchase to avoid similar issues.
---
## Quick Checklist
- [ ] Domain not previously used for spam/malware
- [ ] Not on any security blocklists
- [ ] Hosting provider IPs not blocked in target countries
- [ ] No trademark conflicts
- [ ] Clean WHOIS history
---
## Step 1: Check Domain Availability & History
### 1.1 Basic Availability
```bash
# WHOIS check
whois <domain> | grep -iE "registrar|creation|status"
# If "No match" = available and clean
# If registered = check who owns it
```
### 1.2 Historical Usage (Archive.org)
Check if domain was previously used (and for what):
- **URL:** `https://web.archive.org/web/*/https://<domain>`
- Look for: spam, adult content, piracy, gambling
### 1.3 Expired Domain History
- **ExpiredDomains.net:** `https://www.expireddomains.net/domain-name-search/?q=<domain>`
- Check: Previous backlinks, spam score, previous usage
---
## Step 2: Security & Reputation Checks
### 2.1 VirusTotal (Multi-vendor scan)
```
URL: https://www.virustotal.com/gui/domain/<domain>
```
- Check: Detection ratio (should be 0/90+)
- Look for: Any vendor flagging as malicious
### 2.2 Google Safe Browsing
```
URL: https://transparencyreport.google.com/safe-browsing/search?url=<domain>
```
- Status should be: "No unsafe content found"
### 2.3 Sucuri SiteCheck
```
URL: https://sitecheck.sucuri.net/?scan=<domain>
```
- Check: Blacklist status, malware, spam flags
- Should show: Clean across all vendors
### 2.4 Other Reputation Services
| Service | URL |
|---------|-----|
| MXToolbox | `https://mxtoolbox.com/blacklists.aspx` |
| Spamhaus | `https://check.spamhaus.org/` |
| SURBL | `http://www.surbl.org/surbl-analysis` |
| PhishTank | `https://phishtank.org/` |
| URLhaus | `https://urlhaus.abuse.ch/browse/` |
---
## Step 3: Regional Blocking Check (Critical for Spain)
### 3.1 Spanish ISP LaLiga Blocks
If targeting Spanish users, check if the hosting provider's IPs are blocked:
**Live Block Monitor:**
```
URL: https://hayahora.futbol
API: https://hayahora.futbol/estado/data.json
```
**Check specific IP:**
```bash
# Get your hosting provider's IPs
dig +short <domain> A
# Check if those IPs are in the block list
curl -s "https://hayahora.futbol/estado/data.json" | \
python3 -c "
import json, sys
data = json.load(sys.stdin)
ip = '<YOUR_IP>' # Replace with actual IP
for entry in data.get('data', []):
if entry.get('ip') == ip:
print(f\"ISP: {entry['isp']} | Blocked: {entry['stateChanges'][-1]['state']}\")
"
```
### 3.2 Spanish ISPs That Block
| ISP | Brands | Block Type |
|-----|--------|------------|
| **MásOrange** | Orange, Yoigo, Jazztel, Masmovil, Simyo, Pepephone, Lebara, Lyca, Llamaya, Euskaltel | IP-based |
| **Movistar** | Movistar, O2 | IP-based |
| **Vodafone** | Vodafone, Lowi | IP-based |
| **DIGI** | DIGI | IP-based |
**Blocking occurs:** During LaLiga matches (weekends, some weekdays)
**Season:** August - May each year
### 3.3 Global Accessibility Test
```bash
# Multi-location HTTP check
curl -s "https://check-host.net/check-http?host=<domain>&max_nodes=10" \
-H "Accept: application/json"
# Wait 5 seconds, then get results
curl -s "https://check-host.net/check-result/<request_id>" \
-H "Accept: application/json"
```
### 3.4 Internet Censorship Check (China, Russia, Turkey)
```
URL: https://www.experte.com/internet-censorship
```
---
## Step 4: Hosting Provider Risk Assessment
### 4.1 High-Risk Providers for Spain
These providers use shared IPs that get blocked by LaLiga:
| Provider | Risk Level | Reason |
|----------|------------|--------|
| **Cloudflare (free)** | 🔴 HIGH | Shared IPs frequently blocked |
| **Vercel** | 🔴 HIGH | Affected by same blocks |
| **Netlify** | 🟡 MEDIUM | Some IP ranges blocked |
| **GitHub Pages** | 🟡 MEDIUM | Occasionally affected |
| **BunnyCDN** | 🟡 MEDIUM | Some blocks reported |
### 4.2 Lower-Risk Options for Spain
| Solution | Risk Level | Notes |
|----------|------------|-------|
| **Cloudflare Pro/Business** | 🟢 LOW | Dedicated IPs available |
| **Dedicated VPS** | 🟢 LOW | Own IP, not shared |
| **AWS CloudFront** | 🟢 LOW | Different IP ranges |
| **Non-CDN hosting** | 🟢 LOW | Direct IP, no sharing |
### 4.3 Check If Hosting IPs Are Clean
```bash
# Get hosting provider's IP range
dig +short <cdn-domain> A
# Check against Spanish block list
curl -s "https://hayahora.futbol/estado/data.json" | \
grep -c "<IP_PREFIX>"
# If count > 0, that IP range has been blocked before
```
---
## Step 5: DNS & Email Reputation
### 5.1 Check DNS Blacklists
```bash
# Using MXToolbox
curl -s "https://mxtoolbox.com/api/v1/lookup/blacklist/<domain>"
```
### 5.2 Email Deliverability
If you'll send emails from this domain:
- Check if IP range is on Spamhaus
- Verify no previous spam history
- Set up SPF, DKIM, DMARC from day 1
---
## Step 6: Trademark & Legal Check
### 6.1 Trademark Search
- **USPTO:** `https://tmsearch.uspto.gov`
- **EUIPO:** `https://euipo.europa.eu/eSearch/`
- **WIPO Global:** `https://branddb.wipo.int`
### 6.2 Domain Disputes History
- Check UDRP decisions: `https://www.wipo.int/amc/en/domains/search/`
---
## Complete Pre-Purchase Script
```bash
#!/bin/bash
DOMAIN="$1"
echo "=== Domain Pre-Purchase Check: $DOMAIN ==="
echo -e "\n[1/6] WHOIS Check"
whois "$DOMAIN" 2>/dev/null | grep -iE "registrar|creation|status|No match" | head -5
echo -e "\n[2/6] DNS Resolution"
dig +short "$DOMAIN" A
echo -e "\n[3/6] Security Check (Sucuri)"
echo "Visit: https://sitecheck.sucuri.net/?scan=$DOMAIN"
echo -e "\n[4/6] VirusTotal"
echo "Visit: https://www.virustotal.com/gui/domain/$DOMAIN"
echo -e "\n[5/6] Archive.org History"
echo "Visit: https://web.archive.org/web/*/$DOMAIN"
echo -e "\n[6/6] Spanish ISP Block Check"
IP=$(dig +short "$DOMAIN" A | head -1)
if [ -n "$IP" ]; then
echo "IP: $IP"
BLOCKED=$(curl -s "https://hayahora.futbol/estado/data.json" 2>/dev/null | grep -c "\"$IP\"")
if [ "$BLOCKED" -gt 0 ]; then
echo "⚠️ WARNING: This IP has been blocked by Spanish ISPs"
else
echo "✅ IP not in Spanish block list"
fi
else
echo "No IP yet (domain not configured)"
fi
echo -e "\n=== Check Complete ==="
```
---
## Red Flags to Avoid
| Red Flag | Why It's Bad |
|----------|--------------|
| Previously used for spam | Email deliverability issues |
| On any security blocklist | SEO penalties, browser warnings |
| Hosting on shared Cloudflare IPs | Spanish ISP blocks |
| Similar to trademarked name | Legal disputes |
| Expired domain with backlinks from spam sites | Google penalties |
| Previously used for piracy/gambling | Regulatory blocks |
---
## Recommended Workflow
```
1. Check availability (WHOIS)
2. Check history (Archive.org)
3. Security scan (VirusTotal, Sucuri)
4. Check hosting provider's IPs against block lists
5. If targeting Spain: Verify IPs not in LaLiga blocks
6. Trademark search
7. Purchase domain
8. Set up on dedicated IP or low-risk CDN
```
---
## Resources
| Resource | URL | Purpose |
|----------|-----|---------|
| hayahora.futbol | https://hayahora.futbol | Spanish ISP block monitor |
| VirusTotal | https://virustotal.com | Multi-vendor security scan |
| Sucuri SiteCheck | https://sitecheck.sucuri.net | Website security check |
| Archive.org | https://web.archive.org | Historical usage |
| Check-Host | https://check-host.net | Multi-location accessibility |
| EXPERTE | https://experte.com/internet-censorship | Censorship check |
| MXToolbox | https://mxtoolbox.com | Email/DNS blacklists |
---
## Lessons from whymyrating.com
**What happened:**
- Domain registered Jan 30, 2026
- Hosted on Cloudflare (free tier, shared IPs)
- IPs 188.114.97.5 and 188.114.96.5 are in LaLiga block rotation
- Blocked by Orange and Masmovil in Spain during matches
**How to avoid:**
1. Use dedicated IPs or non-Cloudflare CDN for Spanish audience
2. Check hayahora.futbol before choosing hosting
3. Consider Cloudflare Pro for dedicated IPs
4. Have VPN/alternative access ready for Spanish users
---
**Related:** See CLAUDE.md > Domain Configuration Workflow for setup after purchase.

259
docs/hard-disk-change.md Normal file
View File

@@ -0,0 +1,259 @@
# NUC Hard Disk Change Guide
**Date:** 2026-02-01 17:30
**Context:** Step-by-step guide to replace NUC hard disk with larger one
---
## Prerequisites
- [ ] Clonezilla USB boot drive ([download](https://clonezilla.org/downloads.php))
- [ ] External USB drive (larger than current disk usage)
- [ ] New internal disk (must be >= current used space)
- [ ] 1-2 hours of downtime
---
## Before You Start
### Check current disk usage
```bash
ssh nuc "df -h / && lsblk"
```
### Verify services are backed up
```bash
ssh nuc "docker exec kopia kopia snapshot list --all | tail -10"
```
### Export critical configs (safety net)
```bash
ssh nuc "tar czf /tmp/nuc-configs-backup.tar.gz /data/coolify /opt/homepage/config 2>/dev/null"
scp nuc:/tmp/nuc-configs-backup.tar.gz ~/Desktop/
```
---
## Phase 1: Create Clonezilla Image
### 1.1 Boot from Clonezilla USB
1. Insert Clonezilla USB into NUC
2. Power on, press **F10** for boot menu
3. Select USB drive
4. Choose: `Clonezilla live (Default)`
### 1.2 Create disk image
```
Select: device-image
Select: local_dev (save to external USB)
Choose: savedisk
Image name: nuc-full-2026-02-01
Source disk: /dev/sda (or /dev/nvme0n1)
Compression: -z1p (parallel gzip, fastest)
Options: -fsck-src-part -c (check & checksum)
```
### 1.3 Wait for completion
- 256GB disk → ~20-40 min
- 512GB disk → ~40-60 min
### 1.4 Verify image
Clonezilla shows checksums. Note them down:
```
Image checksum: ________________________________
```
---
## Phase 2: Swap Physical Disk
### 2.1 Power off NUC
```bash
ssh nuc "sudo shutdown now"
```
### 2.2 Open NUC and replace disk
1. Disconnect power
2. Remove bottom cover (4 screws)
3. Remove old disk (M.2 or 2.5" SATA)
4. Insert new disk
5. Replace cover
---
## Phase 3: Restore to New Disk
### 3.1 Boot Clonezilla again
1. Insert Clonezilla USB + external drive with image
2. Boot from USB
### 3.2 Restore image
```
Select: device-image
Select: local_dev
Choose: restoredisk
Image: nuc-full-2026-02-01
Target: /dev/sda (new disk)
```
### 3.3 Confirm and wait
Same time as backup (~20-60 min)
---
## Phase 4: Expand Partition
After restore, the new disk has the old partition sizes. Expand to use all space:
### 4.1 Boot into Ubuntu
Remove USB drives, boot normally.
### 4.2 Expand partition
```bash
# Check current layout
lsblk
# Expand partition (usually partition 2 or 3)
sudo growpart /dev/sda 2
# Expand filesystem
sudo resize2fs /dev/sda2
# Verify
df -h /
```
### For NVMe drives:
```bash
sudo growpart /dev/nvme0n1 2
sudo resize2fs /dev/nvme0n1p2
```
### For LVM:
```bash
sudo pvresize /dev/sda2
sudo lvextend -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv
sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv
```
---
## Phase 5: Verify Everything Works
### 5.1 Check services
```bash
# All containers running?
docker ps
# Coolify healthy?
curl -s http://localhost:8000/api/health
# Check each service
docker ps --format 'table {{.Names}}\t{{.Status}}' | head -20
```
### 5.2 Test critical services
| Service | Test |
|---------|------|
| Coolify | http://192.168.1.3:8000 |
| Homepage | http://192.168.1.3:3000 |
| Gitea | http://192.168.1.3:3030 |
| n8n | http://192.168.1.3:5678 |
### 5.3 Verify databases
```bash
# LiquidGym Postgres
docker exec postgres-x4kk8g4k8w4g0cw480w84g4g psql -U postgres -c "\\l"
# LiquidGym MySQL
docker exec hgwcgs4oswwc8scg080scoo4 mysql -u root -pliquidgym_root_nuc_2026 -e "SHOW DATABASES;"
```
---
## Troubleshooting
### GRUB bootloader not found
```bash
# Boot from Ubuntu Live USB
sudo mount /dev/sda2 /mnt
sudo mount /dev/sda1 /mnt/boot/efi
sudo chroot /mnt
grub-install /dev/sda
update-grub
exit
reboot
```
### NVMe device name changed
Edit `/etc/fstab` to use UUID instead of device names:
```bash
# Find UUIDs
blkid
# Edit fstab
sudo nano /etc/fstab
# Change /dev/sda2 to UUID=xxxxx
```
### Docker services not starting
```bash
sudo systemctl restart docker
docker ps -a # Check for exited containers
docker start $(docker ps -aq) # Start all
```
---
## Rollback Plan
If new disk fails, you still have:
1. **Original disk** (keep it safe for 1 week)
2. **Clonezilla image** on external drive
3. **Config backup** on your Mac (`~/Desktop/nuc-configs-backup.tar.gz`)
Worst case: put old disk back in, boot, everything works.
---
## Quick Reference
| Step | Time | Risk |
|------|------|------|
| Create image | 30-60 min | Low |
| Swap disk | 10 min | Low |
| Restore image | 30-60 min | Low |
| Expand partition | 5 min | Low |
| Verify services | 10 min | None |
| **Total** | ~2 hours | Low |
---
## Related
- Backup Strategy: `2026-02-01_17-30_nuc-backup-strategy.md`
- Coolify Dashboard: http://192.168.1.3:8000

114
docs/lan-dns-setup.md Normal file
View File

@@ -0,0 +1,114 @@
# NUC.lan DNS Configuration
**Date:** 2026-02-01 19:15
**Context:** Setting up friendly hostname for local NUC access via Tailscale
## Summary
Configured `nuc.lan` as a friendly hostname for accessing NUC services on the local network, working around macOS `.local` mDNS handling.
## Why .lan instead of .local?
macOS reserves the `.local` TLD for multicast DNS (Bonjour/mDNS). This means:
- `.local` domains bypass regular DNS and go to mDNS
- Tailscale split DNS cannot override this behavior
- `.lan` works correctly with standard DNS resolution
## Configuration
### 1. OpenWrt Router DNS Entry
```bash
ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 "
uci add dhcp domain
uci set dhcp.@domain[-1].name='nuc.lan'
uci set dhcp.@domain[-1].ip='192.168.1.3'
uci commit dhcp
/etc/init.d/dnsmasq restart
"
```
### 2. Tailscale Split DNS
| Setting | Value |
|---------|-------|
| **Nameserver** | 192.168.1.1 (router) |
| **Domain** | lan |
| **Type** | Split DNS |
**Dashboard:** https://login.tailscale.com/admin/dns
This tells Tailscale to forward all `.lan` domain queries to the router (192.168.1.1), which resolves `nuc.lan` to `192.168.1.3`.
## Verification
```bash
# DNS resolution
dig nuc.lan +short
# Returns: 192.168.1.3
# HTTP access
curl -s http://nuc.lan:8086
# Returns: NUC Portal (Homer dashboard)
```
## NUC Portal
| Property | Value |
|----------|-------|
| **URL** | **http://nuc.lan** (port 80) |
| **Alt URL** | http://nuc.lan:8086 (direct) |
| **Container** | portal-l44gcskok8c8wcocwswg08w8 |
| **Image** | b4bz/homer:latest |
| **Config** | /www/assets/config.yml |
The portal is routed through Traefik on port 80, making it accessible at the clean URL `http://nuc.lan`.
### Traefik Labels
```yaml
labels:
- "traefik.enable=true"
- "traefik.http.routers.nuc-portal.rule=Host(`nuc.lan`)"
- "traefik.http.routers.nuc-portal.entrypoints=http"
- "traefik.http.services.nuc-portal.loadbalancer.server.port=8080"
```
### Security: Local Only
This portal is **NOT accessible from the internet** because:
1. `nuc.lan` DNS only exists in local router + Tailscale split DNS
2. No Cloudflare Tunnel route exists for `nuc.lan`
3. Traefik only routes requests with `Host: nuc.lan` header
The portal provides links to all NUC services using `nuc.lan` URLs.
## Service URLs
| Service | URL |
|---------|-----|
| NUC Portal | http://nuc.lan:8086 |
| Coolify | http://nuc.lan:8000 |
| Homepage | http://nuc.lan:3000 |
| Snappymail | http://nuc.lan:8082 |
| Stalwart Admin | http://nuc.lan:8081 |
| Outline | http://nuc.lan:3080 |
| n8n | http://nuc.lan:5678 |
| NocoDB | http://nuc.lan:8084 |
| Gitea | http://nuc.lan:3030 |
| Uptime Kuma | http://nuc.lan:3001 |
| MinIO | http://nuc.lan:9001 |
| Vaultwarden | http://nuc.lan:8222 |
| Dozzle | http://nuc.lan:9999 |
## Requirements
- Must be connected to Tailscale network
- Works from any device on the Tailnet (Mac, iPhone, etc.)
- Router must be reachable from Tailscale devices
## Related
- NUC Portal artifact: Previous session
- Tailscale DNS: https://login.tailscale.com/admin/dns
- OpenWrt Router: 192.168.1.1

95
docs/mcp-browser-setup.md Normal file
View File

@@ -0,0 +1,95 @@
# MCP Browser Configuration
**Date:** 2026-02-01 15:45
**Context:** Configured Playwriter and Chrome DevTools MCP servers with naming convention for local vs remote browsers
## Naming Convention
```
playwriter-<location>-<id>
chrome-devtools-<location>-<id>
```
| Pattern | Example | Description |
|---------|---------|-------------|
| `<location>` | `local`, `nuc`, `cloud` | Where browser runs |
| `<id>` | `01`, `02`, etc. | Unique instance number |
## Current Configuration
### Local Browser
| Field | Value |
|-------|-------|
| **MCP Name** | `playwriter-local` |
| **Type** | Local |
| **Description** | Uses local machine resources |
| **Command** | `npx playwriter` |
### Remote NUC Browser
| Field | Value |
|-------|-------|
| **MCP Name** | `playwriter-nuc-01` |
| **Type** | Remote |
| **ID** | `nuc-01` |
| **Host** | `192.168.1.3` |
| **Port** | `19988` |
| **WebSocket** | `ws://192.168.1.3:19988` |
| **Token** | `nuc-browser-token` |
| **noVNC** | `http://192.168.1.3:6081/vnc.html` |
| **CDP** | `http://192.168.1.3:9222` |
| **Command** | `npx playwriter --host ws://192.168.1.3:19988 --token nuc-browser-token` |
### Chrome DevTools for NUC
| Field | Value |
|-------|-------|
| **MCP Name** | `chrome-devtools-nuc-01` |
| **Type** | Remote |
| **ID** | `nuc-01` |
| **Browser URL** | `http://192.168.1.3:19988` |
| **Command** | `npx chrome-devtools-mcp@latest --browserUrl http://192.168.1.3:19988` |
## Metadata Fields
Config files use underscore-prefixed fields for metadata (ignored by MCP):
```json
{
"playwriter-nuc-01": {
"_description": "Remote browser on NUC server - no local resources used",
"_id": "nuc-01",
"_host": "192.168.1.3",
"_port": 19988,
"command": "npx",
"args": ["playwriter", "--host", "ws://192.168.1.3:19988", "--token", "nuc-browser-token"]
}
}
```
## Priority Order
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)
## Future Expansion Examples
| MCP Name | Location | Use Case |
|----------|----------|----------|
| `playwriter-nuc-02` | NUC | Second browser for parallel automation |
| `playwriter-cloud-01` | Cloud | Cloud-hosted browser instance |
| `playwriter-mac-mini-01` | Mac Mini | Dedicated Mac browser |
## NUC Browser Container
**Location:** `~/playwriter-browser/` on NUC (deployed via docker compose)
**First-time setup:**
1. Access noVNC at `http://192.168.1.3:6081/vnc.html`
2. Install Playwriter extension
3. Click extension icon to activate (turns green)
## Related
- Config file: `/Users/agutierrez/Desktop/nuc/.mcp.json`
- CLAUDE.md: Browser MCP documentation section
- NUC container: `playwriter-browser` service in Coolify

135
docs/mcp-research-guide.md Normal file
View File

@@ -0,0 +1,135 @@
# MCP Research Guide
**Date:** 2026-02-01
**Purpose:** Comprehensive guide for finding and evaluating MCP servers
---
## Primary MCP Directories (Check First)
| Resource | URL | MCPs | Best For |
|----------|-----|------|----------|
| **MCP Server Finder** | https://mcpserverfinder.com | 1,934+ | Largest directory, searchable |
| **MCPServers.org** | https://mcpservers.org | 100+ | Curated, categories, official/featured tabs |
| **MCP.so** | https://mcp.so | - | Official registry, curated list |
| **Smithery** | https://smithery.ai | - | Searchable MCP marketplace |
| **Glama MCP Directory** | https://glama.ai/mcp/servers | - | Curated collection with categories |
| **PulseMCP** | https://pulsemcp.com | - | Community-driven directory |
| **MCPHub** | https://mcphub.io | - | Hub for discovering MCPs |
| **Cursor MCP Directory** | https://cursor.directory/mcp | - | Cursor-focused MCP collection |
---
## GitHub Resources (For Verification)
| Resource | URL | Stars | Best For |
|----------|-----|-------|----------|
| **Awesome MCP Servers** | https://github.com/punkpeye/awesome-mcp-servers | 80k+ | Most popular curated list |
| **Awesome MCP** | https://github.com/wong2/awesome-mcp | - | Alternative curated list |
| **TensorBlock Awesome** | https://github.com/TensorBlock/awesome-mcp-servers | - | Covers 7,260+ MCPs |
| **MCP Servers Org** | https://github.com/modelcontextprotocol/servers | - | Official Anthropic examples |
| **GitHub Search** | `topic:mcp-server` or `"modelcontextprotocol"` | - | Find new/unlisted MCPs |
---
## MCP Discovery Tools (MCP-of-MCPs)
**Install these MCPs to discover/manage other MCPs from within Claude:**
| MCP | Repo/URL | Description |
|-----|----------|-------------|
| **Magg** | `sitbon/magg` | Meta-MCP hub - LLMs can autonomously discover, install, orchestrate MCPs |
| **MCPDiscovery** | `particlefuture/MCPDiscovery` | Auto-discovery and config of local MCP servers |
| **NCP** | `portel-dev/ncp` | Orchestrates MCP ecosystem, reduces token overhead |
| **AllInOneMCP** | https://mcp.pfvc.io/mcp/ | Remote MCP-of-MCPs, discover and learn MCPs |
| **1mcpserver** | https://mcp.1mcpserver.com/mcp/ | Remote auto-discovery and configuration |
**Official MCP Registry API:** https://registry.modelcontextprotocol.io (programmatic access)
---
## Vendor-Specific MCP Collections
| Vendor | URL | MCPs Included |
|--------|-----|---------------|
| **Google** | https://mcpservers.org/servers/google/mcp | Maps, BigQuery, GKE, Compute Engine, Workspace, Firebase, Cloud Run, Analytics, Cloud Storage, Security, gcloud CLI, Flutter/Dart |
---
## MCP Evaluation Criteria
**MUST evaluate every MCP candidate using this checklist:**
| Criteria | Minimum | Preferred | How to Check |
|----------|---------|-----------|--------------|
| **GitHub Stars** | 50+ | 500+ | Repo main page |
| **Last Commit** | < 6 months | < 1 month | Repo commits tab |
| **Open Issues** | < 50 unresolved | < 20 | Issues tab |
| **Documentation** | README exists | Full docs site | Repo README |
| **License** | Any OSS | MIT/Apache | LICENSE file |
| **TypeScript/Python** | Either | TypeScript | package.json or setup.py |
| **Tool Count** | 3+ tools | 10+ tools | README or source |
| **Active Maintainer** | 1+ | 2+ | Contributors tab |
**Red Flags (AVOID):**
- No commits in 12+ months
- Only 1-2 tools with narrow scope
- No error handling documented
- Requires API keys with no free tier
- Binary-only distribution (no source)
- Excessive permissions requested
---
## Research Workflow
```python
# 1. Search directories first
WebFetch(url="https://mcp.so/search?q=<category>", prompt="Find MCPs for <purpose>")
WebFetch(url="https://smithery.ai/search?q=<keyword>", prompt="List relevant MCPs")
# 2. Verify on GitHub
WebFetch(url="https://github.com/<org>/<repo>", prompt="Check stars, last commit, issues")
# 3. Check awesome lists for mentions
WebFetch(url="https://github.com/punkpeye/awesome-mcp-servers", prompt="Is <mcp> listed?")
# 4. Review actual tools provided
WebFetch(url="<repo>/blob/main/src/index.ts", prompt="List all available tools")
```
---
## Quick Search Commands
```bash
# Search GitHub for MCP servers by topic
# Use WebSearch with these queries:
"site:github.com mcp-server <category> stars:>100"
"site:smithery.ai <category> mcp"
"site:mcp.so <category>"
```
---
## MCP Categories Quick Reference
| Need | Search Terms | Top Options |
|------|--------------|-------------|
| **Mac/Desktop** | macos, desktop, system | `mcp-server-commands`, `desktop-mcp` |
| **File System** | filesystem, files, directory | `@anthropic/filesystem` |
| **Browser** | browser, playwright, puppeteer | `playwright-mcp`, `browsermcp` |
| **Database** | postgres, mysql, sqlite | `mcp-server-postgres`, `sqlite-mcp` |
| **Git/GitHub** | git, github, gitlab | `github-mcp`, `@anthropic/github` |
| **Docker** | docker, containers | `docker-mcp` |
| **Cloud** | aws, gcp, azure | `aws-mcp`, `cloudflare-mcp` |
| **API** | rest, graphql, http | `fetch-mcp`, `openapi-mcp` |
| **Search** | search, web, brave | `brave-search-mcp`, `exa-mcp` |
| **Notes/Docs** | notion, obsidian, markdown | `notion-mcp`, `obsidian-mcp` |
---
## Related
- CLAUDE.md - Main NUC server documentation
- See "Available MCPs for NUC Management" section in CLAUDE.md for currently installed MCPs

50
docs/n8n-mcp-setup.md Normal file
View File

@@ -0,0 +1,50 @@
# n8n MCP Setup
**Date:** 2026-02-01 13:50
**Context:** Set up n8n MCP server with API key for workflow automation
## API Key Details
- **API Key:** `n8n_api_d8bae3df1be532498e38a7bf3870a4e62713170f`
- **Label:** Claude-MCP
- **User ID:** ddac5ff1-e6fd-48bc-9a4a-970b6d73bafc (alezmad@gmail.com)
- **Scopes:** `["workflow:read","workflow:execute","user:read"]`
## MCP Configuration
Configured via MCP Docker gateway:
```bash
docker mcp secret set n8n.api_key=n8n_api_d8bae3df1be532498e38a7bf3870a4e62713170f
```
Config set:
```json
{
"api_url": "http://192.168.1.3:5678"
}
```
## Available Tools (42 total)
Key tools:
- `n8n_list_workflows` - List all workflows
- `n8n_create_workflow` - Create new workflow
- `n8n_get_workflow` - Get workflow by ID
- `search_nodes` - Search 543 n8n nodes
- `search_templates` - Browse workflow templates
- `n8n_trigger_webhook_workflow` - Execute via webhook
- `validate_workflow` - Validate workflow config
## Creation Method
API key created via direct SQLite insert (n8n was stopped briefly):
```bash
docker run --rm --user 1000:1000 -v uk0o04o0g84s4sc80kkoooc0_n8n-data:/data \
keinos/sqlite3 sqlite3 /data/database.sqlite \
"INSERT INTO user_api_keys (...) VALUES (...)"
```
## Related
- n8n UI: http://192.168.1.3:5678
- n8n container: n8n-uk0o04o0g84s4sc80kkoooc0
- MCP version: 2.22.17 (update available: 2.33.5)

117
docs/scripts/domain-check.sh Executable file
View File

@@ -0,0 +1,117 @@
#!/bin/bash
# Domain Pre-Purchase Check Script
# Usage: ./domain-check.sh example.com
DOMAIN="$1"
if [ -z "$DOMAIN" ]; then
echo "Usage: $0 <domain>"
echo "Example: $0 whyrating.com"
exit 1
fi
echo "╔════════════════════════════════════════════════════════════╗"
echo "║ Domain Pre-Purchase Check: $DOMAIN"
echo "╚════════════════════════════════════════════════════════════╝"
# 1. WHOIS Check
echo -e "\n━━━ [1/7] WHOIS Registration ━━━"
WHOIS_RESULT=$(whois "$DOMAIN" 2>/dev/null)
if echo "$WHOIS_RESULT" | grep -qi "No match"; then
echo "✅ Domain is AVAILABLE (not registered)"
else
echo "⚠️ Domain is REGISTERED"
echo "$WHOIS_RESULT" | grep -iE "registrar|creation|updated|status" | head -5
fi
# 2. DNS Resolution
echo -e "\n━━━ [2/7] DNS Resolution ━━━"
IPS=$(dig +short "$DOMAIN" A 2>/dev/null)
if [ -n "$IPS" ]; then
echo "IPs: $IPS"
else
echo "No A records (domain not configured or available)"
fi
# 3. HTTP Check
echo -e "\n━━━ [3/7] HTTP Accessibility ━━━"
HTTP_STATUS=$(curl -sI --max-time 5 "https://$DOMAIN" 2>/dev/null | head -1)
if [ -n "$HTTP_STATUS" ]; then
echo "$HTTP_STATUS"
else
echo "No HTTP response (site not live)"
fi
# 4. Spanish ISP Block Check
echo -e "\n━━━ [4/7] Spanish ISP Block Check (LaLiga) ━━━"
if [ -n "$IPS" ]; then
for IP in $IPS; do
echo "Checking IP: $IP"
BLOCK_DATA=$(curl -s "https://hayahora.futbol/estado/data.json" 2>/dev/null)
if [ -n "$BLOCK_DATA" ]; then
BLOCKED=$(echo "$BLOCK_DATA" | grep -c "\"$IP\"")
if [ "$BLOCKED" -gt 0 ]; then
echo " 🚫 WARNING: IP found in Spanish block list!"
echo "$BLOCK_DATA" | python3 -c "
import json, sys
data = json.load(sys.stdin)
ip = '$IP'
for entry in data.get('data', []):
if entry.get('ip') == ip:
last = entry['stateChanges'][-1]
status = '🚫 BLOCKED' if last['state'] else '✅ OK'
print(f\" {entry['isp']}: {status}\")
" 2>/dev/null
else
echo " ✅ IP not in Spanish block list"
fi
else
echo " ⚠️ Could not reach hayahora.futbol API"
fi
done
else
echo "No IPs to check (domain not configured)"
fi
# 5. Global Accessibility
echo -e "\n━━━ [5/7] Global Accessibility ━━━"
if [ -n "$IPS" ]; then
RESULT=$(curl -s "https://check-host.net/check-http?host=$DOMAIN&max_nodes=5" -H "Accept: application/json" 2>/dev/null)
REQUEST_ID=$(echo "$RESULT" | grep -o '"request_id":"[^"]*' | cut -d'"' -f4)
if [ -n "$REQUEST_ID" ]; then
echo "Check initiated: https://check-host.net/check-report/$REQUEST_ID"
sleep 3
LOCATIONS=$(curl -s "https://check-host.net/check-result/$REQUEST_ID" -H "Accept: application/json" 2>/dev/null | \
python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
for loc, result in data.items():
if result and result[0]:
status = '✅' if result[0][0] == 1 else '❌'
code = result[0][3] if len(result[0]) > 3 else 'N/A'
print(f' {status} {loc}: HTTP {code}')
except: pass
" 2>/dev/null)
if [ -n "$LOCATIONS" ]; then
echo "$LOCATIONS"
fi
fi
else
echo "Skipped (no IPs configured)"
fi
# 6. Security Links
echo -e "\n━━━ [6/7] Security Check Links ━━━"
echo " VirusTotal: https://www.virustotal.com/gui/domain/$DOMAIN"
echo " Sucuri: https://sitecheck.sucuri.net/?scan=$DOMAIN"
echo " Safe Browsing: https://transparencyreport.google.com/safe-browsing/search?url=$DOMAIN"
# 7. History Links
echo -e "\n━━━ [7/7] Domain History ━━━"
echo " Archive.org: https://web.archive.org/web/*/$DOMAIN"
echo " ExpiredDomains: https://www.expireddomains.net/domain-name-search/?q=${DOMAIN%.*}"
echo -e "\n╔════════════════════════════════════════════════════════════╗"
echo "║ Check Complete ║"
echo "╚════════════════════════════════════════════════════════════╝"

View File

@@ -0,0 +1,94 @@
FROM debian:bookworm-slim
# Install system dependencies
RUN apt-get update && apt-get install -y \
# Basic utilities
wget \
gnupg \
curl \
ca-certificates \
# Xvfb for virtual display
xvfb \
# VNC server and noVNC
x11vnc \
novnc \
websockify \
# Window manager (lightweight)
openbox \
# For clicking extension icon automatically
xdotool \
# Credential storage (gnome-keyring for password persistence)
gnome-keyring \
libsecret-1-0 \
libsecret-1-dev \
dbus-x11 \
# Chrome dependencies
fonts-liberation \
libasound2 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libatspi2.0-0 \
libcups2 \
libdbus-1-3 \
libdrm2 \
libgbm1 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libwayland-client0 \
libxcomposite1 \
libxdamage1 \
libxfixes3 \
libxkbcommon0 \
libxrandr2 \
xdg-utils \
libu2f-udev \
libvulkan1 \
# Node.js for Playwriter relay server
nodejs \
npm \
&& rm -rf /var/lib/apt/lists/*
# Install Google Chrome (better extension support than Chromium)
RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg \
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \
&& apt-get update \
&& apt-get install -y google-chrome-stable \
&& rm -rf /var/lib/apt/lists/*
# Install Playwriter CLI globally
RUN npm install -g playwriter
# Create directories for Chrome profile and keyrings
RUN mkdir -p /app \
/root/.config/google-chrome/Default/Extensions \
/root/.local/share/keyrings \
/run/dbus \
&& chmod 700 /root/.local/share/keyrings
WORKDIR /app
# Copy startup scripts
COPY start.sh /app/start.sh
COPY auto-activate.sh /app/auto-activate.sh
RUN chmod +x /app/start.sh /app/auto-activate.sh
# Run as root (required for dbus/keyring; Chrome uses --no-sandbox in containers)
USER root
# Environment variables
ENV DISPLAY=:99
ENV HOME=/root
ENV GNOME_KEYRING_CONTROL=/tmp/keyring
# Expose ports
# 5900 - VNC
# 6080 - noVNC web interface
# 19988 - Playwriter WebSocket relay
EXPOSE 5900 6080 19988
# Health check - verify Chrome and relay are running
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD pgrep -x chrome > /dev/null && curl -s http://localhost:19988 || exit 1
CMD ["/app/start.sh"]

View File

@@ -0,0 +1,219 @@
# Playwriter Browser Container
A persistent browser container running on the NUC for AI-driven browser automation via Playwriter MCP.
## Purpose
Provides an always-available browser for Claude/AI agents to:
- Navigate websites and perform web tasks
- Fill forms, click buttons, extract data
- Debug web applications
- Access services that require browser interaction
- Eliminate need for local browser resources
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ NUC Server │
│ ┌─────────────────────────────────────────────────┐ │
│ │ playwriter-browser container │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │ │
│ │ │ Xvfb │ │ Chrome │ │ Playwriter │ │ │
│ │ │ :99 │ │ +ext │ │ Relay │ │ │
│ │ └──────────┘ └──────────┘ └───────────────┘ │ │
│ │ │ │ │ │ │
│ │ ┌──────────┐ │ ws://19988 │ │
│ │ │ x11vnc │ │ │ │
│ │ │ :5901 │ CDP://9222 │ │
│ │ └──────────┘ │ │
│ │ │ │ │
│ │ ┌──────────┐ │ │
│ │ │ noVNC │ │ │
│ │ │ :6081 │ │ │
│ │ └──────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
Web Browser DevTools MCP Playwriter MCP
(manual) (AI agent) (AI agent)
```
## Ports
| Port | Service | Purpose |
|------|---------|---------|
| 5901 | VNC | Direct VNC client access |
| 6081 | noVNC | Web-based browser view |
| 19988 | Playwriter | MCP WebSocket relay |
| 9222 | CDP | Chrome DevTools Protocol |
## Deployment to NUC
### Option 1: Via Coolify
1. Create a new Docker Compose service in Coolify
2. Paste the contents of `docker-compose.yml`
3. Deploy
### Option 2: Direct Docker Compose
```bash
# Copy files to NUC
scp -r playwriter-browser nuc:/opt/
# SSH to NUC and deploy
ssh nuc
cd /opt/playwriter-browser
docker compose up -d
```
## First-Time Setup
1. Access noVNC at `http://192.168.1.3:6081/vnc.html`
2. Install Playwriter extension from Chrome Web Store:
https://chromewebstore.google.com/detail/playwriter-mcp/jfeammnjpkecdekppnclgkkffahnhfhe
3. Navigate to any website (e.g., google.com)
4. Click the Playwriter extension icon to activate (turns green)
5. The extension stays active until container restart
## Auto-Activation Attempt
The container includes an auto-activation script that tries to click the extension icon using xdotool. However, due to security design, this may not always work. Manual activation via noVNC is the reliable method.
```bash
# Run auto-activation manually
docker exec playwriter-browser /app/auto-activate.sh
```
## MCP Configuration
### Remote Connection (Recommended)
Configure Claude Code to use NUC's browser remotely:
```json
{
"playwriter": {
"command": "npx",
"args": ["playwriter", "--host", "ws://192.168.1.3:19988", "--token", "nuc-browser-token"]
}
}
```
This connects to the NUC browser container, eliminating local resource usage.
### Keep Local Browser Option
If you also want local browser control:
```json
{
"playwriter-local": {
"command": "npx",
"args": ["playwriter"]
}
}
```
### Chrome DevTools MCP (Alternative)
For performance analysis and debugging:
```json
{
"chrome-devtools": {
"command": "npx",
"args": ["chrome-devtools-mcp@latest"]
}
}
```
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `PLAYWRITER_TOKEN` | `nuc-browser-token` | Auth token for remote connections |
## Session Persistence
The container persists browser state across restarts using Docker volumes:
| Volume | Path | Purpose |
|--------|------|---------|
| `playwriter-chrome-profile` | `/root/.config/google-chrome` | Full Chrome profile (cookies, localStorage, passwords, bookmarks) |
| `playwriter-browser-keyring` | `/root/.local/share/keyrings` | Encrypted credentials via gnome-keyring |
| `playwriter-browser-sessions` | `/root/.config/google-chrome/Default/Sessions` | Browser session tabs |
### What's Persisted
- **Login sessions**: Stay logged into websites (Google, GitHub, etc.)
- **Cookies**: Session cookies, preferences
- **Passwords**: Chrome password manager entries (encrypted via gnome-keyring)
- **localStorage/IndexedDB**: Web app data
- **Extensions**: Installed extensions including Playwriter
- **Bookmarks**: Any saved bookmarks
### First Login via noVNC
1. Access noVNC: `http://192.168.1.3:6081/vnc.html`
2. Navigate to website and log in normally
3. Allow Chrome to save password when prompted
4. Sessions persist across container restarts
## Usage Examples
### Via Playwriter MCP
```javascript
// Navigate to a page
await page.goto('https://example.com');
// Get page state
console.log(await accessibilitySnapshot({ page }));
// Interact with elements
await page.locator('aria-ref=e5').click();
// Take screenshot
await page.screenshot({ path: '/tmp/shot.png', scale: 'css' });
```
### Via Chrome DevTools MCP
```
"Check the LCP of https://example.com"
"Analyze network requests on this page"
"Find console errors"
```
## Troubleshooting
### Extension not activated
Access noVNC and click the Playwriter extension icon manually.
### Container unhealthy
```bash
docker logs playwriter-browser
docker exec playwriter-browser pgrep -la chrome
```
### Reset Chrome profile (lose all logins)
```bash
docker compose down
docker volume rm playwriter-chrome-profile playwriter-browser-keyring playwriter-browser-sessions
docker compose up -d
```
### Backup browser profile
```bash
# Create backup of logged-in sessions
docker run --rm -v playwriter-chrome-profile:/data -v $(pwd):/backup alpine tar czf /backup/chrome-profile-backup.tar.gz -C /data .
```
## Security Notes
- Playwriter requires explicit activation per tab (security by design)
- Only tabs where extension is activated can be controlled
- WebSocket relay only accepts localhost connections
- VNC has no password (internal network only)

View File

@@ -0,0 +1,64 @@
#!/bin/bash
# Auto-activate Playwriter extension by clicking its icon
# This script attempts to find and click the Playwriter extension icon
export DISPLAY=:99
echo "Attempting to auto-activate Playwriter extension..."
# Wait for Chrome to fully load
sleep 5
# Get Chrome window ID
CHROME_WINDOW=$(xdotool search --name "Chrome" | head -1)
if [ -z "$CHROME_WINDOW" ]; then
echo "Chrome window not found. Trying again..."
sleep 5
CHROME_WINDOW=$(xdotool search --name "Chrome" | head -1)
fi
if [ -n "$CHROME_WINDOW" ]; then
echo "Found Chrome window: $CHROME_WINDOW"
# Focus the Chrome window
xdotool windowactivate --sync $CHROME_WINDOW
sleep 1
# Navigate to a page first (extension won't activate on chrome:// pages)
xdotool key ctrl+l
sleep 0.5
xdotool type "https://www.google.com"
xdotool key Return
sleep 3
# The extension icon is typically in the top-right corner
# Get window geometry
GEOMETRY=$(xdotool getwindowgeometry $CHROME_WINDOW)
WIDTH=$(echo "$GEOMETRY" | grep "Geometry" | sed 's/.*Geometry: \([0-9]*\)x.*/\1/')
# Extension icons are usually ~100-200px from the right edge, ~50px from top
# Try multiple positions since exact location varies
echo "Attempting to click extension icon area..."
# Click positions to try (relative to window, near extension area)
for OFFSET in 100 130 160 190 220; do
X_POS=$((WIDTH - OFFSET))
Y_POS=50
echo "Trying position: $X_POS, $Y_POS"
xdotool mousemove --window $CHROME_WINDOW $X_POS $Y_POS
sleep 0.3
xdotool click 1
sleep 1
done
echo "Auto-activation attempts complete."
echo "If extension didn't activate, please click it manually via noVNC at:"
echo "http://localhost:6080/vnc.html"
else
echo "Could not find Chrome window. Please activate extension manually."
fi
# Alternative: Use Chrome DevTools Protocol to check extension status
# This would require more sophisticated scripting with CDP

View File

@@ -0,0 +1,48 @@
version: '3.8'
services:
playwriter-browser:
build:
context: .
dockerfile: Dockerfile
container_name: playwriter-browser
hostname: playwriter-browser
ports:
- "5901:5900" # VNC (use 5901 to avoid conflicts)
- "6081:6080" # noVNC web interface
- "19988:19988" # Playwriter WebSocket relay
- "9222:9222" # Chrome DevTools Protocol
volumes:
# Persist full Chrome profile (login sessions, cookies, localStorage, bookmarks, passwords)
- chrome-profile:/root/.config/google-chrome
# Persist keyrings for encrypted credentials (gnome-keyring)
- browser-keyring:/root/.local/share/keyrings
# Persist session data for quick restore
- browser-sessions:/root/.config/google-chrome/Default/Sessions
environment:
- DISPLAY=:99
- PLAYWRITER_TOKEN=${PLAYWRITER_TOKEN:-nuc-browser-token}
# Enable password saving in Chrome
- CHROME_ENABLE_PASSWORDS=true
# Chrome requires shared memory for stability
shm_size: 2gb
# Chrome capabilities
cap_add:
- SYS_ADMIN
security_opt:
- seccomp:unconfined
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pgrep -x chrome > /dev/null || exit 1"]
interval: 30s
timeout: 10s
start_period: 60s
retries: 3
volumes:
chrome-profile:
name: playwriter-chrome-profile
browser-keyring:
name: playwriter-browser-keyring
browser-sessions:
name: playwriter-browser-sessions

View File

@@ -0,0 +1,90 @@
#!/bin/bash
set -e
echo "=== Starting Playwriter Browser Container ==="
# Start Xvfb (virtual display)
echo "Starting Xvfb..."
Xvfb :99 -screen 0 1920x1080x24 -ac +extension GLX +render -noreset &
export DISPLAY=:99
sleep 2
# Initialize D-Bus for gnome-keyring
echo "Starting D-Bus session..."
mkdir -p /run/dbus
if [ ! -f /run/dbus/pid ]; then
dbus-daemon --system --fork 2>/dev/null || true
fi
export $(dbus-launch)
echo "D-Bus started: $DBUS_SESSION_BUS_ADDRESS"
# Initialize gnome-keyring (enables Chrome password saving)
echo "Starting gnome-keyring daemon..."
mkdir -p /root/.local/share/keyrings
# Create default keyring with empty password for headless operation
eval $(echo '' | gnome-keyring-daemon --unlock --start --components=secrets,pkcs11 2>/dev/null || true)
export GNOME_KEYRING_CONTROL
export SSH_AUTH_SOCK
echo "Keyring initialized at: $GNOME_KEYRING_CONTROL"
# Start window manager (needed for proper window handling)
echo "Starting Openbox window manager..."
openbox &
sleep 1
# Start VNC server
echo "Starting VNC server on port 5900..."
x11vnc -display :99 -forever -shared -rfbport 5900 -nopw -bg
sleep 1
# Start noVNC web interface
echo "Starting noVNC web interface on port 6080..."
websockify --web=/usr/share/novnc/ 6080 localhost:5900 &
sleep 1
# Start Playwriter relay server in background (binds to all interfaces for remote access)
echo "Starting Playwriter relay server on port 19988..."
playwriter serve --host 0.0.0.0 --token "${PLAYWRITER_TOKEN:-nuc-browser-token}" &
RELAY_PID=$!
sleep 2
# Start Chrome with remote debugging enabled
# The extension needs to be installed from Chrome Web Store on first run
echo "Starting Google Chrome..."
google-chrome-stable \
--no-sandbox \
--disable-gpu \
--disable-dev-shm-usage \
--remote-debugging-port=9222 \
--remote-debugging-address=0.0.0.0 \
--user-data-dir=/root/.config/google-chrome \
--start-maximized \
--no-first-run \
--disable-default-apps \
--disable-background-networking \
--disable-sync \
--disable-translate \
--password-store=gnome \
--enable-features=PasswordImport \
"chrome://extensions" &
CHROME_PID=$!
echo ""
echo "=== Playwriter Browser Ready ==="
echo "VNC: vnc://localhost:5900"
echo "noVNC Web: http://localhost:6080/vnc.html"
echo "Playwriter Relay: ws://localhost:19988"
echo "Chrome DevTools: http://localhost:9222"
echo ""
echo "IMPORTANT: On first run, install Playwriter extension from Chrome Web Store:"
echo "https://chromewebstore.google.com/detail/playwriter-mcp/jfeammnjpkecdekppnclgkkffahnhfhe"
echo ""
echo "Then run: /app/auto-activate.sh to try auto-activating the extension"
echo ""
# Wait for auto-activation script if extension is installed
sleep 10
/app/auto-activate.sh &
# Keep container running
wait $CHROME_PID

157
services.yaml Normal file
View File

@@ -0,0 +1,157 @@
# NUC Server Services Inventory
# Last updated: 2026-02-01
server:
hostname: nuc
ip: 192.168.1.3
user: alezmad
ssh_key: ~/.ssh/id_ed25519_nuc
services:
# ============ Core Infrastructure ============
coolify:
port: 8000
url: http://192.168.1.3:8000
description: Self-hosted PaaS - manages all other services
container: coolify
type: standalone
traefik:
ports: [80, 443, 8080]
description: Reverse proxy managed by Coolify
container: coolify-proxy
# ============ Development ============
gitea:
port: 3030
ssh_port: 22222
url: http://192.168.1.3:3030
description: Git hosting + OAuth2 provider
container: gitea-*
coolify_service_id: unknown
oauth2:
outline_client_id: 249a3a1d-92d4-47d8-b4a9-81c64e1da6ab
adminer:
port: 8088
url: http://192.168.1.3:8088
description: Database administration UI
container: adminer
type: standalone
# ============ Documentation ============
outline:
port: 3080
url: http://192.168.1.3:3080
description: Team wiki and documentation
container: outline-pccg80wks4c084008owokkkg
coolify_service_id: 9
proxy: nginx (strips HSTS)
auth: OIDC via Gitea
database: postgres-pccg80wks4c084008owokkkg
cache: redis-pccg80wks4c084008owokkkg
# ============ Dashboard ============
homepage:
port: 3000
url: http://192.168.1.3:3000
description: Service dashboard
container: homepage-eo0g84scsss4osk0skk040ck
config_path: /opt/homepage/config/
# ============ Automation ============
n8n:
port: 5678
url: http://192.168.1.3:5678
description: Workflow automation
container: n8n-uk0o04o0g84s4sc80kkoooc0
runners: task-runners-uk0o04o0g84s4sc80kkoooc0
# ============ Security ============
vaultwarden:
port: 8222
url: http://192.168.1.3:8222
description: Bitwarden-compatible password manager
container: vaultwarden-h40w0ss4kgs0c8cgc0sc8k48
authentik:
port: 9090
url: http://192.168.1.3:9090
description: Identity provider (partially configured)
containers:
- authentik-server-e8owcw0s4wcswc4w4css0sws
- authentik-worker-e8owcw0s4wcswc4w4css0sws
database: postgresql-e8owcw0s4wcswc4w4css0sws
# ============ Storage ============
minio:
console_port: 9001
api_port: 9000
console_url: http://192.168.1.3:9001
api_url: http://192.168.1.3:9000
description: S3-compatible object storage
container: minio-dg4wkgg8skcssww0040sgk80
filebrowser:
port: 8085
url: http://192.168.1.3:8085
description: Web-based file manager
container: filebrowser-o4swwwsowwg88coo0ws4cg48
kopia:
port: 51515
url: http://192.168.1.3:51515
description: Backup management
container: kopia
type: standalone
# ============ Notifications ============
ntfy:
port: 8333
url: http://192.168.1.3:8333
description: Push notification service
container: ntfy-xgkkg8gkgg048g8gkc8ck4os
# ============ Monitoring ============
uptime_kuma:
port: 3001
url: http://192.168.1.3:3001
description: Service uptime monitoring
container: uptime-kuma
type: standalone
dozzle:
port: 9999
url: http://192.168.1.3:9999
description: Docker log viewer
container: dozzle
type: standalone
# Port forwarding containers (for Coolify internal services)
port_forwarders:
- name: port-fwd-outline
image: nginx:alpine
external_port: 3080
internal_target: outline-pccg80wks4c084008owokkkg:3000
note: Strips HSTS headers
- name: port-fwd-homepage
image: alpine/socat
external_port: 3000
- name: port-fwd-gitea
image: alpine/socat
external_port: 3030
- name: port-fwd-minio-console
image: alpine/socat
external_port: 9001
- name: port-fwd-minio-api
image: alpine/socat
external_port: 9000
- name: port-fwd-authentik
image: alpine/socat
external_port: 9090
internal_port: 9000

23
setup-ssh.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/bin/bash
# Setup SSH config for NUC server
SSH_CONFIG="$HOME/.ssh/config"
NUC_CONFIG="Host nuc
HostName 192.168.1.3
User alezmad
IdentityFile ~/.ssh/id_ed25519_nuc"
# Check if nuc config already exists
if grep -q "Host nuc" "$SSH_CONFIG" 2>/dev/null; then
echo "NUC SSH config already exists in $SSH_CONFIG"
else
echo "Adding NUC config to $SSH_CONFIG"
echo "" >> "$SSH_CONFIG"
echo "$NUC_CONFIG" >> "$SSH_CONFIG"
echo "Done! You can now connect with: ssh nuc"
fi
# Test connection
echo ""
echo "Testing connection..."
ssh -o ConnectTimeout=5 nuc "echo 'Successfully connected to NUC!'" 2>/dev/null || echo "Connection failed. Make sure the SSH key exists at ~/.ssh/id_ed25519_nuc"