Playing with automatic deployment

Today everything tend to move to cloud; still, I wanted to make the experiment in hosting my own blog on my own hardware, in my home network, and then setting up a fully automated deployment flow – very much like GitHub pages.
I am not sure this will work out forever, as the hardware (and bandwidth) I have is not really able to sustain an heavy traffic; still it is for sure a nice learning experience. Besides, the ability to have a reverse proxy on my lan is a useful alternative to tools like ngrok.

Here is how I did it.

Self-hosting the website

There are few prerequisites here: a server that can host the blog, a router with possibility to configure the firewall, and a domain.

Generally speaking, this is the stack I have settled on:

  • GoDaddy for the domain. I am not really fond of GoDaddy. but this was the only option for using a personalized email address in outlook.com.
    In my case, I had to add an A record for “rob” pointing to 77.237.25.148 in GoDaddy’s DNS management

  • A router with openwrt. I started with tomato, then migrated to openwrt. I like being in control of my network, and I like being forced to learn and know about networking.
    Here, I added a firewall rule forwarding http and https traffic to the internal server. To be honest, I am not totally happy with this, but in the end I am exposing only static content, so it should be OK.

  • A home server, with ubuntu and nginx. Nothing really to say here, but this home server has already caused me a lot of unintended work. The site configuration was very simple, something like

    server {
      server_name rob.liffredo.net;
      location / {
          root /data/www/rob.liffredo.net;
      }
    }
    
  • A static site, made with Hugo (see later for details).

  • Letsencrypt for certificates. I am literally in awe for how they have simplified the management of an ssl certificate; I still remember how hard and expensive it was in the past. Now? Just literally five minutes.

    sudo apt install certbot python3-certbot-nginx
    sudo certbot --nginx -d rob.liffredo.net
    

At this point, I was able to see the default nginx message when going to https://rob.liffredo.net. Success!

Generate static content

The end goal is to have a git-managed flow: commit the change, push, and see the updates on the site.
However, this requires some tinkering on the CI side, so I want first to secure the manual process.
I am using hugo, but the workflow should be about the same with any other system.

  1. Ensure to be in the correct timezone. Hugo will use it for displaying the time.

    timedatectl
    sudo timedatectl set-timezone Europe/Warsaw
    
  2. Install hugo

    sudo apt install hugo
    
  3. Create access token from https://github.com/settings/tokens Everybody should use 2FA; but this will make things a bit less comfortable. In this case, I have to create a new token that will be used only here. I gave it repo permissions, but I suspect it might be enough to give something less.

  4. Clone the repo, using https (ssh won’t be available later when moving to automatic deployment; we could update the origin, but it’s much easier to use https immediately):

    git clone https://github.com/rliffredo/roblog.git
    

    passing the normal username, and the token as a password.

  5. The themes are added as submodules; as such, they need to be initialized.

    git submodule init
    git submodule update
    
  6. Publish the site, and then copy on the destination (from directory with data)

    hugo --cleanDestinationDir -d ../www/rob.liffredo.net
    
  7. Check https://rob.liffredo.net

Automatic deploy

The general idea is quite simple:

  • Get notification from github on each push using a webhook
  • The webhook on our side will pull the repo and then build it again with hugo

I am going to use webhook to create an endpoint that will be sitting behind nginx, which will be in this case acting as a reverse proxy.
The endpoint will be working in a service and with its own user.
Note that the version available for ubuntu/debian is too old and has issues with secret in payload, so it’s better to download one from github.

The user will need to have its own home directory, because that is where we are going to store credentials for pulling the repo. The credentials are stored only once, and separated from the code that could (and should!) be committed in a repository.

Troubleshooting

If something goes wrong, these tools can be useful for troubleshooting the webhook:

  • netcat is a very nice tool to do anything on tcp/udp. In this case, it is possible to inspect the payload sent by github by using nc -l 9000
  • payload is hashed using sha. shasum is a utility to check that out, even if it might be a bit too much work.
  • curl is another swiss knife for everything related to http; in this case it is possible to test the basic of the connectivity using something like curl -X POST -k -i 'https://rob.liffredo.net/_hooks/deploy-roblog'
  • Permissions on all files for both directories (sources and target) must be granted to the deployment user, or there will be issues in update and deployment.

Step-by-step instructions

Here is a list of all the command actually used for the process described above; the actual content might need to be adjusted for a different setup or a different distro from what I used (Ubuntu).

# Create user
sudo adduser --system --disabled-login --group deployment
# Change ownership of the repo to the user, and ensure that password is stored
cd /data/roblog
sudo chown -R deployment:deployment .
sudo chown -R deployment:deployment /data/www/rob.liffredo.net
sudo -u deployment git config credential.helper store
sudo -u deployment git pull
# Setup webhook (see later for details)
sudo mkdir /data/webhook
sudo wget https://github.com/adnanh/webhook/releases/download/2.8.0/webhook-linux-amd64.tar.gz
sudo tar -zxvf webhook-linux-amd64.tar.gz webhook-linux-amd64/webhook
sudo mv webhook-linux-amd64/webhook /data/webhook
sudo rm -rd webhook-linux-amd64
sudo rm webhook-linux-amd64.tar.gz
sudo vim /data/webhook/deploy_roblog.sh
sudo chmod +x /data/webhook/deploy_roblog.sh
sudo vim /data/webhook/hooks.json
sudo vim /lib/systemd/system/webhooks.service
sudo systemctl enable webhooks
# Setup reverse proxy
sudo vim /etc/nginx/sites-available/rob.liffredo.net
sudo nginx -t
sudo systemctl restart nginx.service
# Test webhook - perform some deploy
sudo -u deployment /usr/bin/webhook -hooks /data/webhook/hooks.json --verbose
# If everything is successful, kill the testing process and start the service
sudo systemctl start webhooks
  • deploy_roblog.sh

    #!/bin/sh
    
    git pull -f -ff -q
    hugo --cleanDestinationDir -d ../www/rob.liffredo.net
    
  • hooks.json:

    [
      {
        "id": "deploy-roblog",
        "execute-command": "/data/webhook/deploy_roblog.sh",
        "command-working-directory": "/data/roblog",
        "pass-arguments-to-command":
        [
          {
            "source": "payload",
            "name": "head_commit.id"
          },
          {
            "source": "payload",
            "name": "pusher.name"
          },
          {
            "source": "payload",
            "name": "pusher.email"
          }
        ],
        "trigger-rule":
        {
          "and":
          [
            {
              "match":
              {
                "type": "payload-hmac-sha256",
                "secret": "some_secret",
                "parameter":
                {
                  "source": "header",
                  "name": "X-Hub-Signature-256"
                }
              }
            },
            {
              "match":
              {
                "type": "value",
                "value": "refs/heads/main",
                "parameter":
                {
                  "source": "payload",
                  "name": "ref"
                }
              }
            }
          ]
        }
      }
    ]
    
  • webhook.service

    [Unit]
    Description=Webhooks server
    After=network.target
    
    [Service]
    Type=simple
    User=deployment
    Group=deployment
    WorkingDirectory=/data/webhook
    ExecStart=/usr/bin/webhook -hooks /data/webhook/hooks.json
    Restart=on-abort
    
    [Install]
    WantedBy=multi-user.target