---
title: "Networking"
space: "BenchPress Documentation"
url: "https://docs.benchpress.cloud/docs/reference/networking"
updated: "2026-08-29"
---

# Networking

Every address a bench answers on, and who can reach each one.

**Who this is for.** Somebody who cannot reach a bench, or who is changing how
one is addressed.

**Before you start.** A bench has up to four addresses at once. They are not
alternatives. Each one exists for a different caller, and three of the four are
private.

## The four addresses

`benchpress/addressing.py` builds all of them in one place, and the app renders
what it returns rather than assembling a URL from a port it declared itself.

| Address | Form | Reachable from |
|---|---|---|
| Public site | `https://<instance id>.<base domain>` | the internet, through Traefik |
| Public IDE | `https://ide-<instance id>.<base domain>` | the internet, through Traefik |
| Tunnel site | `http://<wg ip>:8000` | a device on the VPN |
| Tunnel IDE | `http://<wg ip>:8080/` | a device on the VPN |

The two ports are properties of the bench image, not settings. They are
constants in `addressing.py`.

| Port | Serves |
|---|---|
| 8000 | the Frappe site |
| 8080 | code-server, the browser IDE |

When a bench has no tunnel address the same helpers fall back to the container's
bridge address. When `base_domain` is empty or `localhost`, the two public
addresses do not exist and the helpers return nothing.

## Bench bridges

A bench container joins a BenchPress bridge network, never the control plane's
own network.

| Setting | Default | Means |
|---|---|---|
| `bench_subnet_base` | `10.20` | the first two octets of every bench bridge |
| `bench_bridge_count` | 16 | how many bridges may exist |
| `bench_slots_per_bridge` | 1000 | how many endpoints one bridge holds before the next is used |

Bridge `N` is derived, not configured:

| Property | Value for index `N` |
|---|---|
| Network name | `benchpress-N` |
| Bridge device | `bpbrN` |
| Subnet | `<base>.<N × 16>.0/20` |
| Gateway | `<base>.<N × 16>.1` |

The third octet steps by 16 so every `/20` starts on its own boundary. A later
scheme giving each node a `/16` renumbers nothing.

**Choosing a bridge is advisory.** `placement.pick_network` names the lowest
index with room. It takes no lock, and the count can go stale between the count
and the create. Docker is the arbiter. It refuses the create when the address
pool is empty, and the deploy rolls on to the next bridge.

A bridge is created when a bench actually reaches it. A `Draft` bench that is
never deployed costs no network.

Three infrastructure containers are attached to every bench bridge, so Docker's
embedded DNS answers in both directions: Traefik, the shared MariaDB and the
shared Redis. **The control plane's own database and Redis are deliberately
absent.** A tenant bridge that reaches them is a breach, not a convenience.

## The WireGuard plane

Every bench and every user device is a `VPN Peer` in the `vpn_management` app.
`benchpress/vpn_adapter.py` is the only seam between the two apps.

| Peer | Created by | Key |
|---|---|---|
| A bench | `create_container_peer`, at deploy step 5 | generated locally, private half written into the container and stored nowhere else |
| A device | `register_device`, from the Devices screen | supplied by the device, or generated by the server |

The insert claims the tunnel address atomically from the server's pool. Nothing
picks an address by hand.

A bench peer keeps a 25 second keepalive. A device peer is named
`[Type] Name`, which is how the adapter tells devices from bench containers in
one peer table.

Seven device types are accepted: Mobile, Laptop, Desktop, Tablet, Server, IoT
and Embedded. `get_device_types` returns the list so no screen hand-types it.

A bench created before the bridge families existed falls back to the legacy
gateway `172.30.0.1`.

## The public front door

`benchpress/ingress.py` owns every write to the Traefik route directory, and
that directory path lives in that module and nowhere else.

Traefik's file provider does not recurse, so the directory is flat. One file
holds one router for one bench, named by the instance id, which is always 32
hexadecimal characters.

Two files in that directory are never a bench's, and the convergence pass must
never delete them. They are named one by one rather than matched by shape.

| File | Owned by | Deleting it |
|---|---|---|
| `dynamic.yml` | the parent `benchpress_devops` repository | takes the control plane off the internet |
| `wildcard-anchor.yml` | `ingress.ensure_anchor` | takes every bench certificate with it |
| `<instance id>.yml` | one bench | removes that bench's public address |

**A route file names the container, never an address.** The container name is
the instance id, so no lifecycle transition can make the file stale.

### Why bench routers name no certificate resolver

A bench router is written with `tls: {}`. That turns TLS on and names no
resolver, so the router serves whatever certificate the store already holds for
the requested name.

This is the point of the design. Let's Encrypt allows five certificates for each
identifier set every seven days. A router that asked for its own certificate
would cap bench churn at five a week.

`ensure_anchor` is what puts `*.<base domain>` in the store, and it is the only
place in this app that names a resolver. The anchor is written at deploy step 1,
minutes ahead of the image build, because DNS-01 has to finish issuing before a
bench route goes live.

The anchor is never deleted at teardown. It has to outlive every bench, because
it is what keeps the certificate renewing.

The IDE hostname is one label under the same base domain, so the same wildcard
covers it. Only the site hostname is checked when a deploy logs the certificate
state.

## Who can reach what

| Caller | Public site | Tunnel site | SSH | Control plane |
|---|---|---|---|---|
| The internet | yes, if `base_domain` is set | no | no | yes |
| A device on the VPN | yes | yes | yes | yes |
| Another bench on the same bridge | no | no | no | no |
| A bench, reaching the shared MariaDB | — | — | — | the database only |

No bench is on the public internet by port. The only public path is Traefik, and
it terminates TLS on the wildcard.

## Verify

Read what a bench answers on:

```bash
bench --site <site> execute frappe.client.get_value \
  --kwargs "{'doctype':'Bench Instance','filters':{'name':'<id>'},'fieldname':['public_url','code_server_url','wg_ip','container_ip','bridge_network']}"
```

List the bridges Docker actually has:

```bash
docker network ls --filter name=benchpress-
```

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| The public address returns 404 | The bench has no route file | The `*/5` reconcile pass rewrites it |
| The public address returns a TLS error | The wildcard is not in the store | Check `wildcard-anchor.yml` and the DNS-01 credentials |
| No public address at all | `base_domain` is empty or `localhost` | Set it. See [Settings reference](docs/operator/settings-reference) |
| The tunnel address does not answer | The device is not on the VPN | [Connect over SSH and the VPN](docs/user/connect-ssh-vpn) |
| A deploy fails at step 5 | The worker has no WireGuard agent socket | Run deploys on `queue-long` |
| A deploy fails to create a container | Every bridge is full | Raise `bench_bridge_count` |
| A bench reaches the control plane database | A bridge was attached by hand | Remove it. Only three infrastructure containers belong |
| Every bench lost its certificate | The anchor file was deleted | Redeploy any bench, or call `ensure_anchor` |

## Reference

| Fact | Value |
|---|---|
| Site port | 8000 |
| IDE port | 8080 |
| Default subnet base | `10.20` |
| Bridges | 16 |
| Slots for each bridge | 1000 |
| Subnet for bridge `N` | `<base>.<N × 16>.0/20` |
| Bridge device | `bpbrN` |
| Network name | `benchpress-N` |
| Legacy gateway | `172.30.0.1` |
| Container keepalive | 25 seconds |
| Device types | 7 |
| Route directory | flat, one file for each bench |
| Protected route files | `dynamic.yml`, `wildcard-anchor.yml` |

## Related

- [WireGuard and the VPN plane](docs/operator/wireguard-setup) — standing the tunnel up.
- [Deploy pipeline](docs/reference/deploy-pipeline) — the steps that produce these addresses.
- [Open your site](docs/user/open-your-site) — the same addresses, from a user's side.
- [Data model](docs/reference/data-model#devices-are-vpn-peers) — why there is no Device DocType.
