Skip to content
Last updated

Deploy MoreLogin on a headless Ubuntu server and automate browser profiles via the Local API — no desktop environment required.


What You'll Achieve

By the end of this guide you will have:

  1. A running MoreLogin instance on an Ubuntu 24.04 headless server
  2. Network forwarding configured so external machines can connect via CDP (Chrome DevTools Protocol)
  3. A working Python automation script that creates, launches, controls, and cleans up browser profiles

Architecture Overview

┌──────────────────────────────────────────────────────────┐
│                    Ubuntu 24.04 Server                   │
│                                                          │
│  ┌──────────────┐    ┌───────────────────────────────┐   │
│  │   xvfb       │───▶│  MoreLogin AppImage           │   │
│  │ (virtual     │    │  Local API :40000             │   │
│  │  display)    │    │  CDP debug :<dynamic>         │   │
│  │              │    │    (127.0.0.1, per profile)   │   │
│  └──────────────┘    └───────────────────────────────┘   │
│                              │                           │
│                        socat forwarding                  │
│                              │                           │
│                     0.0.0.0:40001 → 127.0.0.1:40000      │
│                     0.0.0.0:<N+1> → 127.0.0.1:<N>        │
└──────────────────────────────────────────────────────────┘

                          External machine
                     (Playwright / Puppeteer / Selenium)

[!NOTE] The CDP debug port is dynamic — each browser profile gets its own port, returned by the /api/env/start endpoint. The diagram above uses <N> as a placeholder.


Prerequisites

RequirementDetails
Operating SystemUbuntu 24.04 Server (x86_64)
Recommended Specs8 vCPU, 8 GB RAM (supports ~5 concurrent profiles)
NetworkOutbound internet access; open inbound port 40001 and the forwarded CDP ports you choose, or use SSH tunnels instead
Python (optional)Python 3.8+ with pip for running the example script

Step 1 — Install System Dependencies

Connect to your server via SSH and install the required packages:

# FUSE support (required for AppImage)
sudo apt install -y libfuse2t64

# GTK / accessibility / display libraries
sudo apt install -y libatk1.0-0 libatk-bridge2.0-0 libatspi2.0-0
sudo apt install -y libcups2
sudo apt install -y libgtk-3-0 libgdk-pixbuf2.0-0
sudo apt install -y libgbm1 libxkbcommon0 libasound2t64

# Virtual framebuffer (headless display)
sudo apt install -y xvfb

# TCP forwarder
sudo apt install -y socat

Install Fonts (Optional)

If browser pages render without text or show missing characters, install the corresponding font packages:

# CJK (Chinese, Japanese, Korean)
sudo apt install -y fonts-noto-cjk fonts-noto-cjk-extra

# Arabic
sudo apt install -y fonts-noto-color-emoji fonts-noto-extra

For other languages, install the matching Noto font family package.


Step 2 — Download & Launch MoreLogin

2.1 Download the AppImage

wget https://get.morelogin.com/client/prod/linux/x64/2.54.0/MoreLogin_x86_64_2.54.0.AppImage
chmod +x MoreLogin_x86_64_2.54.0.AppImage

Replace 2.54.0 with the latest version available from your MoreLogin account or the download page.

2.2 Start MoreLogin in the Background

Use xvfb-run to provide a virtual display, then run the AppImage:

nohup xvfb-run -a ./MoreLogin_x86_64_2.54.0.AppImage --no-sandbox > morelogin.log 2>&1 &

Verify it started successfully:

# Check the process is running
ps aux | grep MoreLogin

The process may take 5–10 seconds to fully initialize.


Step 3 — Log In to MoreLogin via API (Required)

[!CAUTION] You must log in before calling any other Local API endpoint. On a headless server there is no GUI to log in manually, so you must authenticate via the API. Without this step, all API calls will return:

{"status": "error", "code": 401, "message": "Your login status has expired, please log in again"}

3.1 Get Your API Credentials

Open the MoreLogin desktop client (on any machine where you're logged in) and navigate to Settings → API & MCP. Copy the APP ID and API Key from the Open API section:

MoreLogin API Settings — Copy APP ID and API Key from the Open API section

3.2 Log In via curl

Call the login endpoint with your credentials:

curl -X POST http://127.0.0.1:40000/api/user/login \
  -H "Content-Type: application/json" \
  -d '{
    "apiId": "YOUR_APP_ID",
    "apiKey": "YOUR_API_KEY"
  }'

A successful response looks like:

{"code": 0, "msg": null, "data": true}

3.3 Verify the Login

Confirm the API is ready by listing browser profiles:

curl -s -X POST http://127.0.0.1:40000/api/env/page \
  -H "Content-Type: application/json" \
  -d '{"pageNo": 1, "pageSize": 1}'

A {"code":0, ...} response means you are logged in and the API is ready.

Checkpoint: Your MoreLogin server is fully operational. Proceed to Step 4 for remote access, or jump directly to Step 5 if running scripts on the same server.

[!NOTE] The login session persists as long as the MoreLogin process is running. If you restart the AppImage, you will need to log in again.


Step 4 — Configure Network Forwarding (socat)

By default, both the Local API (:40000) and CDP debug ports bind to 127.0.0.1. If you need to access them from an external machine (e.g., your development laptop), use socat to forward traffic.

[!WARNING] Security risk — do not expose these ports to the public internet.

  • The Local API has no built-in authentication for most endpoints.
  • A CDP debug port grants full remote control of the browser instance (read cookies, inject scripts, capture screenshots).

Recommendations:

  • Use an SSH tunnel instead of socat for remote access: ssh -L 40000:127.0.0.1:40000 user@server
  • If you must use socat, restrict access with firewall rules to specific IPs only
  • Use a VPN or cloud provider security groups to limit inbound traffic
  • Never open ports 40001 / CDP ports to 0.0.0.0 on a public-facing server without IP restrictions

4.1 Forward the Local API Port

nohup socat TCP-LISTEN:40001,fork,reuseaddr,bind=0.0.0.0 TCP:127.0.0.1:40000 &

External machines can now reach the API at http://<server-ip>:40001.

4.2 Forward CDP Debug Ports

When you start a browser profile via the API, the response includes a dynamic debugPort (e.g., 9222). Each profile may receive a different port. Forward it so external automation tools (Playwright, Puppeteer, Selenium) can connect:

# Example: if debugPort=9222, forward to external port 9223 (debugPort + 1)
# Adjust both ports to match the actual debugPort returned by /api/env/start
nohup socat TCP-LISTEN:9223,fork,reuseaddr,bind=0.0.0.0 TCP:127.0.0.1:9222 &

[!WARNING] When running multiple profiles concurrently, ensure forwarded ports do not collide with other profiles' debug ports. For example, if profile A gets debugPort=9222 and you forward to 9223, but profile B receives debugPort=9223, there will be a port conflict. Consider using a larger offset or a dedicated port range.

[!TIP] In production, create socat forwarding dynamically after each /api/env/start call, using the returned debugPort. See the Python example for a working implementation.

If running automation scripts on the same server, you can skip socat and connect directly to 127.0.0.1.

4.3 Open Firewall Ports

If you use socat, restrict access to trusted IPs only:

# Allow only a specific IP (recommended)
sudo ufw allow from <YOUR_IP> to any port 40001 proto tcp
sudo ufw allow from <YOUR_IP> to any port 9223 proto tcp

# Or allow from any IP (NOT recommended for production)
# sudo ufw allow 40001/tcp
# sudo ufw allow 9223/tcp

For the most secure remote access, use an SSH tunnel — no firewall changes or socat needed.

Because the CDP port is dynamic (assigned when you start a profile), the workflow is:

  1. Tunnel the API port first:

    # Run this on your local machine
    ssh -L 40000:127.0.0.1:40000 user@<server-ip>
  2. Start the profile via the tunneled API (http://127.0.0.1:40000/api/env/start) and read the returned debugPort.

  3. Open a second tunnel for the CDP port:

    # Replace <debugPort> with the actual port returned by the API
    ssh -L <debugPort>:127.0.0.1:<debugPort> user@<server-ip>
  4. Connect to http://127.0.0.1:<debugPort> from your local Playwright / Puppeteer scripts as if the server were local.

[!TIP] You can combine both tunnels in one command if you know the port range in advance, e.g.:

ssh -L 40000:127.0.0.1:40000 -L 9222:127.0.0.1:9222 -L 9223:127.0.0.1:9223 user@<server-ip>

But in practice it's easier to start the API tunnel first, then add per-profile tunnels as needed.


Step 5 — Quick Automation Example

Below is a minimal Python example showing the full lifecycle of a browser profile. For the complete production-ready script with concurrency and error handling, see the full example on GitHub.

5.1 Install Python Dependencies

pip install requests playwright

Playwright is used here only for its CDP client (connect_over_cdp). You do not need to run playwright install — MoreLogin provides its own browser.

5.2 Create → Start → Automate → Cleanup

This example assumes the script runs on the same server as MoreLogin. For remote scenarios, see the notes after the code.

import requests
from playwright.sync_api import sync_playwright

# ── Configuration ──────────────────────────────────────────
# Local:  script runs on the SAME server as MoreLogin
# Remote: script runs on a DIFFERENT machine — see notes below
API_BASE  = "http://127.0.0.1:40000"


# ① Create a browser profile
resp = requests.post(f"{API_BASE}/api/env/create/quick", json={
    "browserTypeId": 1,
    "operatorSystemId": 1,
    "quantity": 1
})
resp_data = resp.json()
assert resp_data["code"] == 0, f"Create failed: {resp_data}"
env_id = resp_data["data"]["envIds"][0]
print(f"✅ Created profile: {env_id}")


# ② Start the profile (headless)
resp = requests.post(f"{API_BASE}/api/env/start", json={
    "envId": env_id
})
resp_data = resp.json()
assert resp_data["code"] == 0, f"Start failed: {resp_data}"
debug_port = resp_data["data"]["debugPort"]   # Dynamic — different for each profile
print(f"✅ Started — debug port: {debug_port}")


# ③ Build the CDP URL
cdp_url = f"http://127.0.0.1:{debug_port}"


# ④ Connect via CDP and automate
with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(cdp_url)
    page = browser.contexts[0].pages[0]
    page.goto("https://www.google.com")
    page.screenshot(path=f"screenshot_{env_id}.png")
    print(f"✅ Screenshot saved")
    # Use disconnect() — not close() — to detach without killing the browser.
    # The profile will be stopped cleanly via the API in step ⑤.
    browser.disconnect()


# ⑤ Stop the profile
requests.post(f"{API_BASE}/api/env/close", json={"envId": env_id})
print(f"✅ Profile stopped")


# ⑥ Delete the profile
requests.post(f"{API_BASE}/api/env/remove", json={"envIds": [env_id]})
print(f"✅ Profile deleted")

[!NOTE] Running from a remote machine? Two approaches:

Option A — SSH tunnel (recommended): Set up SSH tunnels from your local machine to the server (see § 4.4), then keep API_BASE = "http://127.0.0.1:40000" and cdp_url = f"http://127.0.0.1:{debug_port}" — SSH makes the remote ports appear local.

Option B — socat on the server: On the server, start socat forwarding for the API port and for each CDP port:

# Run these on the SERVER, not on your local machine
socat TCP-LISTEN:40001,fork,reuseaddr,bind=0.0.0.0 TCP:127.0.0.1:40000 &
socat TCP-LISTEN:$((debug_port+1)),fork,reuseaddr,bind=0.0.0.0 TCP:127.0.0.1:$debug_port &

Then in your script, set API_BASE = "http://<server-ip>:40001" and cdp_url = f"http://<server-ip>:{debug_port + 1}". ⚠️ Restrict access with firewall rules — see § 4.3.

[!IMPORTANT] The resp.json()["data"]["envIds"] structure matches the current /api/env/create/quick response format. If you encounter a different shape (e.g., data: ["id1", ...]), check the API Reference for your version — response formats may vary across releases.


Performance Benchmark

The following results were obtained on an Ubuntu 24.04 Server VM (8 vCPU, 8 GB RAM) using the full stress test script:

MetricValue
Total runs100
Concurrency4 (simultaneous)
Success rate100.0%
Total time604.06 s
Avg time per task6.04 s
Throughput0.17 tasks/s

These numbers serve as a baseline. Actual performance depends on server specs, network conditions, and page complexity.


Production Deployment (systemd)

For production servers, run MoreLogin as a systemd service to get automatic startup on boot, restart on crash, and centralized logging.

Create the Service File

sudo tee /etc/systemd/system/morelogin.service > /dev/null <<'EOF'
[Unit]
Description=MoreLogin Browser (headless)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=root
WorkingDirectory=/opt/morelogin
ExecStart=/usr/bin/xvfb-run -a /opt/morelogin/MoreLogin_x86_64_2.54.0.AppImage --no-sandbox
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF

Adjust WorkingDirectory and ExecStart paths to match where you placed the AppImage.

[!TIP] For production, consider creating a dedicated user (e.g., morelogin) instead of running as root, and adjust file ownership and permissions accordingly. If the AppImage currently requires root privileges, you may keep User=root, but isolating the process under a non-root account is a best practice.

Enable and Start

sudo systemctl daemon-reload
sudo systemctl enable morelogin    # Auto-start on boot
sudo systemctl start morelogin     # Start now

Manage the Service

sudo systemctl status morelogin    # Check status
sudo journalctl -u morelogin -f    # Stream logs
sudo systemctl restart morelogin   # Restart
sudo systemctl stop morelogin      # Stop

[!NOTE] After a restart, you must call the login endpoint again — the API session does not survive process restarts. For automated recovery, consider adding an ExecStartPost script or a health-check cron job that calls the login endpoint after the service starts.


Troubleshooting

MoreLogin fails to start

AppImages require FUSE to run.

Fix: Install FUSE support:

sudo apt install -y libfuse2t64

Browser pages show blank text / missing characters

Fix: Install the font package for the target language (see Step 1 — Install Fonts).

CDP connection refused from external machine

Cause: CDP debug ports bind to 127.0.0.1 by default.

Fix: Set up socat forwarding (see Step 4) and ensure firewall rules allow the forwarded port.

curl to Local API returns "Connection refused"

Cause: MoreLogin hasn't finished starting yet, or the process crashed.

Fix:

  1. Wait 5–10 seconds after startup
  2. Check morelogin.log for errors
  3. Verify the process is running: ps aux | grep MoreLogin

API returns 401 "login status has expired"

{"status": "error", "code": 401, "message": "Your login status has expired, please log in again"}

Cause: You haven't logged in via the API, or the MoreLogin process was restarted.

Fix: Call the login endpoint before any other API call (see Step 3).


Next Steps

GoalLink
Full Browser API referenceBrowser API
Authentication setupAuthentication Guide
Playwright / Selenium / Puppeteer examplesAutomation Examples
Complete Linux stress test scriptGitHub — linux_server_test.py
CLI quick startCLI Guide