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 proxy start --helpmullgate proxy logs --helpmullgate proxy status --helpmullgate proxy doctor --helpmullgate proxy relay --helpmullgate proxy relay recommend --helpmullgate config --helpmullgate version --helpmullgate completions --help
Install forms
Mullgate has two operator-facing install forms:
- Installed
mullgatecommand — the published npm package installed vianpm,pnpm,bun, or the convenience installer scripts - Packed release asset — the GitHub release
.tgzartifact or extracted standalone binary
This guide uses installed mullgate ... commands by default. If you are using a release artifact instead, invoke the extracted mullgate binary with the same flags and subcommands shown here.
Platform support posture
Mullgate reports platform support truthfully on Linux, macOS, and Windows.
| Platform | 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 |
| 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 / shared private-network host IP | --bind-host <host> | MULLGATE_BIND_HOST |
| Shared private-network host IP or public 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 |
| Proxy password | --password <secret> | MULLGATE_PROXY_PASSWORD |
| 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.- If you omit
MULLGATE_PROXY_PASSWORD, setup saves an empty password. private-networkuses one shared trusted-network host IP. If Tailscale is available during setup and you do not override it, Mullgate should default to the host's100.xaddress.publicplus the defaultpublished-routesaccess mode still requires one explicit bind IP per routed location, and multi-route public exposure still requires distinct bind IPs.inline-selectoruses one shared host IP in every exposure mode because route selection moves to the proxy username.
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 proxy accessWhat 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
- direct bind-IP entrypoints work immediately and are the canonical local access path
- 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_EXPOSURE_MODE=private-network
export MULLGATE_BIND_HOST=100.124.44.113
export MULLGATE_EXPOSURE_BASE_DOMAIN=proxy.example.com
mullgate setup --non-interactive
mullgate proxy accessWhat to expect:
- route hostnames become
sweden-gothenburg.proxy.example.comandaustria-vienna.proxy.example.com - both hostnames resolve to the same shared private-network host IP
- route 1 keeps the base ports, route 2 moves to the next ports (
1081,8081,8444when HTTPS is enabled) mullgate proxy accessprints the DNS A records operators must publish- each hostname must resolve to that shared host 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 proxy access \
--mode private-network \
--clear-base-domain \
--route-bind-ip 100.124.44.113In that posture:
- there are no DNS records to publish
- direct host-IP entrypoints are the intended access path
mullgate proxy accessis still useful for local-only hostname testing, but it is no longer required for remote clients
Example: private-network inline-selector access
Use this when trusted-network clients should connect to one shared host and select the Mullvad exit inline in the proxy username:
export MULLGATE_ACCOUNT_NUMBER=123456789012
export MULLGATE_PROXY_USERNAME=alice
export MULLGATE_PROXY_PASSWORD=''
export MULLGATE_LOCATIONS=sweden-gothenburg,austria-vienna
export MULLGATE_EXPOSURE_MODE=private-network
export MULLGATE_BIND_HOST=100.124.44.113
mullgate setup --non-interactive
mullgate proxy access --access-mode inline-selectorWhat to expect:
- one shared SOCKS5 listener and one shared HTTP listener on the trusted-network host
- selector examples such as
socks5://se:@100.124.44.113:1080andsocks5://se-got:@100.124.44.113:1080 - exact relay selectors when the user wants a specific server, for example
socks5://se-got-wg-101:@100.124.44.113:1080 mullgate proxy exportdoes not emit route URLs ininline-selectormode
Use the guaranteed scheme://selector:@host:port form. The shorter scheme://selector@host:port form is best-effort only because some clients treat a missing password inconsistently.
For the supported selector families and patterns, see Inline Selector Reference.
Unsupported local config versions
The current CLI only operates on the current v2 config/runtime shape.
If an installed mullgate command reports an unsupported config version:
- treat the reported config/runtime paths as stale local state
- back up or remove the config file and runtime directory named in the error
- rerun
mullgate setup - rerun
mullgate proxy start
Do not expect the current CLI to keep operating on an older runtime bundle in place.
Runtime control and support utilities
Use these commands after setup when you need to preflight, stop, restart, inspect, or hand support metadata to someone else.
Dry-run the rendered runtime bundle
mullgate proxy start --dry-runUse this before touching Docker when you want Mullgate to rerender and validate the runtime artifacts but stop short of launching containers.
Stop or restart the saved bundle
mullgate proxy stop
mullgate proxy restartproxy stopis the canonical clean shutdown for the saved runtime bundleproxy restartis the canonical "stop, rerender, and start again" shortcut after a config or relay change
Read the saved Docker Compose logs
mullgate proxy logs --tail 200
mullgate proxy logs --tail 50 --followUse mullgate proxy logs right after mullgate proxy status when a runtime looks unhealthy and you want evidence from the saved bundle instead of guessing.
Print support metadata or install shell completions
mullgate version
mullgate completions bash > ~/.local/share/bash-completion/completions/mullgatemullgate versionprints the CLI version, config schema, Node version, platform, architecture, and hostname for support reportsmullgate completions <bash|zsh|fish>prints shell completion scripts to stdout so you can install them in the shell-specific location you already use
Exporting proxy lists for clients
Use mullgate proxy export when you want a ready-to-paste inventory of authenticated route URLs instead of assembling client config by hand.
mullgate proxy export currently supports published-routes mode only. If the saved access mode is inline-selector, use mullgate proxy access to inspect the shared listener and selector examples instead.
Discover the curated region groups
mullgate proxy export --regionsThis prints the built-in groups accepted by --region, such as europe and asia-pacific.
Guided proxies.txt export
mullgate proxy export --guidedThe guided flow defaults to writing proxies.txt, only prompts for values you did not already pass on the CLI, and works both on a real terminal and with piped stdin.
It now starts from actual country and region pick-lists, then lets you refine each selected batch with optional city, server, and provider filters.
Deterministic selector batches
mullgate proxy export \
--country se --city got --count 1 \
--region europe --provider m247 --count 2 \
--output proxies.txtRules worth knowing:
- selectors are consumed in CLI order
--countapplies to the immediately preceding--countryor--region--city,--server, and repeated--providervalues refine only the immediately preceding selector batch- each matched route is exported at most once, so later selectors only see routes that earlier selectors did not already consume
- auto-generated filenames reflect the protocol plus selector batch order when you do not pass
--output
Preview or stream the export
mullgate proxy export --dry-run --protocol http --country us --server us-nyc-wg-001 --owner rented
mullgate proxy export --stdout --protocol socks5 --region europe--dry-runprints the real proxy URLs it would export and does not write a file--stdoutwrites the real proxy URLs to stdout and prints the export summary on stderr--forceis required when you want to overwrite an existing export file
Relay discovery, recommendation, and exit verification
Use these commands when you want to move from broad selector intent to exact relay choices you can justify and verify.

Find relays that match a policy
mullgate proxy relay list \
--country Sweden \
--owner mullvad \
--run-mode ram \
--min-port-speed 9000Use cases:
- prefer Mullvad-owned infrastructure over rented servers
- filter down to RAM-disk relays for a stricter operational posture
- exclude slower relays before you probe anything
Probe likely candidates before pinning one
mullgate proxy relay probe --country Sweden --count 2What this does:
- starts from the selector you requested
- picks a spread of likely candidates
- runs
pingagainst those relay IPs - ranks the successes by latency
Preview or apply exact relay recommendations
mullgate proxy relay recommend --country Sweden --count 1
mullgate proxy relay recommend --country Sweden --count 1 --applyUse mullgate proxy relay recommend when you want Mullgate to translate a broad country or region request into exact relay hostnames.
- without
--apply, it stays advisory and prints the exact route it would use - with
--apply, it pins the recommended relay hostname into saved config and refreshes the derived runtime artifacts - ordered selectors still matter, so you can mix country and region batches and keep deterministic intent
Verify a configured route really exits through Mullvad
mullgate proxy relay verify --route sweden-gothenburgThis verifies the configured route across the published proxy protocols and checks the JSON response from https://am.i.mullvad.net/json by default.
Use it when:
- a recommended relay has been applied and you want a quick truth check
- an operator needs to prove that SOCKS5, HTTP, and HTTPS surfaces all still exit through Mullvad
- a route looks suspicious and you want a concrete per-protocol failure report instead of guessing
Hostname and direct-IP access
Mullgate exposes proxy entrypoints in two access models:
- published-routes — route-aware hostnames or per-route ports such as
sweden-gothenburg.proxy.example.com:1080or100.124.44.113:1081 - inline-selector — one shared listener per protocol, with the selector in the username such as
socks5://se:@100.124.44.113:1080
For the full selector syntax and supported selector families, see Inline Selector Reference.
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 proxy access - published DNS records when you use
--base-domain - some other trusted name-resolution system that preserves the same hostname-to-bind-IP mapping
In loopback mode without a base domain, those hostname mappings are optional. Direct bind-IP entrypoints remain the canonical local path even when you skip the hosts block.
Why the mapping matters for multi-route proof
Mullgate's routing layer uses the access mode you chose:
- in
published-routes + loopback, route selection still happens by destination bind IP - in
published-routes + private-network, routes can share one host IP because each route gets its own published port - in
published-routes + public, each route still needs its own bind IP - in
inline-selector, routes share one listener and the username selector chooses the backend
Hostname proof still depends on correct name resolution. If the configured hostname points at the wrong host IP, mullgate proxy doctor reports hostname drift and points you back to mullgate proxy access.
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 mullgate proxy access, start, status, and runtime-manifest.json.
Editing access and exposure after setup
Use mullgate proxy access instead of editing JSON by hand.
Inspect the current posture
mullgate proxy accessThe report includes:
- mode (
loopback,private-network, orpublic) - access mode (
published-routesorinline-selector) - base domain and whether LAN access is expected
- the shared host or per-route hostname and bind IP inventory
- DNS guidance when a base domain is set
- hostname and direct-IP listener URLs with full credentials in
published-routes - selector syntax and selector examples in
inline-selector - whether the current runtime state needs a restart or refresh
Update the posture
Example: switch from loopback to private-network hostnames:
mullgate proxy access \
--mode private-network \
--base-domain proxy.example.com \
--route-bind-ip 100.124.44.113Example: switch to direct-IP public exposure:
mullgate proxy access \
--mode public \
--clear-base-domain \
--route-bind-ip 203.0.113.10 \
--route-bind-ip 203.0.113.11Example: switch the same private-network host to selector-driven access:
mullgate proxy access \
--mode private-network \
--access-mode inline-selector \
--route-bind-ip 100.124.44.113If you intentionally expose inline-selector on a public host with an empty password, add:
--unsafe-public-empty-passwordWithout that explicit override, Mullgate blocks public + inline-selector + empty password.
After an exposure edit, Mullgate marks the runtime state as unvalidated. Do one of these before trusting the saved and runtime surfaces again:
mullgate proxy validate --refresh
# or
mullgate proxy 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 proxy validate
Use this when you want to refresh or verify derived runtime artifacts without starting Docker immediately.
mullgate proxy validate --help
mullgate proxy validate --refreshIt validates the rendered wireproxy config and persists validation metadata under the runtime directory.
mullgate proxy start
Use mullgate proxy 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 proxy start |
runtime-manifest.json | Truthful route/endpoint manifest, including authenticated published URLs and backend topology |
last-start.json | Secret-safe success/failure report for the most recent mullgate proxy 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 |
| Reuse a preserved temp XDG home and skip provisioning a new Mullvad device | --reuse-temp-home <path> |
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 proxy accessoutput and saved hostname → bind IP mappingsmullgate proxy start,mullgate proxy status, andmullgate proxy doctor- authenticated SOCKS5, HTTP, and HTTPS traffic through the published listeners
- direct host-route invariance before/after
mullgate proxy start - distinct exits for the first two routed hostnames when they resolve locally to distinct bind IPs
Shared-device prerequisite
A fresh verifier run needs one free Mullvad WireGuard device slot total because setup provisions one shared Mullvad entry device for the whole proof, not one device per route. If you rerun against a preserved home with --reuse-temp-home <path>, the verifier reuses that saved shared device and does not consume another slot.
Scaling note
The built-in pnpm verify:s06 path still keeps its end-to-end probe contract to the first two saved routes so the default proof stays fast and inspectable.
That does not mean Mullgate consumes one Mullvad slot per route. The current shared-entry runtime still uses one shared Mullvad device for the saved route set.