Hosting a Simple Website with Docker Compose

Published: 2022-09-06

For self-hosting, Docker Compose is more than capable enough for self-hosting. No need for Kubernetes or other elaborate container orchestration tools. Here, I'll show how to setup a basic website without and with Docker Compose.

Without Docker Compose

First, let's assume that we don't know that Docker exists and let's setup a webserver. The webserver will host a simple website containing the two words:

Hello, World!

To do so, go to your cloud or home server and setup a small Ubuntu server. Next, log in to the server via SSH:

ssh -i ~/.ssh/<identity file> root@<ip of the server>

where the <identity file> points to some SSH key. If you just created a new SSH key, then don't forget to store them in your password manager to avoid losing access to the server.

Then, we can install a simple web server. For this example, we'll use Caddy. To install Caddy, we can follow the installation instructions to add a new keyring and a new apt source:

$ apt update

$ apt install -y debian-keyring debian-archive-keyring apt-transport-https

$ curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg

$ curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list

$ apt update

$ apt install caddy

After that whole process, we can continue by doing some real work and create a Caddyfile and an index.html. The Caddyfile specifies the webserver configuration:

:80 {
    root * /public
    file_server
}

This says that the folder /public contains our website. Next, index.html specifies our web page:

<!doctype html>
    <html lang="en">
    <body>
        Hello, World!
    </body>
</html>

Write this file to /public/index.html. Next, tell the running server to reload the configuration:

$ caddy adapt --config /path/to/Caddyfile

If you now go to http://<ip of the server> in your browser, it should show "Hello, World!" 🥳🎉. If that doesn't happen yet, force restart the Caddy server via:

$ caddy stop

$ caddy start

Although this works, there are three things which will cause problems when the setup becomes more complex. Firstly, the Caddyfile required us to specify to the full path to the files for the website. In some cases, we want to make this path variable, that is, depending on other things. In other cases, tools don't even allow changing such paths. That's why it would be much better if we could make some "fake" path which is only visible to the Caddy server. Then, if two services want to read, say, "/data/index.html", then we can still make it possible to make this two different paths. Secondly, notice that installing Caddy involed quite some steps. What if you want to update Caddy or what if you have more involved setups with multiple applications? Then, you would need to install those all separately which is not nice. Thirdly, the setup instructions are platform dependent. Installing Caddy on an Ubuntu system will look different than on a Mac. As a solution for all these problems, Docker containers were created.

Using Docker Compose

Docker images are essentially snapshots of small operating systems which include their own file system. Since the file system is locked into the image, the installed software is too. Next, a Docker container is created by downloading a Docker image and running it. That will make a copy of the container and allow modifications to the contents. For example, you can download a Caddy image from the internet and run Caddy by running a container based on that image. There are many places offering ready-to-use Docker images with popular software pre-installed. To see examples of such software, take a look around at Docker Hub.

So, the only thing that we need to do to run Caddy is to take the right image. Let's start again with a fresh Ubuntu 22.04 installation. (Please always follow the official installation instructions from docker.com for production environments.) We can install Docker via their convienience script:

$ curl -fsSL https://get.docker.com -o get-docker.sh

$ sh ./get-docker.sh

and create a Docker Compose file docker-compose.yml:

version: '3'

services:
  caddy:
    image: 'caddy/caddy:2.5.1-alpine'
    ports:
        - '80:80'

If you now run the following, you'll see that an Docker image containing Caddy will be downloaded:

$ docker compose up

Next, if you go to http://<ip of the server> in the browser, then you'll see the default Caddy page. This works because we've specified a service with a Caddy image from Docker Hub. Secondly, the ports part specifies that Docker should open port 80 at the host (the server) and forward that to port 80 inside the Docker container. Port 80 is the default port for HTTP, so when we opened http://<ip of the server> in the browser, then the browser communicated via the server via port 80.

Let's continue and show "Hello, World!" again. To do so, take the server down again by pressing CTRL + C. Or, if you got disconnected from the server, use

$ docker compose down

After that, create a Caddy configuration file Caddyfile again:

:80 {
    root * /public
    file_server
}

and put it in the same folder as the docker-compose.yml. Also in the same folder, create the webpage index.html again:

<!doctype html>
    <html lang="en">
    <body>
        Hello, World!
    </body>
</html>

and update docker-compose.yml to:

version: '3'

services:
  caddy:
    image: 'caddy/caddy:2.5.2-alpine'
    ports:
      - '80:80'
    volumes:
      - './index.html:/public/index.html:ro'
      - './Caddyfile:/Caddyfile:ro'
    command: 'caddy run --config /Caddyfile'
    restart: 'unless-stopped'

If you now run the service:

docker compose up

and go to http://<ip of the server>, then you'll see "Hello, World!" again. This works because the Caddyfile and the index.html were inserted into the Docker container by Docker Compose. For example, "./index.html:/public/index.html:ro" means "place the file ./index.html in the Docker container at /public/index.html and give the container read-only (ro) permissions to the file". We could have just done "./index.html:/public/index.html", but it's generally good practise to explicitly state whether a file should only read (ro) or is also allowed to write (rw). The quotation marks are also not strictly necessary, but also good practise.

Next, the command "caddy run --config /Caddyfile" overwrites the default command that is built into the container. It runs Caddy with the Caddyfile that we've specified.

Finally, restart tells Docker Compose that the caddy service should be restarted unless we manually run docker compose stop. So, to try this out, smash the power off button of your server and start it again. As you will see, your website will come online again because Docker restarts Caddy automatically 💪.

The text is licensed under CC BY-NC-SA 4.0 and the code under Unlicense.