Foreword

Docker defines a container as a "A standardized unit of software" [1] and does in a nutshell provide the tooling needed to create self-contained containers - think of little neatly-wrapped presents - of software or as separate components working together to achieve a greater good. An example of such a deployment could be a website backed by Node.js in one container. The website could be interacting with a MySQL database running in a different, separate container. The advantage of using containers instead of installing the software needed on the physical machine directly is, among others, simple deployment and maintenance of the software versions, as you can often throw away the container and build an entirely new one with updated software in a matter of seconds. This can of course be utilised to host Tor relays with ease.

Please note that this answer will not cover the port forwarding needed for the outside world to connect to the relay. This answer will furthermore assume, for the sake of simplicity, that we have a unix-like system such as an Ubuntu machine running which is going to host the relay, and that we have full (sudo) rights to install software and configure the machine.

Installing Docker

Go ahead and install Docker as described in the guide here: https://docs.docker.com/install/linux/docker-ce/ubuntu/. The community edition will do in this example. Once finished and after having confirmed that the installation seems to work, I highly recommend changing the permission of the docker binary to only be executable by the root user. Docker with lax permissions is a security concern that you don't want to deal with. To do so, execute the following, assuming docker is located at /usr/bin/docker :

$ sudo chown root:root /usr/bin/docker $ sudo chmod 500 /usr/bin/docker

Building the Tor Relay container

In this example we will first create a directory for everything Docker to reside in:

$ mkdir /docker

I then personally prefer to include four directories: bin, conf, data and images. Create these as well:

$ mkdir /docker/bin # Useful management scripts $ mkdir /docker/conf # Configuration, e.g. torrc $ mkdir /docker/data # Tor relay data, e.g. the fingerprint for consistency $ mkdir /docker/images # Dockerfiles

For simplicity, let's start talking about the configuration file, torrc. This file will be mapped into the container when it's started by using Docker volumes [2] and will be the main configuration of your Tor relay. Create a torrc file at, for example, /docker/conf/my-relay/torrc and populate it with something along the lines of:

ORPort 9001 DirPort 9030 ControlPort 9051 SocksPort 0 Nickname MyAwesomeDockerTorRelay ContactInfo yourusername@someprovider.com BandwidthRate 20 MBits BandwidthBurst 40 MBits CookieAuthentication 1 ExitRelay 0 ExitPolicy reject *:*

For more information about these settings, please see the Tor relay manual [3].

Next, let's create the container image that will contain Tor and - for our convenience - the awesome project nyx [4]. The image will be based on the very latest release of Alpine Linux [5], python 2.7 which is required by nyx, as well as the latest available version of Tor on Alpine Linux. The configuration looks like the following, and should be created at, for example, /docker/images/my-relay/Dockerfile :

FROM alpine:edge RUN apk add --no-cache tor RUN apk add --update \ python3 \ python3-dev \ py3-pip \ build-base \ && pip install nyx EXPOSE 9050 9051 VOLUME ["/var/lib/tor"] USER tor CMD ["/usr/bin/tor"]

Basically this tells Docker to, among other things, build a new image based on the edge version of alpine , install tor and python along with pip and nyx, and run tor when the container starts.

Now comes the building and the running of the containers. I use two scripts for my convenience so I don't need to type out the commands every time: /docker/bin/my-relay/build.sh and /docker/bin/my-relay/run.sh :

build.sh

#!/bin/bash docker build -t "myname/my-relay:latest" /docker/images/myname-my-relay

This builds your tor relay as specced above to your local Docker images registry. The -t flag tags the image with the name specified, indicating that this is the "latest build". One could use any version number for experimenting or for a more secure upgrade process as discussed later on.

run.sh

#!/bin/bash docker run -d --name my-relay \ -p 127.0.0.1:9050:9050 \ -p 9001:9001 \ -p 9030:9030 \ -p 127.0.0.1:9051:9051 \ -v /docker/conf/my-relay/torrc:/etc/tor/torrc:ro \ -v /etc/localtime:/etc/localtime \ -v /docker/data/my-relay:/var/lib/tor \ --restart unless-stopped \ myname/my-relay:latest

This starts the container and exposes in addition to the ports three folders on the physical machine to the container: conf containing torrc, data containing the relay fingerprint etc., and the localtime for good measure. Please note the restart policy --restart unless-stopped , which will ensure that the relay is always running unless asked politely not to.

This should be it. Once the image has been build and the container has started, you can remote into it and check the current status the following way:

sudo docker ps -a to find the id of the running docker container, for example 49283a2b3ffa . sudo docker exec -it 49 sh to get an sh shell inside the docker container. Note that we only specified 49 and not the full ID. Docker will know what you mean, as long as the provided ID is able to identify exactly one container. nyx to get a "graphical" overview of the container.

Upgrading the Tor relay

As indicated when discussing the build script, using latest as the version tag is most likely going to work just fine, but may not be the optimal case for all. If you provide an actual version, e.g. 1.0 and bump this version for every build, building new images will be more manageable and gives you the ability to revert to the previously working image, should you need to. You see, in order to update the image using the same tag, we first have to delete the container and image. This is trivial, however nothing is rock solid in IT, so relying 100% on the image to build perfectly fine another time is quite a bet. Either way, that is up to you. If you prefer to not use latest , you do not need to delete the image, but may still want to delete the container (you could just build another one given the working image if need be). For good measure I always delete the Alpine Linux image regardless. Better to be safe than sorry.

Cleaning up before upgrading

Delete the container Assuming the container still has ID 49..., run the following: sudo docker rm 49

Delete the image To delete the images (alpine and the relay itself), you need to get the IDs of the images:

$ sudo docker images REPOSITORY TAG IMAGE ID CREATED SIZE myname/my-relay latest bba371b29910 6 days ago 251MB alpine edge ffd13b1a1aa2 3 weeks ago 5.56MB

Then run sudo docker image rm bb ff to delete the two images.

Rebuilding

At this point you can run your shell scripts one more time:

$ sudo /docker/bin/my-relay/build.sh $ sudo /docker/bin/my-relay/run.sh

And confirm the status by running sudo docker ps -a . It should look like something along the lines of:

$ sudo docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES bf243a894bfd myname/my-relay:latest "/usr/bin/tor" 2 minutes ago Up 2 minutes 0.0.0.0:9001->9001/tcp, 0.0.0.0:9030->9030/tcp, 127.0.0.1:9050-9051->9050-9051/tcp my-relay

Note that because we exposed the data directory, your stats will continue where they left off because this directory contains the fingerprint.

Conclusion

We discussed how to easily configure a Docker container to run a Tor relay and give back to the community, while also having the convenience of easy updating and easy (un)deployment. Although this page turned out longer than intended, I think it's important to share not only the scripts but also the thoughts behind the content. Please feel free to leave comments and suggestions for improvements.

Thank you for joining me on this journey!