This is the long-form walkthrough. For a quick pitch, see the README.
- What it is
- What you get at the end
- Quick setup (10 minutes, one-time) ← start here
- Prerequisites
- Install
- First-run setup
- Create an access token
- Verify the API works
- Use it — real examples
- MCP tools reference
- Deploy to a VPS
- Running multiple instances on one VPS
- Operating it
- Ports
- Upgrading
- Troubleshooting
- Security notes
- Limits and caveats
beeperbox runs Beeper Desktop inside a Docker container, headlessly. Beeper Desktop is the official cross-platform Beeper app, and it exposes a local HTTP API (Beeper's own "Developer mode" feature) for reading chats and sending messages across every bridge Beeper supports: WhatsApp, iMessage, Signal, Discord, Slack, Telegram, Facebook Messenger, Instagram, LinkedIn, and more.
Normally Beeper Desktop needs a real screen, keyboard, and a human pressing buttons. beeperbox wraps it in a virtual display (Xvfb), a window manager (openbox), and a browser-accessible VNC view (noVNC) so you can run it on a server, log in from anywhere via a web browser, and then use the API from any programming language that can speak HTTP.
beeperbox is built for one specific situation: autonomous agents that need messaging reach without a human at a Beeper Desktop.
Concretely:
- AI agents running on a VPS that need to reply to customer messages
- Cron jobs that fan out notifications to your phone across multiple messengers
- Multi-tenant SaaS where each customer needs their own Beeper account behind their own agent
- Headless servers that need to send alerts to humans on whichever messenger they prefer
- Anything in a container, on a Raspberry Pi, in CI, on a remote box where you cannot keep a Desktop GUI session running
If you are a laptop user with Beeper Desktop installed locally, you do not need beeperbox. Beeper already provides:
- A native HTTP API on
localhost:23373(the same one beeperbox exposes inside the container) - A built-in MCP server for AI agent runtimes like Claude Desktop and Claude Code
- A real GUI you can interact with directly
beeperbox is the same machinery, packaged for environments where running Beeper Desktop on the host is not an option.
It is not a bot framework, not an agent runtime, and not a general-purpose messaging gateway. It is the messaging substrate other software plugs into.
A single HTTP endpoint on your host:
http://localhost:23373
(Or :23374 if you set BEEPERBOX_HOST_PORT=23374 because a native Beeper Desktop on the same host already owns :23373 — see Ports below.)
That endpoint:
- Speaks the Beeper Desktop API — an OpenAPI 3.1 spec documenting ~20 operations (list chats, get messages, send message, search, contacts, reactions, reminders, assets)
- Requires a
Authorization: Bearer <token>header on all real operations (only/v1/infois public) - Covers every messaging network you have connected to your Beeper account
Anything that can make an HTTP request can use it. Your agent framework, your Python script, your cron job, a Zapier-like no-code tool, curl from a terminal — they all look the same to beeperbox.
Beeper Desktop syncs the top ~20 most recently active chats by default. If your beeperbox-driven agent doesn't see a chat that exists in your account, it's almost certainly because:
- The chat is older than the top-20 cutoff
- The chat is archived
- The chat hasn't received messages in long enough that Beeper deprioritized it from the live sync
Workaround: open Beeper Desktop in noVNC (http://localhost:6080/vnc.html), find the chat in the sidebar, and pin it. Pinned chats stay in the live sync regardless of activity. Or scroll to the chat once and Beeper will start syncing it.
For long-tail history (chats from years ago), Beeper has a separate search backend — use /v1/messages/search rather than /v1/chats to find them.
You don't have to go through the bridge-pairing flow inside the container. Bridge state lives on Beeper's servers, not on your device. If you already have a Beeper account configured on your phone (with WhatsApp, Signal, etc. all paired), all you need to do inside beeperbox is:
- Open noVNC
- Sign in with the same Beeper credentials your phone uses
- All your existing bridges show up automatically — no QR codes, no re-pairing
This means you can leave your phone as the "primary" Beeper client (where you do your normal pairing) and treat beeperbox as a read/write API replica. Both stay in sync because they're both talking to the same upstream Matrix homeserver.
This is the linear walkthrough from a clean machine to a working beeperbox + MCP server. Every step is mandatory, in order. After this, the rest of the guide is reference material you can dip into when you hit a specific question.
You will need: Docker installed, a Beeper account, a web browser, and ~10 minutes of attention.
git clone https://github.com/hamr0/beeperbox.git
cd beeperbox
docker compose up -dFirst build takes ~2 minutes. When it finishes, the container is running but Beeper Desktop inside it has no login yet.
http://localhost:6080/vnc.html
Click Connect. You should see a Linux desktop with Beeper Desktop starting up. If the window is grey for the first 30 seconds, that's normal — Electron is slow to start.
Inside the noVNC view, log in to Beeper Desktop with your Beeper account credentials (email code, etc.). When login completes, your chat list should appear.
If you already have Beeper set up on your phone, use the same credentials — all your existing bridges (WhatsApp, Signal, etc.) will inherit automatically. No re-pairing.
Inside Beeper Desktop: Settings → Developers
Toggle on:
- Enable Beeper Desktop API
- Start API on launch ← critical, otherwise you must repeat this step after every container restart
Still in Settings → Developers, scroll to Approved Connections and click + to create a new connection.
In the dialog:
- Name —
beeperbox-mcp(or anything memorable) - Permissions — Allow sensitive actions (the MCP server needs read + write)
- Expiry — Never (unless you have a token-rotation policy)
Click create. Beeper shows you a long random token string.
noVNC clipboard sharing is unreliable. The fastest workaround: inside Beeper Desktop, paste the token into your "Note to self" chat. Then on your host machine, open Beeper on your phone (or any other Beeper client) and copy the token from there.
On your host machine, in the beeperbox directory:
printf 'BEEPER_TOKEN=PASTE-TOKEN-HERE\n' > .env.env is in .gitignore, so it will not be committed accidentally.
docker compose up -dUse up -d, not restart. restart does not re-read environment variables. Only up -d recreates the container with the new env from your .env file.
docker compose logs beeperbox 2>&1 | grep "beeper token"You want to see:
[beeperbox-mcp] beeper token: set
If it says NOT SET, your .env file is in the wrong directory or has a typo. Re-check step 7.
This is the moment of truth. Call the list_inbox tool through the MCP HTTP transport from your host:
curl -s -X POST http://localhost:23375 \
-H 'Content-Type: application/json' \
--data-binary @- <<'EOF'
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_inbox","arguments":{"limit":3}}}
EOFYou should get back JSON with three of your most recently active chats, each carrying its network (whatsapp / telegram / discord / etc.), title, last_message_at, and unread_count. Note-to-self chats are filtered out automatically.
If you see real chats: you are done. beeperbox is running, the MCP server is reachable, and any AI agent runtime that speaks MCP can now consume it.
If you see an error, jump to Troubleshooting.
- Point an AI agent runtime at it: Claude Code, Cursor, Cline, bareagent — any MCP client that supports stdio or HTTP transport can consume beeperbox as a tool source. Configure it once and the LLM sees all 10 tools (
list_inbox,list_unread,read_chat,get_chat,search_messages,list_accounts,send_message,note_to_self,react_to_message,archive_chat). See the MCP tools reference section below. - Build something custom: hit the raw Beeper API on
http://localhost:23373/v1/*from any language with an HTTP client and yourBEEPER_TOKEN. See Use it — real examples for curl / Node / Python snippets. - Deploy to a VPS: same steps work on any Linux box with Docker. SSH-tunnel noVNC for the one-time login. See Deploy to a VPS.
- A Beeper account — sign up at beeper.com. Free tier connects 5 platforms. No affiliation; you bring your own account.
- Docker engine with the Compose plugin, or a compatible runtime (Podman with
podman-dockershim works). - ~2 GB free disk for the image, volume, and Beeper data.
- ~1 GB free RAM for the running container. A $5/month VPS has enough.
- Ports
6080and23373free on the host. If a native Beeper Desktop already runs on:23373, override withBEEPERBOX_HOST_PORT=23374— no compose edit needed (see Ports). - A web browser reachable to the host for the one-time login step.
git clone https://github.com/hamr0/beeperbox.git
cd beeperbox
docker compose up -d
docker compose logs -fFirst build takes ~2 minutes (downloading Debian, installing X server packages, downloading the Beeper AppImage). Subsequent builds are cached.
When you see lines like [ok] beeper api -> http://localhost:23373 in the logs, the container is ready for step-one of the human side. You can Ctrl-C out of logs -f at any time — the container keeps running.
This is the only part that needs a human at a browser. It happens once.
In any browser:
http://localhost:6080/vnc.html
Click Connect. You will see a Linux desktop with Beeper Desktop starting up.
If you are running on a VPS, replace localhost with the VPS's IP. If the VPS is public, see the security notes below first — do not expose 6080 to the open internet without protecting it.
Beeper Desktop will show its login screen. Log in with your Beeper account as you normally would — email code, or whatever method you use. When login completes, you should see your chat list.
Inside Beeper Desktop: Settings → Developers
Enable these two toggles:
- Enable Beeper Desktop API
- Start API on launch (crucial — otherwise you must repeat this step after every container restart)
All API operations except /v1/info require an Authorization: Bearer <token> header. The MCP server inside the container also needs this token to call the local Beeper API. You create the token once and forget it — it persists across container rebuilds and host reboots.
Still inside Beeper Desktop in noVNC: Settings → Developers → Approved Connections → +
In the dialog:
- Name — call it whatever helps you remember (e.g.
beeperbox-mcp) - Permissions — select Allow sensitive actions (the MCP server needs read + write to do anything useful)
- Expiry — pick Never unless you have a specific rotation policy
- Click create
Beeper will display the token string. It is a long random value — treat it like a password.
Read-only vs read-write: the Allow sensitive actions toggle gates write operations. With it on, the token can call every MCP tool including send_message, react_to_message, archive_chat, and note_to_self. With it off, the token is read-only — list_inbox, list_unread, read_chat, get_chat, search_messages, and list_accounts all still work, but write attempts return 401 Unauthorized. If you want a least-privilege agent that can observe but not reply, create a second token with "Allow sensitive actions" off and point that agent at it. You can have as many tokens as you want on one Beeper account; revoke them individually from the same Approved Connections panel.
Note: noVNC clipboard sharing is famously unreliable. If you cannot copy the token directly out of the noVNC view, the simplest workaround is to send the token to yourself in Note to self inside Beeper Desktop, then copy it from there. (Or set up real noVNC clipboard integration, but the workaround is faster.)
Once you have the token on your host machine, save it to a .env file next to docker-compose.yml:
cd ~/PycharmProjects/beeperbox
printf 'BEEPER_TOKEN=PASTE-TOKEN-HERE\n' > .env.env is in .gitignore, so it will not be committed accidentally. Now recreate the container so docker compose picks the new env var up:
docker compose up -d(up -d recreates the container if its env changed. restart does NOT pick up new env vars — you must use up -d.)
Verify the MCP server now sees the token:
docker compose logs beeperbox 2>&1 | grep "beeper token"You should see:
[beeperbox-mcp] beeper token: set
If it still says NOT SET, check that .env is in the same directory as docker-compose.yml and that the file has the literal text BEEPER_TOKEN=... with no quotes around the value.
This is a one-time setup. The token in Beeper persists until you revoke it from the same Approved Connections panel. The .env file persists on disk. Together they survive every kind of restart:
| Action | Token survives? |
|---|---|
docker compose restart |
yes |
docker compose down && docker compose up -d |
yes |
docker compose up -d --build (image rebuild) |
yes |
| Reboot the host | yes |
| Rebuild the host OS | no — recreate the .env file with the same token |
If you are building something other people will run — e.g. an installable agent, a multi-user app, a hosted SaaS — you want the OAuth2 Authorization Code flow with PKCE so each user can grant their own access without you ever seeing their token. The endpoints are discoverable at:
curl http://localhost:23373/v1/info | python3 -m json.toolSee endpoints.oauth in the response. This path is beyond the scope of this guide — see Beeper's own docs at developers.beeper.com.
Three calls. If all three succeed, you're done and everything else in this guide is just examples.
1. Public health probe (no token needed):
curl -s http://localhost:23373/v1/info | python3 -m json.toolExpected: JSON with app.name: "Beeper", server.status: "running".
2. Authenticated call — list accounts:
curl -s -H "Authorization: Bearer $BEEPER_TOKEN" \
http://localhost:23373/v1/accounts | python3 -m json.toolExpected: a JSON array of your connected Beeper accounts (one per bridge — WhatsApp, iMessage, etc).
3. List the 5 most recent chats:
curl -s -H "Authorization: Bearer $BEEPER_TOKEN" \
"http://localhost:23373/v1/chats?limit=5" | python3 -m json.toolExpected: a JSON array of five chats with titles, last-message timestamps, network IDs.
If 1 works but 2/3 return 401 Unauthorized, your token is wrong — go back and regenerate it.
If 1 returns a connection error, the container isn't running or the host port isn't mapped. See troubleshooting.
All examples assume BEEPER_TOKEN is set and beeperbox is on localhost:23373.
First find a chat ID:
curl -s -H "Authorization: Bearer $BEEPER_TOKEN" \
"http://localhost:23373/v1/chats?limit=1" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)[0]["id"])'Then send:
curl -s -X POST \
-H "Authorization: Bearer $BEEPER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text": "hello from beeperbox"}' \
"http://localhost:23373/v1/chats/<chatID>/messages"const TOKEN = process.env.BEEPER_TOKEN;
const BASE = 'http://localhost:23373/v1';
async function listChats(limit = 10) {
const r = await fetch(`${BASE}/chats?limit=${limit}`, {
headers: { Authorization: `Bearer ${TOKEN}` }
});
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
return r.json();
}
async function send(chatID, text) {
const r = await fetch(`${BASE}/chats/${chatID}/messages`, {
method: 'POST',
headers: {
Authorization: `Bearer ${TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ text })
});
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
return r.json();
}
const chats = await listChats(5);
console.log(chats.map(c => c.title));
await send(chats[0].id, 'hi from node');import json, os, urllib.request
TOKEN = os.environ['BEEPER_TOKEN']
BASE = 'http://localhost:23373/v1'
def request(method, path, body=None):
req = urllib.request.Request(
BASE + path,
method=method,
headers={'Authorization': f'Bearer {TOKEN}', 'Content-Type': 'application/json'},
data=json.dumps(body).encode() if body else None,
)
with urllib.request.urlopen(req) as r:
return json.loads(r.read())
chats = request('GET', '/chats?limit=5')
print([c['title'] for c in chats])
request('POST', f'/chats/{chats[0]["id"]}/messages', {'text': 'hi from python'})curl -s -G -H "Authorization: Bearer $BEEPER_TOKEN" \
--data-urlencode 'query=invoice' \
http://localhost:23373/v1/messages/search | python3 -m json.toolcurl -s http://localhost:23373/v1/spec \
| python3 -c 'import json,sys; [print(p) for p in sorted(json.load(sys.stdin)["paths"])]'As of Beeper Desktop 4.2.715, the endpoints are:
/v1/accounts
/v1/accounts/{accountID}/contacts
/v1/accounts/{accountID}/contacts/list
/v1/assets/download
/v1/assets/serve
/v1/assets/upload
/v1/assets/upload/base64
/v1/chats
/v1/chats/search
/v1/chats/{chatID}
/v1/chats/{chatID}/archive
/v1/chats/{chatID}/messages
/v1/chats/{chatID}/messages/{messageID}
/v1/chats/{chatID}/messages/{messageID}/reactions
/v1/chats/{chatID}/reminders
/v1/focus
/v1/info
/v1/messages/search
/v1/search
/v1/spec
beeperbox exposes 10 semantic tools over Model Context Protocol on two interchangeable transports. Any AI agent runtime that speaks MCP (Claude Code, Cursor, Cline, Continue, bareagent, etc.) can consume them.
| Tool | Required | Returns | Use case |
|---|---|---|---|
list_accounts |
— | Array of accounts with network slug + network_label |
Discover which platforms are reachable at session start |
list_inbox |
— | Array of Chat |
Triage: what's happening right now |
list_unread |
— | Array of Chat (unread only) |
"What needs my attention?" — primary inbox check |
get_chat |
chat_id |
Chat |
Refresh one chat's state before replying |
read_chat |
chat_id |
Array of Message (oldest first) |
Pull conversation context for the LLM to reason about |
search_messages |
query |
Array of Message |
Follow-up lookups, historical context, "what did X say about Y" |
send_message |
chat_id, text |
{chat_id, message_id, status} |
The headline reply/notify tool |
note_to_self |
text |
same | Agent self-notes, debug output, scheduled reminders — auto-resolves to the Beeper-native Note to self chat (won't leak into per-platform saved-messages chats) |
react_to_message |
chat_id, message_id, emoji |
{...status: reacted} |
Lightweight ack, no full reply needed |
archive_chat |
chat_id |
{chat_id, archived} |
Clean handled chats out of inbox (closest primitive to mark-as-read that Beeper exposes) |
Chat:
id stable chat identifier
title human-readable chat name
network machine slug ("whatsapp", "telegram", "discord", ...)
network_label human name ("WhatsApp", "Telegram", "Discord", ...)
is_group true if this is a multi-participant chat
is_note_to_self true if this is the user's own self chat (filtered from list_inbox)
last_message_at ISO 8601 timestamp of the most recent activity
unread_count integer
Message:
id stable message identifier
chat_id the chat this message belongs to (always present, no second lookup)
network machine slug (same as Chat)
network_label human name (same as Chat)
sender { id, name, is_self }
text message body (or "[MEDIA]" / "[non-text]" for non-text types)
type "TEXT" | "MEDIA" | ...
timestamp ISO 8601
reply_to parent message id if this is a reply, else null
All calls are JSON-RPC 2.0 POST to http://localhost:23375. The method is always tools/call. Three worked examples:
1. List your top 5 inbox chats
curl -s -X POST http://localhost:23375 \
-H 'Content-Type: application/json' \
--data-binary @- <<'EOF'
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_inbox","arguments":{"limit":5}}}
EOF2. Send a WhatsApp reply
curl -s -X POST http://localhost:23375 \
-H 'Content-Type: application/json' \
--data-binary @- <<'EOF'
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"send_message","arguments":{"chat_id":"!xxx:beeper.local","text":"on my way 👍"}}}
EOF3. Full-text search
curl -s -X POST http://localhost:23375 \
-H 'Content-Type: application/json' \
--data-binary @- <<'EOF'
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search_messages","arguments":{"query":"invoice","limit":10}}}
EOFStdio transport (Claude Code, Cursor, Cline, bareagent) — add to your MCP client config (e.g. ~/.claude/mcp.json for Claude Code):
{
"mcpServers": {
"beeperbox": {
"command": "docker",
"args": ["exec", "-i", "beeperbox", "node", "/opt/mcp/server.js", "--stdio"]
}
}
}The client spawns a fresh server process per session; stdio becomes the protocol channel and the MCP server inherits the container's BEEPER_TOKEN env automatically.
HTTP transport (remote agents, web, no-code tools) — point your client at http://localhost:23375 (or the appropriate host/IP if you've tunneled it). No configuration needed beyond the URL — the same server handles both transports out of one file.
# tools/list — see all registered tools with their schemas
curl -s -X POST http://localhost:23375 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":0,"method":"tools/list"}' \
| python3 -m json.tool
# any tool via stdio from the host
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
| docker exec -i beeperbox node /opt/mcp/server.js --stdioEverything above works identically on a VPS. Differences:
beeperbox idles around 500 MB and peaks around 700–900 MB when Matrix sync is busy. A $5/month Hetzner/RackNerd/DigitalOcean box with 1 GB RAM is enough for beeperbox plus one small agent alongside it. A 512 MB VPS is too tight.
# Debian/Ubuntu
curl -fsSL https://get.docker.com | sh
sudo systemctl enable --now docker
sudo usermod -aG docker $USER # log out/in after thisSame as the laptop install:
git clone https://github.com/hamr0/beeperbox.git
cd beeperbox
docker compose up -dYou need to reach noVNC from your laptop's browser to log in. Three options, from least to most secure:
A — SSH port-forward (recommended)
On your laptop:
ssh -L 6080:localhost:6080 -L 23373:localhost:23373 user@vps.example.comLeave this running. Open http://localhost:6080/vnc.html in your browser — it tunnels through SSH to the VPS. When setup is done, close the SSH tunnel. The API continues running on the VPS locally.
This is the right default: you never open extra ports on the VPS, and you only need the tunnel for the one-time setup.
B — Tailscale / Wireguard
Put the VPS on your private mesh, browse http://<tailscale-ip>:6080/vnc.html directly. Also good; slightly more setup.
C — Expose 6080 to the public internet
Do not do this naively. noVNC has no authentication by default, which means anyone who knows your IP can open Beeper Desktop, read your chats, and send messages. If you must expose it, put a reverse proxy with HTTP basic auth or a Cloudflare Access rule in front. And close the port again after setup.
If your agent runs on the same VPS, it uses http://localhost:23373. If your agent runs elsewhere, you need to either:
- SSH tunnel
23373to wherever the agent runs, or - Put the API behind a reverse proxy with TLS + auth (nginx, Caddy, Traefik — any of them work), or
- Put the agent and the VPS on the same private network (Tailscale, Wireguard).
The API has no TLS, no rate limiting, and no firewall of its own. Do not expose 23373 to the public internet. The Bearer token is the only access control, and it is a single shared secret you pasted in your code.
Caddyfile:
api.example.com {
reverse_proxy localhost:23373
basicauth {
beeperbox <bcrypt-hash>
}
}
Caddy handles TLS certificates via Let's Encrypt automatically. Basic auth adds a second layer in front of your Bearer token.
beeperbox publishes two host ports, both bound to 127.0.0.1 only:
| Default host port | Container port | Purpose | Override env var |
|---|---|---|---|
23373 |
23380 |
Beeper Desktop API (via socat forwarder) | BEEPERBOX_HOST_PORT |
6080 |
6080 |
noVNC web UI for first-run login | BEEPERBOX_NOVNC_PORT |
The defaults assume a clean host. If you're running on a dev machine that already has a native Beeper Desktop installed (which itself binds to :23373), override the API port:
BEEPERBOX_HOST_PORT=23374 docker compose up -dYou can put the override in a .env file next to docker-compose.yml to make it sticky:
BEEPERBOX_HOST_PORT=23374
The container's internal port (23380 after the socat forwarder) never changes regardless of what you override on the host side. Container internal ports live in their own network namespace and cannot conflict with anything on the host — only the host-side mapping is at risk of collision. Read the troubleshooting section on ports below if you're confused about why this works.
After starting the container, confirm which host port you actually got with:
docker port beeperboxbeeperbox is deliberately single-tenant — one container, one Beeper account, one set of bridges. That's because Beeper Desktop itself can only be logged in as one user at a time. If you need to serve multiple Beeper accounts (e.g. you're running beeperbox for two or three small businesses), the answer is spawn multiple beeperbox containers on the same host, one per account.
Each instance needs:
- A unique container name (
BEEPERBOX_CONTAINER_NAMEenv var) - A unique compose project name (
docker compose -p <project>) - A unique set of host ports (the three
BEEPERBOX_*_PORTenv overrides handle this) - A unique named volume for Beeper session persistence (the project prefix handles this automatically)
- Its own
.envfile with a differentBEEPER_TOKEN
Create one .env.<name> file per instance containing both the port overrides and that instance's Beeper token:
# .env.a
BEEPERBOX_CONTAINER_NAME=beeperbox-a
BEEPERBOX_HOST_PORT=23373
BEEPERBOX_NOVNC_PORT=6080
BEEPERBOX_MCP_PORT=23375
BEEPER_TOKEN=paste-customer-a-token-here
# .env.b
BEEPERBOX_CONTAINER_NAME=beeperbox-b
BEEPERBOX_HOST_PORT=23376
BEEPERBOX_NOVNC_PORT=6081
BEEPERBOX_MCP_PORT=23378
BEEPER_TOKEN=paste-customer-b-token-here
# .env.c
BEEPERBOX_CONTAINER_NAME=beeperbox-c
BEEPERBOX_HOST_PORT=23379
BEEPERBOX_NOVNC_PORT=6082
BEEPERBOX_MCP_PORT=23381
BEEPER_TOKEN=paste-customer-c-token-here
Then launch each instance with its own project prefix and env file:
docker compose -p beeperbox-a --env-file .env.a up -d
docker compose -p beeperbox-b --env-file .env.b up -d
docker compose -p beeperbox-c --env-file .env.c up -dDo the first-run login for each instance separately through its own noVNC port (6080 for A, 6081 for B, 6082 for C).
Rough per-instance footprint:
- Idle: ~500MB RAM, near-zero CPU
- Active (Matrix sync, message processing): ~800MB RAM, light CPU
- Image: ~1GB on disk, deduped across instances — only the first pull counts
- Volume: 50–300MB per instance depending on chat history volume
Density table for real VPSes:
| VPS | Cost | Instances |
|---|---|---|
| Oracle Cloud free tier (4 ARM cores, 24GB) | free | 20+ |
| Hetzner CAX21 (4 ARM vCPU, 8GB) | €5.39/mo | 6–8 |
| Hetzner CAX11 (2 ARM vCPU, 4GB) | €3.29/mo | 3–4 |
| DigitalOcean basic (2 vCPU, 2GB) | $12/mo | 2 |
| 1GB VPS | varies | 1 (tight) |
Real issues found while testing this pattern end-to-end:
docker compose up -don an existing container with new env vars does nothing useful. If you change ports orBEEPERBOX_CONTAINER_NAMEand re-runup -d, compose sees the existing container and keeps it — the new settings are ignored. You mustdocker compose -p <name> downfirst, thenup -d, to actually recreate with the new config.--env-fileis not the same as shell env vars.--env-file .env.asets the container's runtime environment (so Beeper seesBEEPER_TOKEN), but compose reads that same file for variable substitution only if no shell env vars override. The safest pattern is to put everything — both the port overrides and the token — in one per-instance.env.<n>file:Then:# .env.a BEEPERBOX_CONTAINER_NAME=beeperbox-a BEEPERBOX_HOST_PORT=23373 BEEPERBOX_NOVNC_PORT=6080 BEEPERBOX_MCP_PORT=23375 BEEPER_TOKEN=paste-token-for-account-adocker compose -p beeperbox-a --env-file .env.a up -d. No inline shell vars needed. Compose reads.env.afor both substitution and runtime env.- Do not reuse the same
BEEPER_TOKENacross instances. Tokens are tied to one Beeper account; sharing them will make multiple containers see the same chats. - First-run login per instance. Each new instance needs its own one-time Beeper login via its own noVNC port (6080 for A, 6081 for B, etc.). Bridge state lives on Beeper's servers, so if all instances use the same human's Beeper account, they inherit the same bridges — but each instance still needs to log in once to populate its local volume.
- For 2–3 instances, manual
docker compose -p <name>invocation is fine - For 5+, a small shell script that templates
.env.<n>+ starts the compose project from a customer list keeps things sane (~30 lines) - For 20+, use Docker Swarm or Kubernetes — at that scale you want real orchestration with health monitoring, automatic restarts, and rolling upgrades
- Never reuse the same
BEEPER_TOKENacross instances — tokens are tied to one account, and sharing them will cause each container to see the same chats instead of separate ones
If you were expecting beeperbox to accept a Bearer token per request and route to different Beeper accounts: that is architecturally impossible. Beeper Desktop is an Electron GUI with one logged-in user. You cannot have two different WhatsApp sessions, two different iMessage sessions, or two different anything inside one Beeper Desktop process. Multi-tenant via per-request tokens would require multi-Beeper-Desktop, which would require multi-Xvfb, multi-openbox, multi-noVNC, and multi-login — at which point you might as well just run multiple containers.
Run one container per account. The compose examples above do exactly that.
docker compose logs -f # follow live
docker compose logs --tail=100 # last 100 linesdocker compose restartdocker compose down # stop and remove the container (volume is kept)
docker compose up -d # start freshdocker ps --filter name=beeperbox --format 'table {{.Names}}\t{{.Status}}'You want to see Up X minutes (healthy). If you see (unhealthy), the API is not responding to /v1/info — see troubleshooting.
docker inspect beeperbox --format '{{json .State.Health}}' | python3 -m json.toolGives the full health log including the last 5 probe results.
beeperbox already has restart: unless-stopped in docker-compose.yml, so the container restarts itself if Docker is running. Make sure Docker itself starts at boot:
sudo systemctl enable dockerCombined with Beeper's Start API on launch setting (see first-run setup), the full chain is automatic: boot → Docker → beeperbox container → Beeper Desktop → API.
cd ~/beeperbox
git pull
docker compose up -d --buildThis rebuilds the image and recreates the container. The beeperbox_config volume is preserved, so you stay logged in to Beeper.
The Dockerfile downloads the latest Beeper Desktop stable AppImage at build time. To pick up a new Beeper version, rebuild the image without cache:
docker compose build --no-cache
docker compose up -dDocker isn't running. Start it:
sudo systemctl start dockerAdd yourself to the docker group so you don't need sudo:
sudo usermod -aG docker $USER
newgrp dockerSomething on your host is already bound to that port. The most common case is that you have a native Beeper Desktop installed on the same machine — its API also binds to :23373. Don't kill native Beeper; just give beeperbox a different host port via the env override (no compose edit needed):
BEEPERBOX_HOST_PORT=23374 docker compose up -dFor the noVNC port:
BEEPERBOX_NOVNC_PORT=16080 docker compose up -dYou can stack both:
BEEPERBOX_HOST_PORT=23374 BEEPERBOX_NOVNC_PORT=16080 docker compose up -dOr put them in a .env file next to docker-compose.yml to make them sticky.
To check which host ports the running container actually owns:
docker port beeperboxContainer probably didn't start. Check:
docker compose ps
docker compose logs --tail=50Look for Xvfb or openbox errors. If you see Electron sandbox errors, make sure --no-sandbox is still in the entrypoint.
Wait 30 seconds. Electron apps are slow to start in a container. If it stays grey past a minute, restart:
docker compose restartIf that still fails, check the container logs for [SDK] lines. No lines at all means Beeper Desktop never launched (usually a missing lib — report it as an issue).
Either the socat forwarder didn't start, or Beeper's API isn't up yet. Check:
docker exec beeperbox curl -sf http://127.0.0.1:23373/v1/info > /dev/null && echo API OK || echo API DOWN- If that prints
API OK: socat is the problem. Restart the container. - If it prints
API DOWN: Beeper API isn't running. You probably haven't enabled it yet — go back to first-run setup step 3, or you forgot to turn on Start API on launch.
Your token is missing or wrong. Confirm:
echo $BEEPER_TOKENIf empty, you didn't export it in the current shell. If set, re-create the token in Beeper Desktop settings and try again. Tokens do not rotate but they can be revoked from the same settings panel.
Beeper API is down or returning errors. Inspect the probe log:
docker inspect beeperbox --format '{{json .State.Health}}' | python3 -m json.toolLook at the Output fields of failed probes. Common cause: you haven't enabled the API at all yet, or you restarted the container without enabling Start API on launch.
Harmless. The Matrix SDK is trying to back up message receipts that it doesn't have local events for (usually after a fresh login while history is still catching up). Ignore it.
docker compose down -v # -v removes the volume too — you will lose your Beeper login
docker compose up -d --buildThe Beeper Desktop API is a single-user, local-trust surface. It was designed to be accessed from the same machine as Beeper Desktop by software you control. beeperbox does not change that — it just moves the "same machine" into a container.
Things you must do:
- Treat the Bearer token like a password. Do not commit it, do not paste it in chat, do not put it in a repo.
- Do not expose port 23373 (or whichever you've remapped it to) to the public internet. If you need remote access, use an SSH tunnel, Tailscale/Wireguard, or a reverse proxy with TLS + authentication.
- Do not expose port 6080 (noVNC) to the public internet without auth. noVNC has no built-in login. Anyone who can reach it can open Beeper Desktop and read your chats. Use the reverse proxy or SSH tunnel for login, and close it when done.
- The container runs as root. Standard for Docker development, fine on a personal VPS, not appropriate for shared hosting. If you need better isolation, run under Podman rootless or add a non-root user in the Dockerfile.
- Beeper's own ToS applies. You are using a real Beeper account. Automation that violates Beeper's or the underlying platforms' terms of service (spam, mass marketing, abuse) can and will get the account flagged or banned.
Things beeperbox deliberately does not do:
- Rate limiting
- Per-user permissions (there is one user — you)
- Audit logging (beyond whatever Beeper Desktop itself logs)
- Encryption at rest for the config volume
If you need those, put them in front of beeperbox (reverse proxy, governance middleware, volume encryption) — beeperbox is the messaging backend, not the security perimeter.
- Image size: ~1 GB. Electron + Chromium are the bulk. Do not expect this to shrink dramatically — musl-libc Alpine builds break Chromium, and stripping X server components breaks Electron.
- Idle RAM: ~500 MB. Not suitable for sub-512 MB VPS plans.
- Single user: one Beeper account per container. If you need multiple accounts, run multiple containers with different ports and volumes.
- Desktop API binds to the loopback interface only inside the container. beeperbox uses
socatto forward0.0.0.0:23380 → 127.0.0.1:23373so the API is reachable from the host. If Beeper ever adds a flag to bind0.0.0.0directly, socat will go away — it is a workaround, not a feature. - WhatsApp on-device bridge sometimes logs
no bridge event foundwarnings during backup. Harmless, ignore. - Multi-arch image:
linux/amd64andlinux/arm64are published from v0.3.0 onward — Raspberry Pi 4/5, Apple-silicon Docker Desktop, and Oracle Cloud's free ARM tier all pull the right variant automatically. - No streaming subscriptions in the API: the Beeper Desktop API is request/response. For realtime updates you poll
/v1/chatsor hook into the Beeper Desktop MCP server (advanced). - Pre-1.0. Current line is v0.4.x — the MCP tool surface, HTTP API, default ports, and
Chat/Messageschemas are usable but not declared stable. See CHANGELOG.md for the versioning policy and what each bump type guarantees. Running on a personal VPS for your own agents is fine; running as a shared service is not.
Questions, bugs, improvements: github.com/hamr0/beeperbox/issues.