OpenVPN and Docker logos. OpenVPN and Docker logos.

This How-To will show you how to launch an OpenVPN server in Docker Swarm, running in dual (TCP/UDP) mode. It will use swarm-launcher to start the processes in privileged mode and Nginx as a loadbalancer/proxy for allowing connections to the VPN server.

Table of Contents

Rationale

If you ever tried running a privileged container in docker swarm, you might have noticed that it’s currently not possible use --cap-add, --cap-drop or --privileged with docker stack deploy. Here is a workaround for that.

I’ll be using in this example the following docker images:

  • ixdotai/openvpn:latest - OpenVPN server in a Docker container complete with an EasyRSA PKI CA
  • ixdotai/swarm-launcher:latest - A docker image to allow the launch of container in docker swarm, with options normally unavailable to swarm mode
  • nginx:latest - an open source reverse proxy server for HTTP, HTTPS, SMTP, POP3, and IMAP protocols, as well as a load balancer, HTTP cache, and a web server (origin server)

Objectives

The objective is to have a fully functional OpenVPN server, that you can connect to, running on docker swarm. I’ll be using ports 1194/udp and 8443/tcp in this example.

Note: All the commands below need to be run on a swarm manager. You can see your manager nodes by running: sudo docker node ls -f role=manager.

Prerequisites

  • Have a fully functional docker swarm
  • Have a public IP address that points is configured on the swarm
  • (optional) Have a DNS entry that points at the IP address

Architectures Supported

  • linux/amd64
  • linux/arm64
  • linux/arm/v7
  • linux/arm/v6

OpenVPN

Getting the OpenVPN server up and running requires a few manual steps before deploying it. Make sure you read the documentation if you run into any problems.

Storage

OpenVPN needs a storage for the configuration and certificates. Ideally, you have a secure distributed storage available in your swarm (like NFS or CEPH). For this tutorial, I will assume that it is mounted under /var/docker/openvpn.

Run the following on one of your swarm

export OVPN_DATA="/var/docker/openvpn"

Initialise the Configuration

If you have a domain name, make sure to replace it below. If not, you can use the public IP address

export ENDPOINT="VPN.EXAMPLE.COM"

Generate the Configuration

sudo docker run -v "${OVPN_DATA}":/etc/openvpn --log-driver=none --rm ixdotai/openvpn ovpn_genconfig -u udp://"${ENDPOINT}" -b
Click to see output example
Unable to find image 'ixdotai/openvpn:latest' locally
latest: Pulling from ixdotai/openvpn
8fa90b21c985: Already exists
a6e1cf67f1ae: Pull complete
29c596d05c0e: Pull complete
3c36e8be4cab: Pull complete
748c329393a2: Pull complete
Digest: sha256:c7eb026fad0d1b7f9d299d5375b025c3f84451782fc31e6baeed8bf171c17efc
Status: Downloaded newer image for ixdotai/openvpn:latest
Disable default setenv opt for 'block-outside-dns'
Processing Route Config: '192.168.254.0/24'
Processing PUSH Config: 'dhcp-option DNS 1.1.1.1'
Processing PUSH Config: 'dhcp-option DNS 1.0.0.1'
Processing PUSH Config: 'comp-lzo no'
Successfully generated config
Cleaning up before Exit ...

This will effectively set the default client configuration to use the port 1194/udp. Take a look at docs/tcp.md for information on TCP.

Initialise the PKI

sudo docker run -v "${OVPN_DATA}":/etc/openvpn --log-driver=none --rm -it ixdotai/openvpn ovpn_initpki

You will need to choose a password (using the nopass option is not secure). You can also set a name for the PKI, or just use the default one.

The command will take some time, since it generates Diffie-Hellman parameters.

Click to see output example
init-pki complete; you may now create a CA or requests.
Your newly created PKI dir is: /etc/openvpn/pki

1+0 records in
1+0 records out

Using SSL: openssl OpenSSL 1.1.1d  10 Sep 2019

Enter New CA Key Passphrase:
Re-Enter New CA Key Passphrase:
Generating RSA private key, 2048 bit long modulus (2 primes)
....................................................................................................+++++
....+++++
e is 65537 (0x010001)
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Common Name (eg: your user, host, or server name) [Easy-RSA CA]:

CA creation complete and you may now import and sign cert requests.
Your new CA certificate file for publishing is at:
/etc/openvpn/pki/ca.crt


Using SSL: openssl OpenSSL 1.1.1d  10 Sep 2019
Generating DH parameters, 2048 bit long safe prime, generator 2
This is going to take a long time
...........................................................................................................................................................................................................................................................................................................................................................+............................................................................................................................................................+.......................................+............................................................................+.......................................................................................................................................................................................................+..........................................................................+.............................................................................................................................+....................+........................................................................+...+................................................................+...........................................................................................+..................................................................................................................+........................................................................................................+...............................+.......+....................................+..+...................+...................................................................................................................+.................................................................+..........................+......................+..................................................................................................................................................................................+....+..........+...................................................+....................................................................................+...............................................+.............................................................................................+...............................................................................................................................+.......+................................+.................................................+..................................................................................+.............................................................+......................................................................................................+......................................+..............+...+...........................................+.............................................................................+......................................................................................................................+........................................................................................................................................................................+..................+..............................................................................................................................................................................................................................................................+............................+...........+..........................................................++*++*++*++*

DH parameters of size 2048 created at /etc/openvpn/pki/dh.pem


Using SSL: openssl OpenSSL 1.1.1d  10 Sep 2019
Generating a RSA private key
.+++++
...........................................................................................................................+++++
writing new private key to '/etc/openvpn/pki/private/VPN.EXAMPLE.COM.key.XXXXAEGCfL'
-----
Using configuration from /etc/openvpn/pki/safessl-easyrsa.cnf
Enter pass phrase for /etc/openvpn/pki/private/ca.key:
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName            :ASN.1 12:'VPN.EXAMPLE.COM'
Certificate is to be certified until Feb  7 08:06:49 2023 GMT (1080 days)

Write out database with 1 new entries
Data Base Updated

Using SSL: openssl OpenSSL 1.1.1d  10 Sep 2019
Using configuration from /etc/openvpn/pki/safessl-easyrsa.cnf
Enter pass phrase for /etc/openvpn/pki/private/ca.key:

An updated CRL has been created.
CRL file: /etc/openvpn/pki/crl.pem

Nginx

Nginx needs a configuration file (nginx.conf). We’ll be using docker config to store it in the swarm.

error_log /dev/stdout info;

events {}

http {}

stream {
  resolver 127.0.0.11 valid=1s ipv6=off;
  map $remote_addr $backend_udp {
    default vpn_vpn-udp_1; # Set this to ${LAUNCH_PROJECT_NAME}_${LAUNCH_SERVICE_NAME}_1 or to ${LAUNCH_CONTAINER_NAME}
  }
  map $remote_addr $backend_tcp {
    default vpn_vpn-tcp_1; # Set this to ${LAUNCH_PROJECT_NAME}_${LAUNCH_SERVICE_NAME}_1 or to ${LAUNCH_CONTAINER_NAME}
  }
  server {
    listen 1194 udp;
    proxy_pass $backend_udp:1194;
  }
  server {
    listen 8443;
    proxy_pass $backend_tcp:1194;
  }
}

Make sure this file is on the swarm manager and is called nginx.conf.

Create the Config

sudo docker config create nginx.conf.v1 nginx.conf

You can see that it’s been created by running:

docker config ls
ID                          NAME                       CREATED             UPDATED
8tx79ul78nwtjvb47xjm4qu7a   nginx.conf.v1              14 seconds ago      14 seconds ago

You’ll notice that I’ve added .v1 to the config name. This will allow you to update later on the config and just point nginx to the new one.

Docker Stack

Luckily, the swarm-launcher doesn’t need any additional configuration, so we can now create and deploy the stack.

Create the Stack YML File

On a swarm manager, create stack.yml with the following content:

version: "3.7"

services:
  vpn-launcher-udp:
    deploy:
      labels:
        ai.ix.auto-update: 'true'
    image: ixdotai/swarm-launcher:latest
    volumes:
      - '/var/run/docker.sock:/var/run/docker.sock:rw'
    environment:
      LAUNCH_IMAGE: ixdotai/openvpn:latest
      LAUNCH_PULL: 'true'
      LAUNCH_EXT_NETWORKS: 'vpn_vpn-proxy'  # Set this to NameOfStack_NameOfNetwork
      LAUNCH_PROJECT_NAME: 'vpn'
      LAUNCH_SERVICE_NAME: 'vpn-udp'
      LAUNCH_CAP_ADD: 'NET_ADMIN'
      LAUNCH_PRIVILEGED: 'true'
      LAUNCH_ENVIRONMENTS: 'OVPN_NATDEVICE=eth1'
      LAUNCH_VOLUMES: '/var/docker/openvpn:/etc/openvpn:rw'
  vpn-launcher-tcp:
    deploy:
      labels:
        ai.ix.auto-update: 'true'
    image: ixdotai/swarm-launcher:latest
    volumes:
      - '/var/run/docker.sock:/var/run/docker.sock:rw'
    environment:
      LAUNCH_IMAGE: ixdotai/openvpn:latest
      LAUNCH_PULL: 'true'
      LAUNCH_EXT_NETWORKS: 'vpn_vpn-proxy'  # Set this to NameOfStack_NameOfNetwork
      LAUNCH_PROJECT_NAME: 'vpn'
      LAUNCH_SERVICE_NAME: 'vpn-tcp'
      LAUNCH_CAP_ADD: 'NET_ADMIN'
      LAUNCH_PRIVILEGED: 'true'
      LAUNCH_ENVIRONMENTS: 'OVPN_NATDEVICE=eth1'
      LAUNCH_VOLUMES: '/var/docker/openvpn:/etc/openvpn:rw'
      LAUNCH_COMMAND: 'ovpn_run --proto tcp'
  nginx-proxy:
    deploy:
      labels:
        ai.ix.auto-update: 'true'
    image: nginx:latest
    networks:
      - vpn-proxy
    configs:
      - source: nginx.conf.v1
        target: /etc/nginx/nginx.conf
    ports:
      - '1194:1194/udp'
      - '8443:8443'
configs:
  nginx.conf.v1:
    external: true
networks:
  vpn-proxy:
    attachable: true
    driver: overlay
    driver_opts:
      encrypted: 'true'

Note: You’ll notice the label ai.ix.auto-update: 'true'. I use this with ixdotai/cioban, to automatically update swarm services to the latest version of the docker image. You can leave it out, if you don’t use it.

WARNING: If you decide not to use the name vpn for the stack, but something else (say beldeneige), you must change the variable LAUNCH_EXT_NETWORKS to match it (e.g. beldeneige_vpn-proxy).

Deploy the Stack

sudo docker stack deploy --compose-file stack.yml --prune vpn

That’s it. You should soon see the services running:

sudo docker service ls -f name=vpn
ID                  NAME                   MODE                REPLICAS            IMAGE                           PORTS
fp8qyz2divix        vpn_nginx-proxy        replicated          1/1                 nginx:latest                    *:8443->8443/tcp, *:1194->1194/udp
7aix7z5mrtnk        vpn_vpn-launcher-tcp   replicated          1/1                 ixdotai/swarm-launcher:latest
xk14lwmvwbw7        vpn_vpn-launcher-udp   replicated          1/1                 ixdotai/swarm-launcher:latest

You can also look on which node ixdotai/swarm-launcher was started:

sudo docker service ps vpn_vpn-launcher-tcp
ID                  NAME                         IMAGE                           NODE                DESIRED STATE       CURRENT STATE          ERROR               PORTS
q02qnd9c9q1o        vpn_vpn-launcher-tcp.1       ixdotai/swarm-launcher:latest   docker-c            Running             Running 3 hours ago
1fziscmqskxn         \_ vpn_vpn-launcher-tcp.1   ixdotai/swarm-launcher:latest   docker-a            Shutdown            Shutdown 3 hours ago

Finally, you can look at all containers started by ixdotai/swarm-launcher locally:

sudo docker ps -f label=ai.ix.started-by=ix.ai/swarm-launcher
CONTAINER ID        IMAGE                         COMMAND                  CREATED             STATUS              PORTS               NAMES
e7e31a40dc79        ixdotai/openvpn:latest        "ovpn_run --proto tcp"   3 hours ago         Up 3 hours          1194/udp            vpn_vpn-tcp_1
ab39ac215297        gitlab/gitlab-runner:alpine   "/usr/bin/dumb-init …"   4 days ago          Up 4 days                               gitlab-ci-runner

(Yes, I start my GitLab Runner also with ixdotai/swarm-launcher)

Troubleshooting

All the logs of ixdotai/swarm-launcher and of the ixdotai/openvpn container are available in the docker service. In our case, we can run:

sudo docker service logs vpn_vpn-launcher-tcp
Click to see output example
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | swarm-launcher: Doing startup check
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | swarm-launcher: Testing compose file
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | networks:
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |   vpn_vpn-proxy:
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |     external: true
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |     name: vpn_vpn-proxy
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | services:
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |   vpn-tcp:
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |     cap_add:
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |     - NET_ADMIN
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |     command: ovpn_run --proto tcp
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |     environment:
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |       OVPN_NATDEVICE: eth1
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |     image: ixdotai/openvpn:latest
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |     labels:
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |       ai.ix.started-by: ix.ai/swarm-launcher
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |     networks:
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |       vpn_vpn-proxy: null
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |     privileged: true
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |     restart: "no"
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |     volumes:
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |     - /var/storage/docker/openvpn:/etc/openvpn:rw
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | version: '3.7'
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | swarm-launcher: Pulling ixdotai/openvpn:latest
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | latest: Pulling from ixdotai/openvpn
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | c9b1b535fdd9: Already exists
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | 83a8a9aaf865: Pulling fs layer
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | 4ae626e5972b: Pulling fs layer
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | 2dd917a98926: Pulling fs layer
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | 190cce0f75ce: Pulling fs layer
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | 190cce0f75ce: Waiting
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | 4ae626e5972b: Verifying Checksum
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | 4ae626e5972b: Download complete
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | 2dd917a98926: Verifying Checksum
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | 2dd917a98926: Download complete
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | 83a8a9aaf865: Download complete
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | 190cce0f75ce: Verifying Checksum
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | 190cce0f75ce: Download complete
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | 83a8a9aaf865: Pull complete
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | 4ae626e5972b: Pull complete
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | 2dd917a98926: Pull complete
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | 190cce0f75ce: Pull complete
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | Digest: sha256:c7eb026fad0d1b7f9d299d5375b025c3f84451782fc31e6baeed8bf171c17efc
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | Status: Downloaded newer image for ixdotai/openvpn:latest
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | ------------------------------------------------------
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | The Docker Engine you're using is running in swarm mode.
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | Compose does not use swarm mode to deploy services to multiple nodes in a swarm. All containers will be scheduled on the current node.
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | To deploy your application across the swarm, use `docker stack deploy`.
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    |
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | Creating vpn_vpn-tcp_1 ...
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | Attaching to vpn_vpn-tcp_1
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | iptables: No chain/target/match by that name.
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | iptables: No chain/target/match by that name.
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Enabling IPv6 Forwarding
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | net.ipv6.conf.all.disable_ipv6 = 0
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | net.ipv6.conf.default.forwarding = 1
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | net.ipv6.conf.all.forwarding = 1
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Running 'openvpn --config /etc/openvpn/openvpn.conf --client-config-dir /etc/openvpn/ccd --crl-verify /etc/openvpn/crl.pem --proto tcp'
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 Unrecognized option or missing or extra parameter(s) in /etc/openvpn/openvpn.conf:27: block-outside-dns (2.4.8)
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 OpenVPN 2.4.8 x86_64-alpine-linux-musl [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [MH/PKTINFO] [AEAD] built on Feb  7 2020
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 library versions: OpenSSL 1.1.1d  10 Sep 2019, LZO 2.10
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 Diffie-Hellman initialized with 2048 bit key
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 Failed to extract curve from certificate (UNDEF), using secp384r1 instead.
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 ECDH curve secp384r1 added
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 Outgoing Control Channel Encryption: Cipher 'AES-256-CTR' initialized with 256 bit key
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 Outgoing Control Channel Encryption: Using 256 bit message hash 'SHA256' for HMAC authentication
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 Incoming Control Channel Encryption: Cipher 'AES-256-CTR' initialized with 256 bit key
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 Incoming Control Channel Encryption: Using 256 bit message hash 'SHA256' for HMAC authentication
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 ROUTE_GATEWAY 172.18.0.1/255.255.0.0 IFACE=eth1 HWADDR=02:42:ac:12:00:1f
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 TUN/TAP device tun0 opened
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 TUN/TAP TX queue length set to 100
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 /sbin/ip link set dev tun0 up mtu 1500
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 /sbin/ip addr add dev tun0 local 192.168.255.1 peer 192.168.255.2
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 /sbin/ip route add 192.168.254.0/24 via 192.168.255.2
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 /sbin/ip route add 192.168.255.0/24 via 192.168.255.2
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 Could not determine IPv4/IPv6 protocol. Using AF_INET
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 Socket Buffers: R=[131072->131072] S=[16384->16384]
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 Listening for incoming TCP connection on [AF_INET][undef]:1194
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 TCPv4_SERVER link local (bound): [AF_INET][undef]:1194
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 TCPv4_SERVER link remote: [AF_UNSPEC]
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 GID set to nogroup
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 UID set to nobody
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 MULTI: multi_init called, r=256 v=256
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 IFCONFIG POOL: base=192.168.255.4 size=62, ipv6=0
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 MULTI: TCP INIT maxclients=1024 maxevents=1028
vpn_vpn-launcher-tcp.1.9jjzfgjlelpo@docker-a    | vpn-tcp_1  | Sun Feb 23 09:58:30 2020 Initialization Sequence Completed

Next Steps

Take a look at the OpenVPN README and Advanced Client Management documentation.

You can start by creating the configuration for your VPN clients:

sudo docker run -v "${OVPN_DATA}":/etc/openvpn --log-driver=none --rm -it ixdotai/openvpn easyrsa build-client-full CLIENTNAME nopass

You then can save the configuration:

sudo docker run -v "${OVPN_DATA}":/etc/openvpn --log-driver=none --rm ixdotai/openvpn ovpn_getclient CLIENTNAME > CLIENTNAME.ovpn