what is docker for? How to start with Docker? An example of a web hosting provider.
Duh!
This is not meant to be the thousandth “How To use Docker”. I’m going to write down my experience I’m making with Docker here. Basically, Docker is only one of many container solutions that exist at the moment, the special thing about Docker is its large user base. Similar to the Raspberry Pi, it is by no means the most powerful device, but the large community makes up for it. There are hundreds of explanations and how to’s for every problem, but copy’ n paste is not the right solution for every problem. I write here in the hope that you want to understand what you are doing and not just throw everything into the terminal without thinking about it.
When I learn something new, I look for a goal, usually the goal is exaggerated – maybe as now. My goal is a machine that a web hoster could use to sell web apps or Webspace to his customers. Without automation, but it can be retrofitted.
So at the end of this article we will have a single docker host on which we can set up a new web app in less than two minutes. The app has its own domain or subdomain and a valid SSL certificate from Let’s Encrypt. After understanding the basic docker commands in the terminal, we use a Gui like Portainer. We will have read a lot of docs and tutorials and have asked ourselves one or the other time “why don’t we just use KVM?”. Probably the blog post will simply become a large collection of links to the topic Docker.
Prerequisites:
- Debain 9 minimal install
- A domain with DNS setup – here we use example. com
That’s it. I strongly assume that you know what a terminal is and how it is used and that your DNS has a www CNAME or a subdomain wildcard entry pointing to the server. Testing the following in a virtual machine will certainly not hurt.
Lets Go
The version of Docker provided by Debian is quite old, Debian-like. So we will install the docker repositories. How, is explained in the Docker docs.
➜ ~ apt install docker-ce
Systemd should start Docker on boot:
➜ ~ systemctl enable docker
Our host will use a Nginx as a proxy server in front of the apps. We only have ports 80 and 443 once and don’t want to have to deal with data like ‘example.com:37145’. In addition, we want to be able to run several domains on one host. These are called ‘Server Blocks‘ and use the ‘server_name‘ directives of Nginx. The equivalent of the server blocks would be the ‘virtual hosts‘ for Apache.
But there are the wonderful projects by Jason Wilder called ‘nginx-proxy’ and ‘docker-gen’.
nginx-proxy sets up a container running nginx and docker-gen. docker-gen generates reverse proxy configs for nginx and reloads nginx when containers are started and stopped.
docker-gen is a file generator that renders templates using docker container meta-data.
In order for this proxy to automatically provide ssl certificates from Let’s Encrypt, Yves Blusseau has devised the ‘LetsEncrypt companion container for nginx-proxy‘.
letsencrypt-nginx-proxy-companion is a lightweight companion container for the nginx-proxy. It allows the creation/renewal of Let’s Encrypt certificates automatically.
What’s going to happen here? Events are sent as soon as a container changes its status. docker-gen picks up the Start/Stop events, creates a new server block for the newly created app and reloads the nginx-proxy. ‘LetsEncrypt companion container for nginx-proxy’ goes one step further and lets Let’s Encrypt issue a certificate for the domain specified when starting an app.
nginx-proxy und docker-gen
What happens now:
- We first start the’ nginx-proxy’ container
- we start a web app with hostname, here a Nginx webserver
➜ ~ docker run -d -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock:ro jwilder/nginx-proxy
Here you can find the docker run reference for an explanation of the command.
Nginx proxy expects the environment variable VIRTUAL_HOST
in which the hostname of the new app is passed. So we start our first web app – or a new webserver.
➜ ~ docker run -e VIRTUAL_HOST=example.com nginx
That’s it. Our newly created Nginx is now available at example.com
, but still unencrypted. Docker places both containers in the default network called’ bridge’, which causes Docker to bind its socket to a publicly accessible container, an unnecessary attack vector – but use docker and can lock EVERYTHING in its own container.
As we have already read above, Yves’ JrCs’ Blusseau describes the possibility to lock all three services into individual containers, which we absolutely want to do. So let’s start over:
➜ ~ docker stop $(docker ps -a -q) && docker rm $(docker ps -a -q)
with this command, ALL running containers are stopped and then deleted.
nginx-proxy, docker-gen and letsencrypt-nginx-proxy-companion
What happens now:
- We create a directory under /var/docker/ for the nginx-proxy where he can persistently store his data and create the file nginx.tmpl for the docker-gen container.
- We launch the official Nginx Container image with persistent volumes – in this example it’s ‘bind mounts‘ and share it with the docker-gen container.
- We launch the docker-gen container, with the volumes of the Nginx and the just created nginx.tmpl and give it access to the docker socket to intercept the start/stop events.
- We launch the letsencrypt-nginx-proxy-companion, with the volumes of the Nginx, a volume for the generated certificates and give it access to the docker socket to intercept the start/stop events.
The letsencrypt-nginx-proxy-companion, as well as the nginx-gen, is informed via the docker socket of Start/Stop Events of a container. Letsencrypt-nginx-proxy-companion expects at least the two self-explanatory environment variables LETSENCRYPT_HOST
and LETSENCRYPT_EMAIL
for each container. If it catches a Start/Stop event of a container and finds these two variables, it tries to get issued a certificate from Let’s Encrypt. To do this, the LETSENCRYPT_HOST
and VIRTUAL_HOST
variables must be the same domain name, of course. SAN certificates are supported, which makes the work easier.
We create the directory /var/docker/nginx
to store all files and save the nginx.tmpl there:
➜ ~ mkdir -p /var/docker/nginx ➜ ~ curl https://raw.githubusercontent.com/jwilder/nginx-proxy/master/nginx.tmpl > /var/docker/nginx/nginx.tmpl
Start the official Nginx container and mount all volumes in our newly created directory:
➜ ~ docker run -d -p 80:80 -p 443:443 --name nginx -v /var/docker/nginx/conf.d:/etc/nginx/conf.d -v /var/docker/nginx/vhost.d:/etc/nginx/vhost.d -v /var/docker/nginx/html:/usr/share/nginx/html -v /var/docker/nginx/certs:/etc/nginx/certs:ro --label com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy nginx
Start docker-gen:
➜ ~ docker run -d --name nginx-gen --volumes-from nginx -v /var/docker/nginx/nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro -v /var/run/docker.sock:/tmp/docker.sock:ro --label com.github.jrcs.letsencrypt_nginx_proxy_companion.docker_gen jwilder/docker-gen -notify-sighup nginx -watch -wait 5s:30s /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
Start letsencrypt-nginx-proxy-companion:
➜ ~ docker run -d --name nginx-letsencrypt --volumes-from nginx -v /var/docker/nginx/certs:/etc/nginx/certs:rw -v /var/run/docker.sock:/var/run/docker.sock:ro jrcs/letsencrypt-nginx-proxy-companion
Here the rough work is done and we will start a container next.
Our first productive app
As we have learned above, we only need three additional environment variables for any web app that should be accessible via a domain:
- VIRTUAL_HOST -> for docker-gen
- LETSENCRYPT_HOST -> for letsencrypt-nginx-proxy-companion
- LETSENCRYPT_EMAIL -> for letsencrypt-nginx-proxy-companion
For our example, this time we start an Apache and use the great Apache Container from LinuxServer.io. Here all relevant directories are mapped to a volume on the host (bind mount). This is very convenient for uploading content or backups. As host name we use our domain example.com
and have a SAN certificate issued on example.com
and www.example.com
.
➜ ~ docker run --name example.com -e VIRTUAL_HOST='example.com, www.example.com' -e LETSENCRYPT_HOST='example.com, www.example.com' -e LETSENCRYPT_EMAIL=mail@example.com -v /var/docker/example.com:/config linuxserver/apache
It is even shorter if we define an environment variable first:
➜ ~ HOSTNAME='example.com, www.example.com' ➜ ~ docker run --name example.com -e VIRTUAL_HOST=$HOSTNAME -e LETSENCRYPT_HOST=$HOSTNAME -e LETSENCRYPT_EMAIL=mail@example.com -v /var/docker/example.com:/config linuxserver/apache
Since we read dockers run reference carefully at the beginning, the above command is pretty self-explanatory. We use --name example.com
to specify a self-explanatory name for our container and the location of the volume. Here it’s /var/docker/example.com
. Docker creates /var/docker/example.com
and maps the container’s directory /config
into it:
➜ ~ ls -l /var/docker/example.com/ total 0 drwxr-xr-x 1 911 911 64 Oct 25 12:21 apache drwxr-xr-x 1 911 911 32 Oct 25 12:21 keys drwxr-xr-x 1 911 911 12 Oct 25 12:21 log drwxr-xr-x 1 911 911 20 Oct 25 12:21 www
Finished! Our Apache is now accessible from the Internet under the addresses example.com
and www.example.com
, the communication is encrypted via TLS. We can conveniently place the content that we want to deliver under /var/docker/example.com/www/
.
GUI
Cool kids use the CLI – but im no kid anymore and a good GUI can make life much easier for us. This is where Portainer comes in.
PORTAINER IS AN OPEN-SOURCE LIGHTWEIGHT MANAGEMENT UI WHICH ALLOWS YOU TO EASILY MANAGE YOUR DOCKER HOSTS OR SWARM CLUSTERS
Portainer is as easy to deploy as any other container, here is the documentation.
➜ ~ docker volume create portainer_data ➜ ~ docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer
First a persistent volume is created and then the container container is bound to it and started. Communication with this container via our browser is now possible via http://example.com:9000 and is not encrypted! It is more comfortable and safer, for example, via a subdomain from example.com. As we have learned above, we can do this without much effort.
So we stop Portainer again. For this we need the name of the container. The command docker ps
lists the current running containers. Unfortunately, I can’t find a possibility to filter containers by used image, but we have grep. A docker stop
CONTAINER_NAME
stops our running Portainer.
➜ ~ docker ps | grep portainer 29645b015c34 portainer/portainer "/portainer" 3 minutes ago Up 3 minutes 0.0.0.0:9000->9000/tcp mystifying_euler ➜ ~ docker stop mystifying_euler mystifying_euler
Now we start Portainer in our own way. The volume ‘portainer_data’ still exists, we don’t need to recreate it. This time we give the container a name, and we don’t want Portainer to be accessible via a port on the host. For this, we have our nginx-proxy.
What do we need?
- VIRTUAL_HOST -> portainer.example.com
- LETSENCRYPT_HOST -> portainer.example.com
- LETSENCRYPT_EMAIL -> mail@example.com
➜ ~ docker run -d --name portainer -e VIRTUAL_HOST=portainer.example.com -e LETSENCRYPT_HOST=portainer.example.com -e LETSENCRYPT_EMAIL=mail@example.com -v /var/run/docker.sock:/var/run/docker.sock -v /opt/portainer:/data portainer/portainer:latest
Portainer is now available on the Internet at https://portainer.example.com
.
What we have learned so far:
- install Docker
- Nginx – Server Blocks and server_name
- Apache – Virtual Host
- how to use nginx-proxy and docker-gen
- how to use nginx-proxy, docker-gen and letsencrypt-nginx-proxy-companion
- Docker events
- ‘docker run’
- Docker volumes
- Docker bind mounts
- Portainer – start Portainer
- ‘docker ps’
- ‘docker stop’