# Linux Server Deployment

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

| Requirement | Details |
|  --- | --- |
| **Operating System** | Ubuntu 24.04 Server (x86_64) |
| **Recommended Specs** | 8 vCPU, 8 GB RAM (supports ~5 concurrent profiles) |
| **Network** | Outbound 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:


```bash
# 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:


```bash
# 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](https://fonts.google.com/noto) package.


## Step 2 — Download & Launch MoreLogin

### 2.1 Download the AppImage


```bash
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](https://www.morelogin.com/download/).


### 2.2 Start MoreLogin in the Background

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


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

Verify it started successfully:


```bash
# 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:

```json
{"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](/assets/image.cfac9a4c31ae47e8434739e42cab4601ee08d737bba4fccba40f2f3bc78e9648.4b463470.png)

### 3.2 Log In via curl

Call the login endpoint with your credentials:


```bash
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:


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

### 3.3 Verify the Login

Confirm the API is ready by listing browser profiles:


```bash
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](#step-4--configure-network-forwarding-socat) for remote access, or jump directly to [Step 5](#step-5--quick-automation-example) 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


```bash
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:


```bash
# 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](#52-create--start--automate--cleanup) 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:


```bash
# 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
```

### 4.4 Recommended: Use SSH Tunnel Instead

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:**

```bash
# 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:**

```bash
# 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.:

```bash
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](https://github.com/MoreLoginBrowser/MoreLogin-API-Demos/blob/main/MoreLogin-Python/linux_server_test.py).

### 5.1 Install Python Dependencies


```bash
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.


```python
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](#44-recommended-use-ssh-tunnel-instead)), 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:

```bash
# 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](#43-open-firewall-ports).


> [!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](/api-reference/browser) 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](https://github.com/MoreLoginBrowser/MoreLogin-API-Demos/blob/main/MoreLogin-Python/linux_server_test.py):

| Metric | Value |
|  --- | --- |
| **Total runs** | 100 |
| **Concurrency** | 4 (simultaneous) |
| **Success rate** | 100.0% |
| **Total time** | 604.06 s |
| **Avg time per task** | 6.04 s |
| **Throughput** | 0.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


```bash
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


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

### Manage the Service


```bash
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](#32-log-in-via-curl) 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:


```bash
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](#install-fonts-optional)).

### 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](#step-4--configure-network-forwarding-socat)) 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"


```json
{"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](#step-3--log-in-to-morelogin-via-api-required)).

## Next Steps

| Goal | Link |
|  --- | --- |
| Full Browser API reference | [Browser API](/api-reference/browser) |
| Authentication setup | [Authentication Guide](/api-reference/getting-started/authentication) |
| Playwright / Selenium / Puppeteer examples | [Automation Examples](/api-reference/examples) |
| Complete Linux stress test script | [GitHub — linux_server_test.py](https://github.com/MoreLoginBrowser/MoreLogin-API-Demos/blob/main/MoreLogin-Python/linux_server_test.py) |
| CLI quick start | [CLI Guide](/cli/quick-start) |