How do I use Docker to run a Tor middle/guard relay so that I can easily stay up-to-date with the latest software changes?



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:


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.


docker run -d --name my-relay \
  -p \
  -p 9001:9001 \
  -p 9030:9030 \
  -p \
  -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 \

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:

  1. sudo docker ps -a to find the id of the running docker container, for example 49283a2b3ffa.
  2. 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.
  3. 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.


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 >9001/tcp,>9030/tcp,>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.


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!


Thank you for your sharing. i've try your explanation. But we need to edit run.sh and Docker file since docker did't allow us to share /etc/localtime directly.

for run.sh we can use TZ package to retrieve host timezone.


export ETC_LOCALTIME=$(readlink /etc/localtime) echo ${ETC_LOCALTIME}

docker run -d --name containername \ -e TZ='ls -la /etc/localtime | cut -d/ -f8-9' \ -p \ -p 9001:9001 \ -p 9030:9030 \ -p \ -v /Docker/conf/torrc:/etc/tor/torrc:ro \ -v /Docker/data/:/var/lib/tor \ --restart unless-stopped \ myname/my-relay:latest

For Dockerfile, we need to add tzdata. FROM alpine:edge RUN apk add --update tzdata RUN apk add --no-cache tor RUN apk add --update \ python \ python-dev \ py-pip \ build-base \ && pip install nyx

EXPOSE 9050 9051 VOLUME ["/var/lib/tor"] USER tor CMD ["/usr/bin/tor"]

| improve this answer | |
  • Docker has always been able to share local time on my systems. Are you sure you ran the commands with enough privileges? – Birb Feb 29 at 11:24

Use the docker image available (based on Debian image): https://hub.docker.com/r/doudou34/tor-server

Update: Docker image link updated to a more recent dockerhub project.

| improve this answer | |
  • Note that this Docker image is currently 5 months out of date and is likely missing security updates, but the Dockerfile itself may be helpful. – Steve Oct 1 at 0:01
  • Outdated and sounds more like a personal opinion. This is not an official release it seems so at the end of the day it's likely a lot better to manage everything from the ground up one self. – Birb Oct 2 at 0:04
  • Updated link now to: hub.docker.com/r/doudou34/tor-server – danger89 Oct 16 at 2:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.