G'day, I'm Aidan and I like to make things, almost as much as I like to break things. In this blog you can expect to find writeups of various home/personal projects, as well as hackthebox/ctf writeups. Hope you enjoy!

The Raspberry Pi Project: Part 2 - Web Server

So as I mentioned in my previous post, I wanted to use my Raspberry Pi as a webserver so that I could play around with web development projects. More specifically, I wanted to run NGINX, with autogenerated SSL certs from Let's Encrypt, and I wanted a Dynamic DNS address so that I didn't have to deal with changing IP addresses provided by my ISP. Oh, and I wanted this to all be described by one simple docker-compose.yml file.

Installing Prerequisites

First things first, you'll need to get a Raspberry Pi up and running with Raspbian. There's plenty of guides on this so if you have know idea, do some googling.

Next, we need to install Docker & Docker Compose. We'll start with Docker.

curl -fsSL get.docker.com -o get-docker.sh && sh get-docker.sh

Next, I'd recommend adding yourself to the Docker group so that you don't have to use sudo every time you want to run a Docker command.

sudo usermod -aG docker pi

Next, update your packages.

sudo apt update && sudo apt upgrade

Now we shall enable the Docker service so that it automatically starts up on boot.

sudo systemctl enable docker

Last but not least, let's install docker-compose

sudo apt install docker-compose

T o make sure everything is working properly, let's run the infamous hello-world container!

docker run hello-world

You should see Docker look for the hello-world container locally, fail to find it, pull it from the Docker Registry, run it and then display the result in the terminal! If you see this, your Docker installation is working!

Docker & Docker Compose

So what is Docker? Well, I won't regurgitate what 1000 other blogs have already explained far better than I ever could, so instead, here's the TLDR:

Docker lets you package apps and all their dependencies into 'containers' that can be downloaded and run on any platform that supports Docker.

Docker Compose?

Docker compose lets you write down how to run a container(s), so that you don't have to write it all out in the command line every time you wanna boot it up.

It's more complicated than that, but if you want to find out more, I won't stop you.

So rather then explain how Docker Compose files work in general, I figured I'd walk you through my docker-compose.yml file, because I learn best from examples!

DuckDNS

So here is a snippet of my compose file that I use to build my web server. This part in particular is used to run my DuckDNS client, so that my dynamic DNS address will always stay up to date. It's a fairly straight forward example, so let's take a look.

version: '3'
services:
  duckdns:
    image: linuxserver/duckdns:arm32v7-latest
    container_name: duckdns
    environment:
      TZ: "Australia/Brisbane"
      TOKEN: "${DUCKDNS_TOKEN}"
      SUBDOMAINS: 'deluqs'
      PUID: 1000
      GUID: 1000
      LOG_FILE: 'true'
    volumes:
      - './duckdns/config:/config'
    restart: unless-stopped

To start with, I declare which version of docker-compose I am using. At the time of writing, 3 is the latest, so that is what I am using. Next I declare the services I want to run, which in this case, is just one service I've called 'duckdns'.

I then tell Docker to use the 'linuxserver/duckdns:arm32v7-latest' image, which just describes which container I want to run. More specifically, look for the 'duckdns' image by the Docker Hub account 'linuxserver', with the tag 'arm32v7-latest'. Note that it is important to declare the use of the 'arm32v7-latest' tag, since the Raspberry Pi 4 runs on arm32v7 architecture. Remember this, because I will bring it up again later.

Next I give the container the name 'duckdns', as well as a bunch of environment variables. To see the explanation behind each of these environment variables, see the linuxserver/duckdns documentation. It is also worth noting that in the same directory as the docker-compose.yml file, I have a .env file that contains my DUCKDNS_TOKEN, which will get substituted in at runtime.

Next, I tell Docker to mount the /config folder within the duckdns container to the folder ./duckdns/config on my Pi. This allows me to easily see the duck.log file from the host, as it gets written to /config/duck.log within the container.

Finally, I make sure that the container always restarts, unless I've manually stopped it. This makes sure that it will automatically start on boot.

NGINX

Next, the container I use to run NGINX.

version: '3'
services:
  duckdns:
    ...
  nginx-proxy:
    image: aidanstansfield/nginx-proxy:arm32v7
    container_name: nginx-proxy
    restart: unless-stopped
    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
      - conf:/etc/nginx/conf.d
      - dhparam:/etc/nginx/dhparam
    environment:
      DEFAULT_HOST: "deluqs.duckdns.org"

volumes:
  conf:
  vhost:
  html:
  dhparam:
  certs:

Now, there's a couple of differences between this and the DuckDNS example above. The most obvious one, is that it's a different image (duh). The image I'm using is a customized version of jwilder's nginx-proxy that I built myself, with support for the arm32v7 architecture.

NOTE: I plan on doing a blog post on why and how I built this container in the future, so keep your eyes peeled!

If you aren't familiar with jwilder's nginx-proxy, what it does is observe the Docker containers you run, and if any have the environment variable VIRTUAL_HOST, it will automagically setup an NGINX proxy and point it to that container!

For instance, say I spin up a container of some kind that serves web content out of port 80. If I add the environment variable VIRTUAL_HOST: "deluqs.duckdns.org" to that service, then NGINX will see that and setup the necessary config files to proxy any requests for the host "deluqs.duckdns.org" to port 80 within that container. See the pieces starting to come together?

The next difference with the docker-compose.yml is that I've declared some port bindings. These simply mean listen to this port on the host:pass it to this port in the container. So any traffic that hits port 80 on my Pi will be forwarded to the nginx-proxy container's port 80.

The final difference is the volumes! Notably, you will see that I've mounted a special /var/run/docker.sock:/tmp/docker.sock:ro volume, as well as some other named volumes like html:/usr/share/nginx/html. The first volume is special as it allows communication between the container and the docker daemon running on the host, and the :ro specifies it is read-only.

The named volume is similar to the ones we described earlier, but we're letting docker decide where to store the volume on the host, and we're giving it a name so that we may reference it later as you shall soon see. Note that named volumes need to be declared at the global level, as seen at the bottom of the compose file.

Let's Encrypt

Finally, the magic that turns SSL on for all nginx-proxy sites.

version: '3'
services:
  duckdns:
    ...
  nginx-proxy:
    ...
  letsencrypt-nginx-proxy-companion:
    image: aidanstansfield/docker-letsencrypt-nginx-proxy-companion:arm32v7
    container_name: le-nginx-proxy-companion
    restart: unless-stopped
    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
      - conf:/etc/nginx/conf.d
      - dhparam:/etc/nginx/dhparam
    environment:
      DEFAULT_EMAIL: "aidan.stansfield@gmail.com"
      NGINX_PROXY_CONTAINER: "nginx-proxy"

volumes:
  conf:
  vhost:
  html:
  dhparam:
  certs:

This has nothing you haven't seen before, but I'm including it for completeness sake. You'll notice that again, I'm using an image that I have built myself, based upon jrcs' docker-letsencrypt-nginx-proxy-companion, so that it supports the arm32v7 architecture.

Notice that we're using the same named volumes as the nginx-proxy service, and that allows these two seperate services to both access the same files.

Summary

So that's how I run my DuckDNS, NGINX and Let's Encrypt services on my Raspberry Pi with just a single docker-compose up. Note that I never have to touch NGINX config files, or deal with setting up SSL, all I need to do is pass in an environment variable or two into any container I spin up and it's all handled for me, how neat. Hope you found some of this useful!