Sign inSign up

misioslav/expressvpn

By misioslav

•Updated 3 months ago

ExpressVPN in a box

Image
Networking
Security
14

100K+

misioslav/expressvpn repository overview

⁠ExpressVPN

Container based on polkaned/expressvpn⁠ with additional features and automation.

⁠Table of Contents

⁠Features

  • ExpressVPN 5.x CLI (expressvpnctl) with headless activation.
  • Automatic activation via CODE and background mode enable.
  • Protocol selection with validation.
  • Network Lock support with optional LAN access and routed subnets.
  • SOCKS5 proxy (microsocks) with auth and whitelist support.
  • Prometheus metrics exporter with /metrics or custom .cgi path.
  • Optional control server API to query status and control connections.
  • Healthcheck with optional DDNS/IP validation and healthchecks.io support.
  • DNS whitelist for custom resolvers.
  • Built on debian:trixie-slim (amd64) with updated system packages.

⁠Quickstart (docker run)

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

⁠Docker Compose

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

⁠SERVERS AVAILABLE

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.

⁠Configuration

Environment variables (defaults shown):

ENVDescriptionDefault
CODEExpressVPN activation codecode
SERVERRegion name or smartsmart
PROTOCOLVPN protocollightwayudp
NETWORKNetwork Lock (on/off)on
ALLOW_LANAllow LAN access while Network Lock is ontrue
LAN_CIDRComma-separated LAN CIDRs for return routes(empty)
WHITELIST_DNSComma-separated DNS servers to allow via iptables(empty)
DDNSDomain to compare with ExpressVPN public IP for healthcheck(empty)
IPStatic IP to compare with ExpressVPN public IP for healthcheck(empty)
BEARERipinfo.io bearer token (healthcheck and /v1/ip)(empty)
HEALTHCHECKhealthchecks.io UUID(empty)
METRICS_PROMETHEUSEnable metrics exporter (on/off)off
METRICS_PORTMetrics port9797
METRICS_PATHMetrics path (absolute, ends with .cgi)/metrics.cgi
CONTROL_SERVEREnable control server (on/off)off
CONTROL_IPControl server bind IP0.0.0.0
CONTROL_PORTControl server port8000
AUTH_CONFIGAuth config file path/expressvpn/config.toml
CLOUDFLARE_SPEED_TIMEOUTSpeed test timeout in seconds (control server)120
SOCKSEnable SOCKS5 proxy (on/off)off
SOCKS_IPSOCKS bind IP0.0.0.0
SOCKS_PORTSOCKS port1080
SOCKS_USERSOCKS username(empty)
SOCKS_PASSSOCKS password(empty)
SOCKS_WHITELISTComma-separated IPs bypassing auth(empty)
SOCKS_AUTH_ONCECache auth by IP (true/false)false
SOCKS_LOGSEnable microsocks logs (true/false)true

⁠Protocols

Supported values for PROTOCOL:

  • auto
  • lightwayudp (default)
  • lightwaytcp
  • openvpnudp
  • openvpntcp
  • wireguard

⁠Network Lock and LAN Access

  • Network Lock is enabled by default (NETWORK=on).
  • If your kernel does not support Network Lock (minimum 4.9), it will be disabled at runtime.
  • ALLOW_LAN=true lets LAN traffic through while Network Lock is on.
  • Set LAN_CIDR (comma-separated) to add return routes for your LAN subnets.

⁠SOCKS5 Proxy

Enable the SOCKS5 proxy with:

  • SOCKS=on
  • Optional auth: set both SOCKS_USER and SOCKS_PASS
  • Optional whitelist: SOCKS_WHITELIST=ip1,ip2
  • Optional cache: SOCKS_AUTH_ONCE=true

⁠Control Server API

Enable the control API with:

  • CONTROL_SERVER=on
  • CONTROL_IP=0.0.0.0
  • CONTROL_PORT=8000
⁠Endpoints
MethodPathDescription
GET/v1/statusConnection status, server, IP
GET/v1/vpn/statusVPN status (running/stopped)
GET/v1/vpn/settingsVPN settings (protocol, region, allow_lan)
POST/v1/vpn/settingsUpdate VPN settings (protocol, region, allow_lan)
GET/v1/serversList available regions
GET/v1/ipPublic IP info (uses BEARER if set)
GET/v1/publicip/ipPublic IP ({"public_ip":"x.x.x.x"})
GET/v1/dnsResolver info and /etc/resolv.conf
GET/v1/dns/statusDNS status (running/stopped)
GET/v1/dnsleakDNS leak test result
GET/v1/speedtestCloudflare speed test (cloudflare-speed-cli --json)
GET/v1/healthAPI health check
POST/v1/connectConnect to server (JSON: { "server": "smart" })
POST/v1/disconnectDisconnect 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⁠

⁠Authentication

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_NAME
  • CONTROL_AUTH_USER
  • CONTROL_AUTH_PASSWORD
  • CONTROL_API_KEY
  • CONTROL_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
⁠Example usage

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

⁠Prometheus Metrics

Enable metrics with:

  • METRICS_PROMETHEUS=on
  • METRICS_PORT=9797
  • METRICS_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.

⁠Exported metrics
  • expressvpn_connection_status (0/1)
  • expressvpn_connection_state{state}
  • expressvpn_connection_uptime_seconds
  • expressvpn_last_state_change_timestamp_seconds
  • expressvpn_state_changes_total
  • expressvpn_connect_attempts_total
  • expressvpn_connect_failures_total
  • expressvpn_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}
⁠Prometheus scrape config example
scrape_configs:
  - job_name: expressvpn
    metrics_path: /metrics.cgi
    static_configs:
      - targets: ["expressvpn:9797"]
⁠Grafana dashboard

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.

⁠Healthcheck

The container healthcheck runs every 2 minutes.

  • Set DDNS or IP to compare against the ExpressVPN public IP.
  • Optional BEARER (ipinfo.io token) improves reliability.
  • Optional HEALTHCHECK posts status to healthchecks.io.

If DDNS or IP are not set, the healthcheck is always healthy.

⁠DNS Leak Check

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

⁠Servers Available

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

⁠Building

./expressbuild.sh 5.0.1.11498 test-repo

⁠Download

docker pull misioslav/expressvpn

Tag summary

Content type

Image

Digest

sha256:60c4d5da7…

Size

158.9 MB

Last updated

3 months ago

docker pull misioslav/expressvpn