Desktop-bound GoLogin/AdsPower workflows do not scale on Linux CI. Multilogin cloud API lets workers on Ubuntu launch profiles remotely and attach via CDP — no GUI on the runner. This recipe covers headless flags, Xvfb fallback, and a GitHub Actions skeleton.
Architecture
┌─────────────────────┐ ┌──────────────────────────┐ │ GitHub Actions │ HTTPS │ Multilogin Cloud API │ │ ubuntu-latest │ ──────► │ profile/start → CDP URL │ │ Playwright client │ ◄────── │ Mimic on MLX infra │ └─────────────────────┘ WS └──────────────────────────┘
Alternative: self-hosted Linux VM with Multilogin agent + local API — same CDP attach code, different base URL.
Headless launch payload
POST /profile/start
{
"profile_id": "uuid",
"headless": true,
"automation": true
}
If headless crashes on target site (canvas/WebGL checks), retry with headless: false + Xvfb on a dedicated worker — not on shared GitHub-hosted runners for prod accounts.
Xvfb fallback (self-hosted VM)
sudo apt-get install -y xvfb Xvfb :99 -screen 0 1920x1080x24 & export DISPLAY=:99 python run_job.py
GitHub Actions workflow
name: mlx-profile-job
on:
workflow_dispatch:
inputs:
profile_id:
required: true
jobs:
run:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install httpx playwright
- run: playwright install chromium
- name: Run profile job
env:
MLX_TOKEN: ${{ secrets.MLX_TOKEN }}
MLX_PROFILE_ID: ${{ inputs.profile_id }}
run: python ci_mlx_job.py
ci_mlx_job.py (minimal)
import os
import sys
import httpx
from playwright.sync_api import sync_playwright
MLX = "https://api.multilogin.com"
TOKEN = os.environ["MLX_TOKEN"]
PROFILE = os.environ["MLX_PROFILE_ID"]
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
def main():
r = httpx.post(f"{MLX}/profile/start",
json={"profile_id": PROFILE, "headless": True},
headers=HEADERS, timeout=90)
r.raise_for_status()
cdp = r.json().get("cdp_url") or r.json().get("wsUrl")
if not cdp:
sys.exit("No CDP URL")
try:
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(cdp, timeout=60_000)
page = browser.contexts[0].pages[0]
page.goto("https://example.com", wait_until="domcontentloaded")
print("title:", page.title())
browser.close()
finally:
httpx.post(f"{MLX}/profile/stop",
json={"profile_id": PROFILE}, headers=HEADERS)
if __name__ == "__main__":
main()
CI hardening checklist
- Secrets —
MLX_TOKENin GitHub Secrets; never in repo - Concurrency — one profile per job; matrix jobs need separate profile UUIDs
- Timeout — job-level + CDP connect 60s + navigation timeout
- Always stop —
finallyblock calls profile/stop (billing + slot release) - Artifacts — upload trace/screenshot on failure only
- Rate limits — queue scheduled runs; avoid 10 parallel starts
When NOT to use GitHub-hosted runners
Shared runner IPs are datacenter ASNs — fine for smoke tests, risky for logged-in prod accounts. Use sticky residential proxy inside profile + dedicated self-hosted worker for revenue accounts. See proxy types and scaling guide.
Related
Disclosure: MLX-MMO affiliated with Multilogin. Headless and cloud API availability depend on your plan — verify with Multilogin.