Usage
Full Mullgate usage guide, organized from the original repository source.
This guide expands on the consumer-facing repository README. It keeps the deeper setup, platform, runtime, verifier, and troubleshooting detail out of the landing page while staying anchored to the CLI help contract exposed by:
mullgate --helpmullgate setup --helpmullgate start --helpmullgate status --helpmullgate doctor --helpmullgate config --help
Install forms
Mullgate has three truthful invocation forms, in order of preference:
- Installed
mullgatecommand — the published npm package installed vianpm,pnpm,bun, or the convenience installer scripts - Packed release asset — the GitHub release
.tgzartifact verified bypnpm verify:m003-install-path - Contributor/source-checkout path —
node dist/cli.js ...orpnpm exec tsx src/cli.ts ...
This guide uses installed mullgate ... commands by default. If you are working from a checkout instead, replace mullgate ... with either node dist/cli.js ... after pnpm build, or pnpm exec tsx src/cli.ts ... while developing.
Platform support posture
Mullgate reports platform support truthfully on Linux, macOS, and Windows.
| Platform | config path | status / doctor | Current runtime execution |
|---|---|---|---|
| Linux | Supported | Supported | Fully supported |
| macOS | Supported | Supported | Limited — Docker Desktop host networking does not match Linux |
| Windows | Supported | Supported | Limited — Docker Desktop host networking does not match Linux |
Use Linux for the full setup/runtime/probe workflow. On macOS and Windows, treat the CLI and runtime manifest as supported config and diagnostic surfaces, but use a Linux host or Linux VM when you need the shipped multi-route Docker runtime to behave truthfully end to end.
Non-interactive setup inputs
mullgate setup --non-interactive fails instead of prompting when required inputs are missing. You can supply values with flags, environment variables, or a mix of both.
Required for a normal non-interactive run
| Purpose | Flag | Environment variable |
|---|---|---|
| Mullvad account number | --account-number <digits> | MULLGATE_ACCOUNT_NUMBER |
| Proxy username | --username <name> | MULLGATE_PROXY_USERNAME |
| Proxy password | --password <secret> | MULLGATE_PROXY_PASSWORD |
| One or more routed locations | --location <alias> (repeatable) | MULLGATE_LOCATION or MULLGATE_LOCATIONS |
Common optional inputs
| Purpose | Flag | Environment variable |
|---|---|---|
| Device label | --device-name <name> | MULLGATE_DEVICE_NAME |
| Bind host / first route bind IP | --bind-host <host> | MULLGATE_BIND_HOST |
| Per-route bind IPs | --route-bind-ip <ip> (repeatable) | MULLGATE_ROUTE_BIND_IPS |
| Exposure mode | --exposure-mode <mode> | MULLGATE_EXPOSURE_MODE |
| Base domain | --base-domain <domain> | MULLGATE_EXPOSURE_BASE_DOMAIN |
| SOCKS5 port | --socks-port <port> | MULLGATE_SOCKS_PORT |
| HTTP port | --http-port <port> | MULLGATE_HTTP_PORT |
| HTTPS port | --https-port <port> | MULLGATE_HTTPS_PORT |
| HTTPS certificate path | --https-cert-path <path> | MULLGATE_HTTPS_CERT_PATH |
| HTTPS key path | --https-key-path <path> | MULLGATE_HTTPS_KEY_PATH |
| Mullvad provisioning endpoint | --mullvad-wg-url <url> | MULLGATE_MULLVAD_WG_URL |
| Mullvad relay metadata endpoint | --mullvad-relays-url <url> | MULLGATE_MULLVAD_RELAYS_URL |
Notes:
MULLGATE_LOCATIONis shorthand for route 1.MULLGATE_LOCATIONSis the ordered, comma-separated form for multi-route setup.MULLGATE_ROUTE_BIND_IPSis also ordered and comma-separated.- Non-loopback exposure requires one explicit bind IP per routed location.
- Multi-route non-loopback exposure requires distinct bind IPs.
Setup examples
Example: local two-route loopback setup
This is the easiest way to prove the CLI/runtime flow on one Linux machine:
export MULLGATE_ACCOUNT_NUMBER=123456789012
export MULLGATE_PROXY_USERNAME=alice
export MULLGATE_PROXY_PASSWORD='replace-me'
export MULLGATE_LOCATIONS=sweden-gothenburg,austria-vienna
export MULLGATE_DEVICE_NAME=mullgate-loopback
mullgate setup --non-interactive
mullgate config hostsWhat to expect:
- route 1 gets
127.0.0.1 - route 2 gets
127.0.0.2 - the configured hostnames default to the route aliases in loopback mode
- hostname access on the local machine works only after you install the emitted hosts block

Example: private-network hostnames with a base domain
Use this when clients should reach the proxies from your LAN, VPN, or overlay network and you want real hostnames per route:
export MULLGATE_ACCOUNT_NUMBER=123456789012
export MULLGATE_PROXY_USERNAME=alice
export MULLGATE_PROXY_PASSWORD='replace-me'
export MULLGATE_LOCATIONS=sweden-gothenburg,austria-vienna
export MULLGATE_ROUTE_BIND_IPS=192.168.10.10,192.168.10.11
export MULLGATE_EXPOSURE_MODE=private-network
export MULLGATE_EXPOSURE_BASE_DOMAIN=proxy.example.com
mullgate setup --non-interactive
mullgate config exposureWhat to expect:
- route hostnames become
sweden-gothenburg.proxy.example.comandaustria-vienna.proxy.example.com config exposureprints the DNS A records operators must publish- each hostname must resolve to its own bind IP before hostname-based routing proof can work remotely
Example: direct-IP exposure with no base domain
If you do not set a base domain in private-network or public mode, Mullgate falls back to the bind IPs as the hostnames clients should use:
mullgate config exposure \
--mode private-network \
--clear-base-domain \
--route-bind-ip 192.168.10.10 \
--route-bind-ip 192.168.10.11In that posture:
- there are no DNS records to publish
- direct bind-IP entrypoints are the intended access path
config hostsis still useful for local-only hostname testing, but it is no longer required for remote clients
Hostname and direct-IP access
Mullgate exposes the same per-route listeners in two parallel forms:
- hostname entrypoints — route-aware names such as
sweden-gothenburg,austria-vienna, orsweden-gothenburg.proxy.example.com - direct-IP entrypoints — the bind IP for each route, such as
127.0.0.1,127.0.0.2, or192.168.10.10
When hostname access works
Hostname-based access works only when the client resolves each configured hostname to the bind IP Mullgate assigned to that route.
That mapping can come from:
- local
/etc/hostsentries produced bymullgate config hosts - published DNS records when you use
--base-domain - some other trusted name-resolution system that preserves the same hostname-to-bind-IP mapping
Why the mapping matters for multi-route proof
Mullgate's routing layer dispatches by destination bind IP, not by a late application-level hint. That means:
- every remote route needs its own bind IP
- hostname proof only works when name resolution lands on the correct IP first
- if two hostnames resolve to the same bind IP, they collapse to the same route
- if a hostname resolves somewhere else,
mullgate doctorreports hostname drift and points you back tomullgate config hosts
Example curl forms
SOCKS5 with a hostname-mapped local route:
curl \
--proxy socks5h://sweden-gothenburg:1080 \
--proxy-user "$MULLGATE_PROXY_USERNAME:$MULLGATE_PROXY_PASSWORD" \
https://am.i.mullvad.net/jsonHTTP using a direct bind IP:
curl \
--proxy http://127.0.0.2:8080 \
--proxy-user "$MULLGATE_PROXY_USERNAME:$MULLGATE_PROXY_PASSWORD" \
https://am.i.mullvad.net/jsonIf HTTPS proxy support is configured with a cert, key, and HTTPS port, the same route inventory appears in config exposure, start, status, and runtime-manifest.json.
Editing exposure after setup
Use mullgate config exposure instead of editing JSON by hand.
Inspect the current posture
mullgate config exposureThe report includes:
- mode (
loopback,private-network, orpublic) - base domain and whether LAN access is expected
- per-route hostname and bind IP inventory
- DNS guidance when a base domain is set
- hostname and direct-IP listener URLs with credentials redacted
- whether the current runtime state needs a restart or refresh
Update the posture
Example: switch from loopback to private-network hostnames:
mullgate config exposure \
--mode private-network \
--base-domain proxy.example.com \
--route-bind-ip 192.168.10.10 \
--route-bind-ip 192.168.10.11Example: switch to direct-IP public exposure:
mullgate config exposure \
--mode public \
--clear-base-domain \
--route-bind-ip 203.0.113.10 \
--route-bind-ip 203.0.113.11After an exposure edit, Mullgate marks the runtime state as unvalidated. Do one of these before trusting the saved and runtime surfaces again:
mullgate config validate --refresh
# or
mullgate startRuntime inspection demo
The runtime status and diagnostics surfaces are easiest to read when you see them on a seeded healthy install:

Validation and runtime lifecycle
mullgate config validate
Use this when you want to refresh or verify derived runtime artifacts without starting Docker immediately.
mullgate config validate --help
mullgate config validate --refreshIt validates the rendered wireproxy config and persists validation metadata under the runtime directory.
mullgate start
Use mullgate start when you are ready to render the runtime and bring the local proxy environment up.
Operational guidance
In day-to-day use, the safest pattern is:
- configure through CLI commands
- inspect exposure and route inventory
- validate derived runtime state
- start the runtime
- verify route behavior with
status,doctor, and a real probe
Related guides
Inspectable files and what they mean
The fastest way to find the active XDG paths is:
mullgate config pathThat report also prints:
- the resolved platform id and whether it came from the real host or
MULLGATE_PLATFORM - the current platform support level (
fullon Linux,partialon macOS/Windows) - the runtime story and host-networking limitations for the current platform
- platform guidance and warnings that match the wording used by
status,doctor, andruntime-manifest.json
Important files:
| File | Meaning |
|---|---|
config.json | Canonical Mullgate config with routed locations, exposure settings, and persisted runtime status |
wireproxy.conf | Primary rendered wireproxy config path recorded in the canonical config |
wireproxy-configtest.json | Persisted config validation report |
docker-compose.yml | Rendered runtime bundle entrypoint for mullgate start |
runtime-manifest.json | Truthful route/endpoint manifest, including redacted published URLs and backend topology |
last-start.json | Secret-safe success/failure report for the most recent mullgate start attempt |
Integrated release verifier
Use the integrated Linux-first proof command when you want one end-to-end check for the assembled setup/runtime flow instead of running setup, start, status, doctor, and curl probes manually.
Required environment variables
| Purpose | Environment variable |
|---|---|
| Mullvad account number | MULLGATE_ACCOUNT_NUMBER |
| Proxy username | MULLGATE_PROXY_USERNAME |
| Proxy password | MULLGATE_PROXY_PASSWORD |
| Deterministic Mullvad device name | MULLGATE_DEVICE_NAME |
Optional verifier and setup inputs
| Purpose | Environment variable / flag |
|---|---|
Routed locations (default sweden-gothenburg,austria-vienna) | MULLGATE_LOCATIONS |
Direct-route check target (default 1.1.1.1) | MULLGATE_VERIFY_ROUTE_CHECK_IP / --route-check-ip |
Exit-check URL (default https://am.i.mullvad.net/json) | MULLGATE_VERIFY_TARGET_URL / --target-url |
HTTPS port (default 8443 when the verifier generates TLS assets) | MULLGATE_HTTPS_PORT |
| Existing HTTPS cert/key paths | MULLGATE_HTTPS_CERT_PATH, MULLGATE_HTTPS_KEY_PATH |
| Preserve the temp XDG home even on success | --keep-temp-home |
Command
export MULLGATE_ACCOUNT_NUMBER=123456789012
export MULLGATE_PROXY_USERNAME=alice
export MULLGATE_PROXY_PASSWORD='replace-me'
export MULLGATE_DEVICE_NAME=mullgate-s06-proof
pnpm verify:s06What the verifier proves
- non-interactive
mullgate setupagainst the real CLI mullgate config hostsoutput and saved hostname → bind IP mappingsmullgate start,mullgate status, andmullgate doctor- authenticated SOCKS5, HTTP, and HTTPS traffic through the published listeners
- direct host-route invariance before/after
mullgate start - distinct exits for the first two routed hostnames when they resolve locally to distinct bind IPs