DIY VPN with Docker
I’ve worked with both ExpressVPN and NordVPN. Both are great services but, from my perspective, have one major shortcoming: they’re currently blocked by Amazon Web Services (AWS). When using either of them you are simply not able to access any of the AWS services.
The most common scenario in which I’d be using a VPN is if I’m on a restrictive network where I’m only able to access web sites. Typically just ports 80, 8080 and 443 are open. Forget about SSH (port 22), SMTP (ports 25, 465 and 587) or NTP (port 123). I want to be able to connect by SSH to my AWS servers, send mail over SMTP and synchronise my clock. The latter items are normally possible over commercial VPN providers (like ExpressVPN and NordVPN) but not being able to connect to AWS is a deal breaker.
Luckily there is a simple solution: run your own VPN server. Using a low end cloud instance on AWS or DigitalOcean (costing $5 or less per month) this is eminently plausible.
Launch a Cloud Server
Obviously this step needs to be done before you actually need the VPN! Spin up a minimal Ubuntu server on the cloud service of your choice (we’ll be using AWS for illustration).
Open ports 1194 (UDP) and 443 (TCP) on the server.
Make a SSH connection to the remote server (assuming that port 22 is open by default!). The instructions which follow should all be executed on the remote server.
docker pull $OVPN_IMAGE
The VPN configuration and certificates will be stored in a Docker volume. Create that now.
OVPN_DATA="ovpn-data" docker volume create --name $OVPN_DATA
You can check the contents of this volume using the following:
sudo ls /var/lib/docker/volumes/ovpn-data/_data
Grab the (public) DNS name for the server and stash it in a shell variable.
To reduce the volume of logging information it can be handy to include the
--log-driver=none option with the folowing invocations of
First we’ll set up a VPN operating over UDP on port 1194. From a bandwidth perspective this is efficient, but this port may well be closed (in which case see the TCP option below).
Generate the OpenVPN configuration.
docker run -v $OVPN_DATA:/etc/openvpn --rm $OVPN_IMAGE ovpn_genconfig -u udp://$DNSNAME -b
Initialise the EasyRSA Public Key Infrastructure (PKI).
docker run -v $OVPN_DATA:/etc/openvpn --rm -it $OVPN_IMAGE ovpn_initpki
Enter and verify a suitable private key (PEM) pass phrase when prompted. At the prompt for a Common Name, just accept the default. Boil the kettle. Enter the pass phrase when prompted. And again.
Now launch the OpenVPN daemon process.
docker run --name openvpn -v $OVPN_DATA:/etc/openvpn -d -p 1194:1194/udp --cap-add=NET_ADMIN $OVPN_IMAGE
Execute the commands below for a VPN over TCP on port 443 (this is the port for HTTPS, so is almost definitely going to be open, no matter how repressive the network!).
docker run -v $OVPN_DATA:/etc/openvpn --rm $OVPN_IMAGE ovpn_genconfig -u tcp://$DNSNAME:443 -b docker run -v $OVPN_DATA:/etc/openvpn --rm -it $OVPN_IMAGE ovpn_initpki
Launch the daemon.
docker run --name openvpn -v $OVPN_DATA:/etc/openvpn -d -p 443:1194/tcp --cap-add=NET_ADMIN $OVPN_IMAGE
User and Configuration
Regardless of whether you are creating a VPN over TCP or UCP, you now need to create the configuration file which will be used with the
openvpn client on your local machine.
docker run -v $OVPN_DATA:/etc/openvpn --rm -it $OVPN_IMAGE easyrsa build-client-full CLIENTNAME nopass
Enter the pass phrase when prompted.
docker run -v $OVPN_DATA:/etc/openvpn --rm $OVPN_IMAGE ovpn_getclient CLIENTNAME >CLIENTNAME.ovpn
Now disconnect from the server.
Now, back on your local machine use SFTP or SCP to get a local copy of the
.ovpn file from the server.
sudo apt install openvpn
Connect to the VPN.
sudo openvpn --config CLIENTNAME.ovpn
If everything goes well then you should see “Initialization Sequence Completed”. Confirm that your effective IP address is now that of the VPN server. Enjoy!
This setup is simple and cost effective. Typically I’ll only need a VPN for a few days in succession, so it’s very convenient that I can literally spin up a VPN when I know that I’m going to need it, then take it down when I’m done. No long term commitment. No hassles accessing any port or protocol I need.