Hosting static sites with Traefik

Hosting Static Sites with Traefik and Static Web Server

Traefik is amazing to host complex services like with containers. On the other hand it’s harder than you’d think to host a simple static html site. I wanted to share my current approach that is based on Static Web Server Project.

Static Web Server (SWS)

Static Web Server (or SWS abbreviated) is a simple and really fast web server with the goal to serve static web files or assets. The tiny docker image is only 4 MB with a small memory footprint. We can therefore afford to run a container for each static site.

Architecture

On the server we set up all static sites in one folder called static-sites. As we run the SWS with docker-compose we add the docker-compose.yml to this folder too. The following is an example setup with two static sites on seperate domains.

| static-sites
|
| docker-compose.yml
| - domain1.example.org
   | - index.html
| - domain2.example.org
   | - index.html
   | - fonts
      | - Open-Dyslexic.odf

For each domain we add a folder. I like to name them by the domain name but this is not necessary (just remember the volume in the docker-compose.yml too).

With this done we can now fill the appropriate information in the docker-compose.yml. Copy the following and replace domain1.example.org, domain2.example.org, site-one and site-two.

version: "3.3"

services:
  domain1.example.org:
    image: joseluisq/static-web-server:2
    container_name: "domain1.example.org"
    environment:
      # Note: those envs are customizable but also optional
      - SERVER_PORT=8080
      - SERVER_ROOT=/public
      - SERVER_LOG_LEVEL=info
    volumes:
      - ./domain1.example.org:/public
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      - "traefik.http.routers.site-one.rule=Host(`domain1.example.org`)"
      - "traefik.http.routers.site-one.service=site-one"
      - "traefik.http.routers.site-one.entrypoints=web-secure"
      - "traefik.http.routers.site-one.tls=true"
      - "traefik.http.routers.site-one.tls.certResolver=default"
      - "traefik.http.services.site-one.loadbalancer.server.port=8080"
    networks:
      - traefik

  domain2.example.org:
    image: joseluisq/static-web-server:2
    container_name: "domain2.example.org"
    environment:
      # Note: those envs are customizable but also optional
      - SERVER_PORT=8080
      - SERVER_ROOT=/public
      - SERVER_LOG_LEVEL=info
    volumes:
      - ./domain2.example.org:/public
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      - "traefik.http.routers.site-two.rule=Host(`domain2.example.org`)"
      - "traefik.http.routers.site-two.service=site-two"
      - "traefik.http.routers.site-two.entrypoints=web-secure"
      - "traefik.http.routers.site-two.tls=true"
      - "traefik.http.routers.site-two.tls.certResolver=default"
      - "traefik.http.services.site-two.loadbalancer.server.port=8080"
    networks:
      - traefik

networks:
  traefik:
    name: traefik
    external: true

This assumes traefik runs in a docker-network called traefik. This network must already exist.

As a last step add at least a index.html in the appropriate folder. Then you can start the webserver with docker-compose up. Add -d to run it in the background.

Deploying static sites

Deploying files manually (via Filezilla, scp or rsync) is not something I like to do. I therefore normally set up a CI job to automatically deploy the site when I push a new commit to GitHub, either via GitHub Actions or my Woodpecker CI instance.

I order to do that I

  • create a new user on the server, specifically for that purpose (one per site). The command is useradd USERNAME -m
  • create a SSH key without a password ssh-keygen -t ed25519 -a 100 -C "COMMENT" -f FILENAME
  • copy the public key that was just created at FILENAME.pub on the server in the textfile /home/USERNAME/.ssh/authorized_keys
  • Add the private key to the secrets of your CI

A typical CI configuration will look like this with a static site and GitHub Actions

name: Deploy Production Website via SSH

on:
  push:
    branches: [main]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v1
      - name: Deploy to Server
        uses: easingthemes/ssh-deploy@main
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          ARGS: "-rltgoDzvO --delete"
          SOURCE: ""
          REMOTE_HOST: ${{ secrets.REMOTE_HOST }}
          REMOTE_USER: ${{ secrets.REMOTE_USER }}
          TARGET: ${{ secrets.REMOTE_PRODUCTION_TARGET }}
          EXCLUDE: ".github/, .gitignore"

or this, when using HUGO and Woodpecker CI

---

pipeline:
  build:
    image: klakegg/hugo 
    commands:
      - hugo

  deploy:
    image: appleboy/drone-scp
    settings:
      strip_components: 1
      host:
        - example.org
      username: moanos
      target: /home/USERNAME/static-sites/
      source: public/
      key:
        from_secret: ssh_key

not manually put the files on the server

Student of Medical Informatics, Developer, He/Him