Skip to content

Playwright Screenshot API

Playwright is a browser automation service used by Aegis for visual monitoring, screenshots, and web scraping.

Overview

  • Image: ghcr.io/vlazic/playwright-screenshot-api:latest
  • Container: aegis-playwright
  • Internal Port: 3000
  • External Port: 3002 (mapped to host)
  • URL: http://playwright:3000 (from scheduler container)
  • Network: traefik_proxy

Architecture

┌───────────────────────────────────────┐
│      Aegis Scheduler                  │
│                                       │
│  aegis.monitor.scheduler              │
│         ↓                             │
│  Website monitoring jobs              │
│         ↓                             │
│  HTTP POST http://playwright:3000     │
└───────────────────────────────────────┘
┌───────────────────────────────────────┐
│   Playwright Screenshot API           │
│                                       │
│  - Launches headless Chromium         │
│  - Renders page                       │
│  - Captures screenshot                │
│  - Returns PNG/JPEG                   │
└───────────────────────────────────────┘

Configuration

Docker Compose

playwright:
  image: ghcr.io/vlazic/playwright-screenshot-api:latest
  container_name: aegis-playwright
  ports:
    - "3002:3000"
  restart: unless-stopped
  networks:
    traefik_proxy:
  healthcheck:
    test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:3000/health"]
    interval: 30s
    timeout: 10s
    retries: 3

Environment Variables (Scheduler)

environment:
  PLAYWRIGHT_URL: http://playwright:3000

API Endpoints

POST /screenshot

Capture a screenshot of a URL.

Request:

curl -X POST http://localhost:3002/screenshot \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://aegisagent.ai",
    "width": 1920,
    "height": 1080,
    "fullPage": false,
    "type": "png"
  }'

Request Body:

{
  "url": "https://example.com",          // Required
  "width": 1920,                         // Optional, default: 1920
  "height": 1080,                        // Optional, default: 1080
  "fullPage": false,                     // Optional, default: false
  "type": "png",                         // Optional: "png" or "jpeg", default: "png"
  "quality": 90,                         // Optional: 0-100 (JPEG only)
  "waitFor": 0,                          // Optional: milliseconds to wait
  "selector": null,                      // Optional: CSS selector to screenshot
  "clip": {                              // Optional: specific region
    "x": 0,
    "y": 0,
    "width": 800,
    "height": 600
  }
}

Response: Binary image data (PNG or JPEG)

Response Headers: - Content-Type: image/png or image/jpeg - Content-Length: <size>

GET /health

Health check endpoint.

Request:

curl http://localhost:3002/health

Response:

{
  "status": "healthy"
}

Usage in Aegis

Website Monitoring

Location: /home/agent/projects/aegis-core/aegis/monitor/

Scheduled job captures screenshots of monitored websites:

import aiohttp
import asyncio
from pathlib import Path

async def capture_screenshot(url: str, output_path: Path):
    """Capture screenshot using Playwright API."""
    playwright_url = os.getenv("PLAYWRIGHT_URL", "http://playwright:3000")

    payload = {
        "url": url,
        "width": 1920,
        "height": 1080,
        "fullPage": True,
        "type": "png"
    }

    async with aiohttp.ClientSession() as session:
        async with session.post(
            f"{playwright_url}/screenshot",
            json=payload,
            timeout=aiohttp.ClientTimeout(total=60)
        ) as resp:
            if resp.status == 200:
                image_data = await resp.read()
                output_path.write_bytes(image_data)
                return True
            else:
                error = await resp.text()
                logger.error("screenshot_failed", url=url, status=resp.status, error=error)
                return False

Screenshot Storage

Screenshots are stored in volume-mounted directory:

/home/agent/projects/aegis-core/data/monitor/screenshots/
├── aegisagent.ai_2026-01-25_12-00-00.png
├── intel.aegisagent.ai_2026-01-25_12-05-00.png
└── ...

Mounted in scheduler container:

volumes:
  - /home/agent/projects/aegis-core/data/monitor/screenshots:/tmp/aegis/monitor/screenshots

Competitor Monitoring

Captures screenshots of competitor pages for change detection:

from aegis.monitor.competitor import CompetitorMonitor

monitor = CompetitorMonitor()

# Add competitor
await monitor.add_competitor(
    name="Competitor Corp",
    url="https://competitor.com/pricing"
)

# Capture and compare
result = await monitor.check_competitor("Competitor Corp")

if result["changed"]:
    print(f"Change detected! Diff: {result['diff_percentage']}%")
    # Screenshot saved to data/monitor/screenshots/

Browser Configuration

Default Settings

The Playwright Screenshot API launches Chromium with:

  • Headless mode: Enabled (no GUI)
  • Viewport: Configurable per request (default: 1920x1080)
  • User agent: Chromium default
  • JavaScript: Enabled
  • Images: Enabled
  • Cookies: Isolated per request
  • Network conditions: Normal (not throttled)

Supported Features

  • Full page screenshots: Scrolls and captures entire page
  • Element screenshots: Target specific CSS selectors
  • Custom viewports: Any resolution
  • Wait conditions: Wait for network idle, specific selectors
  • Authentication: Basic auth, cookies (if supported by API)
  • Responsive testing: Change viewport size

Browser Engines

The API uses Chromium (not Firefox or WebKit). This provides: - Best compatibility with modern web standards - Consistent rendering across platforms - DevTools Protocol support

Performance

Typical Timings

  • Simple page: 2-5 seconds
  • Complex SPA: 5-10 seconds
  • Full page screenshot: 10-15 seconds (depends on page length)

Resource Usage

  • Memory: ~200-500 MB per browser instance
  • CPU: Burst during page render, idle otherwise
  • Disk: Minimal (no persistent cache)

Concurrency

The API handles multiple concurrent requests. Each request launches a separate browser context (isolated).

Recommended limits: - Max 5 concurrent screenshots - Timeout requests after 60 seconds

Error Handling

Common Errors

Connection refused:

Error: connect ECONNREFUSED http://playwright:3000
- Check container is running: docker ps | grep playwright - Check network connectivity: docker exec aegis-scheduler ping playwright

Timeout:

Error: Timeout of 60000ms exceeded
- Page took too long to load - Increase waitFor in request - Check URL is accessible

Page load failed:

Error: net::ERR_NAME_NOT_RESOLVED
- DNS resolution failed - Check URL is valid - Check network can reach external internet

Browser crash:

Error: Browser closed unexpectedly
- Check container memory limits - Restart container: docker restart aegis-playwright

Retry Logic

Implement retries with exponential backoff:

import asyncio

async def capture_with_retry(url: str, max_retries: int = 3):
    for attempt in range(max_retries):
        try:
            return await capture_screenshot(url)
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            wait_time = 2 ** attempt  # 1s, 2s, 4s
            logger.warning("screenshot_retry", url=url, attempt=attempt, wait=wait_time)
            await asyncio.sleep(wait_time)

Monitoring

Health Check

Container includes built-in healthcheck:

docker inspect aegis-playwright | jq '.[0].State.Health'

Expected:

{
  "Status": "healthy",
  "FailingStreak": 0,
  "Log": [
    {
      "ExitCode": 0,
      "Output": "..."
    }
  ]
}

Logs

# View container logs
docker logs aegis-playwright -f

# Filter for errors
docker logs aegis-playwright 2>&1 | grep -i error

# Check recent requests
docker logs aegis-playwright --tail 100

Resource Usage

docker stats aegis-playwright

Monitor for: - Memory spikes (>1GB indicates issue) - CPU usage (sustained 100% indicates stuck render) - Network I/O (unexpected high traffic)

Security

Sandboxing

Chromium runs with sandbox enabled by default. This isolates browser processes from the host system.

Benefits: - Prevents malicious pages from escaping container - Limits damage from browser exploits - Resource isolation

Note: If sandbox fails to start, check kernel capabilities.

URL Validation

Always validate URLs before sending to Playwright:

from urllib.parse import urlparse

def is_safe_url(url: str) -> bool:
    """Validate URL is safe to screenshot."""
    try:
        parsed = urlparse(url)
        # Allow only http/https
        if parsed.scheme not in ("http", "https"):
            return False
        # Block localhost/private IPs (optional)
        hostname = parsed.hostname
        if hostname in ("localhost", "127.0.0.1", "0.0.0.0"):
            return False
        return True
    except:
        return False

Rate Limiting

Implement rate limits to prevent abuse:

from collections import defaultdict
import time

# Simple in-memory rate limiter
screenshot_counts = defaultdict(list)

def check_rate_limit(client_ip: str, limit: int = 10, window: int = 60):
    """Allow max 'limit' screenshots per 'window' seconds."""
    now = time.time()
    timestamps = screenshot_counts[client_ip]

    # Remove old timestamps
    timestamps[:] = [t for t in timestamps if now - t < window]

    if len(timestamps) >= limit:
        return False

    timestamps.append(now)
    return True

Development

Local Testing

# Start Playwright container
docker compose up -d playwright

# Test screenshot endpoint
curl -X POST http://localhost:3002/screenshot \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "fullPage": true}' \
  -o test.png

# View screenshot
open test.png  # macOS
xdg-open test.png  # Linux

Custom Playwright Script

For advanced use cases, run custom Playwright scripts:

from playwright.async_api import async_playwright

async def custom_screenshot():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page(viewport={"width": 1920, "height": 1080})

        await page.goto("https://example.com")

        # Wait for specific element
        await page.wait_for_selector(".main-content")

        # Capture screenshot
        await page.screenshot(path="custom.png", full_page=True)

        await browser.close()

Note: This requires Playwright Python library installed in your environment.

Debugging

Enable verbose logging:

# Run container with debug logs
docker run --rm \
  -e DEBUG=pw:api \
  -p 3000:3000 \
  ghcr.io/vlazic/playwright-screenshot-api:latest

Troubleshooting

Container won't start

Check logs:

docker logs aegis-playwright

Common issues: - Port 3000 already in use - Insufficient memory - Kernel missing capabilities for Chromium sandbox

Screenshots are blank

Possible causes: - Page requires JavaScript (check waitFor delay) - Page blocks headless browsers - Page uses unusual viewport detection

Solutions: - Increase waitFor milliseconds - Use waitForSelector for specific element - Try different viewport size

Memory leaks

If container memory grows over time:

  1. Check for stuck browser processes:

    docker exec aegis-playwright ps aux
    

  2. Restart container:

    docker restart aegis-playwright
    

  3. Add memory limit:

    deploy:
      resources:
        limits:
          memory: 1G