misioslav/expressvpn

By misioslav

Updated 9 days 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

9 days ago

docker pull misioslav/expressvpn