ExpressVPN in a box
100K+
Container based on polkaned/expressvpn with additional features and automation.
expressvpnctl) with headless activation.CODE and background mode enable./metrics or custom .cgi path.debian:trixie-slim (amd64) with updated system packages.docker run \
--env=CODE=code \
--env=SERVER=smart \
--cap-add=NET_ADMIN \
--device=/dev/net/tun \
--privileged \
--detach=true \
--tty=true \
--name=expressvpn \
--publish 1080:1080 \
--publish 8000:8000 \
--publish 9797:9797 \
--env=PROTOCOL=lightwayudp \
--env=ALLOW_LAN=true \
--env=LAN_CIDR=192.168.55.0/24 \
--env=METRICS_PROMETHEUS=on \
--env=CONTROL_SERVER=on \
--env=SOCKS=off \
misioslav/expressvpn \
/bin/bash
Another container using the VPN network:
docker run \
--name=example \
--net=container:expressvpn \
maintainer/example:version
services:
example:
image: maintainer/example:version
container_name: example
network_mode: service:expressvpn
depends_on:
expressvpn:
condition: service_healthy
expressvpn:
image: misioslav/expressvpn:latest
container_name: expressvpn
restart: unless-stopped
ports:
- 1080:1080 # socks5 (optional)
- 8000:8000 # control server (optional)
- 9797:9797 # metrics (optional)
environment:
- CODE=code
- SERVER=smart
- PROTOCOL=lightwayudp
- ALLOW_LAN=true
- LAN_CIDR=192.168.55.0/24
- METRICS_PROMETHEUS=on
- CONTROL_SERVER=on
- SOCKS=off
# Optional healthcheck/IP validation
# - DDNS=yourDdnsDomain
# - IP=yourStaticIp
# - BEARER=ipInfoAccessToken
# - HEALTHCHECK=healthchecks.ioId
# Optional DNS whitelist
# - WHITELIST_DNS=192.168.1.1,1.1.1.1
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun
stdin_open: true
tty: true
command: /bin/bash
privileged: true
You can choose to which location ExpressVPN should connect to by setting up SERVER=ALIAS, SERVER=COUNTRY, SERVER=LOCATION or SERVER=SMART
You can check available locations from inside the container by running expressvpn list all command.
Environment variables (defaults shown):
| ENV | Description | Default |
|---|---|---|
| CODE | ExpressVPN activation code | code |
| SERVER | Region name or smart | smart |
| PROTOCOL | VPN protocol | lightwayudp |
| NETWORK | Network Lock (on/off) | on |
| ALLOW_LAN | Allow LAN access while Network Lock is on | true |
| LAN_CIDR | Comma-separated LAN CIDRs for return routes | (empty) |
| WHITELIST_DNS | Comma-separated DNS servers to allow via iptables | (empty) |
| DDNS | Domain to compare with ExpressVPN public IP for healthcheck | (empty) |
| IP | Static IP to compare with ExpressVPN public IP for healthcheck | (empty) |
| BEARER | ipinfo.io bearer token (healthcheck and /v1/ip) | (empty) |
| HEALTHCHECK | healthchecks.io UUID | (empty) |
| METRICS_PROMETHEUS | Enable metrics exporter (on/off) | off |
| METRICS_PORT | Metrics port | 9797 |
| METRICS_PATH | Metrics path (absolute, ends with .cgi) | /metrics.cgi |
| CONTROL_SERVER | Enable control server (on/off) | off |
| CONTROL_IP | Control server bind IP | 0.0.0.0 |
| CONTROL_PORT | Control server port | 8000 |
| AUTH_CONFIG | Auth config file path | /expressvpn/config.toml |
| CLOUDFLARE_SPEED_TIMEOUT | Speed test timeout in seconds (control server) | 120 |
| SOCKS | Enable SOCKS5 proxy (on/off) | off |
| SOCKS_IP | SOCKS bind IP | 0.0.0.0 |
| SOCKS_PORT | SOCKS port | 1080 |
| SOCKS_USER | SOCKS username | (empty) |
| SOCKS_PASS | SOCKS password | (empty) |
| SOCKS_WHITELIST | Comma-separated IPs bypassing auth | (empty) |
| SOCKS_AUTH_ONCE | Cache auth by IP (true/false) | false |
| SOCKS_LOGS | Enable microsocks logs (true/false) | true |
Supported values for PROTOCOL:
NETWORK=on).ALLOW_LAN=true lets LAN traffic through while Network Lock is on.LAN_CIDR (comma-separated) to add return routes for your LAN subnets.Enable the SOCKS5 proxy with:
SOCKS=onSOCKS_USER and SOCKS_PASSSOCKS_WHITELIST=ip1,ip2SOCKS_AUTH_ONCE=trueEnable the control API with:
CONTROL_SERVER=onCONTROL_IP=0.0.0.0CONTROL_PORT=8000| Method | Path | Description |
|---|---|---|
| GET | /v1/status | Connection status, server, IP |
| GET | /v1/vpn/status | VPN status (running/stopped) |
| GET | /v1/vpn/settings | VPN settings (protocol, region, allow_lan) |
| POST | /v1/vpn/settings | Update VPN settings (protocol, region, allow_lan) |
| GET | /v1/servers | List available regions |
| GET | /v1/ip | Public IP info (uses BEARER if set) |
| GET | /v1/publicip/ip | Public IP ({"public_ip":"x.x.x.x"}) |
| GET | /v1/dns | Resolver info and /etc/resolv.conf |
| GET | /v1/dns/status | DNS status (running/stopped) |
| GET | /v1/dnsleak | DNS leak test result |
| GET | /v1/speedtest | Cloudflare speed test (cloudflare-speed-cli --json) |
| GET | /v1/health | API health check |
| POST | /v1/connect | Connect to server (JSON: { "server": "smart" }) |
| POST | /v1/disconnect | Disconnect from VPN |
The speed test endpoint uses cloudflare-speed-cli by kavehtehrani:
https://github.com/kavehtehrani/cloudflare-speed-cli
The DNS leak test script is using: https://github.com/macvk/dnsleaktest
Mount a TOML file at /expressvpn/config.toml (or change AUTH_CONFIG). Example file: example/config.toml.example.
[[roles]]
name = "admin"
routes = ["GET /v1/status", "GET /v1/servers", "GET /v1/dns", "GET /v1/ip", "GET /v1/dnsleak", "GET /v1/speedtest", "POST /v1/connect", "POST /v1/disconnect", "GET /v1/health"]
auth = "basic"
username = "admin"
password = "changeme"
[[roles]]
name = "api_user"
routes = ["GET /v1/status", "POST /v1/connect", "POST /v1/disconnect"]
auth = "apikey"
apikey = "your-secret-api-key"
If the config file is missing, a single role can be defined via environment variables:
CONTROL_AUTH_TYPE (basic, api_key, or none)CONTROL_AUTH_NAMECONTROL_AUTH_USERCONTROL_AUTH_PASSWORDCONTROL_API_KEYCONTROL_AUTH_ROUTES (comma-separated METHOD /path, default *)Example request:
curl -u admin:changeme http://localhost:8000/v1/status
For API key auth, send the key with:
curl -H "X-API-Key: your-secret-api-key" http://localhost:8000/v1/status
Update VPN settings:
curl -X POST http://localhost:8000/v1/vpn/settings \
-H "Content-Type: application/json" \
-u admin:changeme \
-d '{"protocol":"lightwayudp","region":"smart","allow_lan":true}'
Connect to a region:
curl -X POST http://localhost:8000/v1/connect \
-H "Content-Type: application/json" \
-u admin:changeme \
-d '{"server":"germany-frankfurt-1"}'
Disconnect:
curl -X POST http://localhost:8000/v1/disconnect \
-u admin:changeme
Enable metrics with:
METRICS_PROMETHEUS=onMETRICS_PORT=9797METRICS_PATH=/metrics.cgi (absolute and must end with .cgi)Metrics are served on /metrics.cgi (or /metrics) at the configured port.
If the embedded httpd cannot bind the port, a socat fallback server is started.
expressvpn_connection_status (0/1)expressvpn_connection_state{state}expressvpn_connection_uptime_secondsexpressvpn_last_state_change_timestamp_secondsexpressvpn_state_changes_totalexpressvpn_connect_attempts_totalexpressvpn_connect_failures_totalexpressvpn_connection_info{server,connected_server,protocol,network_lock}expressvpn_vpn_ip_info{ip}expressvpn_public_ip_info{ip}expressvpn_vpn_interface_info{interface}expressvpn_vpn_interface_up{interface}expressvpn_vpn_interface_mtu_bytes{interface}expressvpn_network_rx_bytes_total{interface}expressvpn_network_tx_bytes_total{interface}expressvpn_network_rx_packets_total{interface}expressvpn_network_tx_packets_total{interface}expressvpn_network_rx_errors_total{interface}expressvpn_network_tx_errors_total{interface}expressvpn_network_rx_dropped_total{interface}expressvpn_network_tx_dropped_total{interface}scrape_configs:
- job_name: expressvpn
metrics_path: /metrics.cgi
static_configs:
- targets: ["expressvpn:9797"]
An example Grafana dashboard is provided at example/expressvpn-grafana-dashboard.json.
Import it in Grafana and select your Prometheus datasource to view all exported metrics.
The container healthcheck runs every 2 minutes.
DDNS or IP to compare against the ExpressVPN public IP.BEARER (ipinfo.io token) improves reliability.HEALTHCHECK posts status to healthchecks.io.If DDNS or IP are not set, the healthcheck is always healthy.
To avoid DNS leaks, update dependent containers to use the resolv.conf from this container after connect.
Run the DNS leak test inside the container:
curl -s https://raw.githubusercontent.com/macvk/dnsleaktest/refs/heads/master/dnsleaktest.sh | docker exec -i expressvpn bash -s
Set SERVER=smart or any region name.
On startup with SERVER=smart, the container waits briefly for the smart location
to refresh before connecting, so the first connection aligns with the latest smart region.
List regions from inside the container:
expressvpnctl get regions
./expressbuild.sh 5.0.1.11498 test-repo
docker pull misioslav/expressvpn
Content type
Image
Digest
sha256:60c4d5da7…
Size
158.9 MB
Last updated
9 days ago
docker pull misioslav/expressvpn