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,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