Deploy a server with docker and four lines of code.

Through trial and effort, deploying the stack hosting Bonner.is involves four lines of bash code.

git clone https://github.com/moritonal/bonner-deploy.git
cd bonner-deploy
nano .env maybe-chat-server.env
docker-compose up

This is because of the fairly amazing power of the docker-compose.yml file.

Bonner.is serves two kinds of services (HTTPS and HTTP) over ports 80 and 445. To allow many services to share these two ports we have a load-balancer that listens for connections on these ports, and then passes the connection into the relevant service for a particular hostname. To make things interesting, the load-balance is the first Docker container we define.

version: "2"
services:
  nginx-proxy:
    image: jwilder/nginx-proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - certs:/etc/nginx/certs:ro
      - vhost:/etc/nginx/vhost.d
      - html:/usr/share/nginx/html
      - ./vhost.d:/etc/nginx/vhost.d:ro
    labels:
      com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy: ""

  nginx-proxy-companion:
    image: jrcs/letsencrypt-nginx-proxy-companion
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - certs:/etc/nginx/certs:rw
      - vhost:/etc/nginx/vhost.d
      - html:/usr/share/nginx/html
    depends_on:
      - "nginx-proxy"
      - "bonner"

The wall of text above is the opening of our docker-compose.yml which defines an nginx-proxy image which'll act as our load-balancer, and a second image called nginx-proxy-companion which handles requesting the HTTPS certificates from Let's Encrypt.

You can see the shared volumes between the two containers which allows them to interact. Volume are how state is maintained across containers.

Notice also how both have access to the docker.sock which is what the Docker clients use to talk to the Docker service, and therefore lets them interact with the host system and become self-aware of the other services.

Now we'll define our first service.

  bonner:
    build:
      context: "https://${GITHUB_USERNAME}:${GITHUB_AUTH_TOKEN}@github.com/moritonal/bonner.is.git"
    environment:
      VIRTUAL_HOST: bonner.is,tom.bonner.is
      LETSENCRYPT_HOST: bonner.is,tom.bonner.is
      LETSENCRYPT_EMAIL: [email protected]

The service above is named bonner, and the build line means that the docker-compose engine will understand it needs to build the service before deploying it. When docker-compose is told to deploy the server, it'll perform the following steps:

  • Download the code from the GitHub repo using a Username and Password stored in the .env file alongside this.
  • Build the Dockerfile (find the blog for it here) stored within that repo, resulting in an image. This would be considered the CI step from conventional systems.
  • Host the image into a container, setting three environment variables within it.
  • The nginx-proxy will detect a new container has started, and if it has a valid host, direct traffic towards it's sub-domain.
  • The nginx-proxy-companion will be notified of a new domain, and attempt will attempt to request a certificate from Let's Encrypt.

This means that the process of adding/linking a sub-domained service is wholly contained on the 7 lines above and an update to the depends_on from nginx-proxy-companion. Any changes to how the service host's itself is contained within it's own Dockerfile. Each service can choose how to host itself and what to use, such as the following:

maybe-chat-socket:
  build:
    context: "https://${GITHUB_USERNAME}:${GITHUB_AUTH_TOKEN}@github.com/moritonal/maybe-chat-server.git"
  env_file:
    - maybe-chat-server.env
  environment:
    VIRTUAL_HOST: maybe-chat-socket.bonner.is
    LETSENCRYPT_HOST: maybe-chat-socket.bonner.is
    LETSENCRYPT_EMAIL: [email protected]

This service is completely different to bonner, in that it's a node express server that sits at maybe-chat-socket.bonner.is. It's also a private repo, therefore requiring the GITHUB_AUTH_TOKEN to access, and it has an environment file which is passed in for it to use internally for settings.

If you view the full docker-compose.yml file you'll find 10 services hosted at bonner.is, all built and deployed using a single file. This get's even more fun when we take a look at how the github-integration services allows us to redepoy a service whenever it changes.

You can read that blog here.