DEV Community

Cover image for ✨ A practical guide to GitHub Actions: build & deploy a static 11ty website to remote virtual server after push
Vic Shóstak
Vic Shóstak

Posted on • Edited on

✨ A practical guide to GitHub Actions: build & deploy a static 11ty website to remote virtual server after push

Introduction

Hey, DEV community 🖖 It's a long time since I saw you last!

I want to share with you some great news from the CI/CD world: GitHub has finally released its automation tool for all users! Let's look at a comprehensive real-life example, that will help you understand and start working with GitHub Actions faster! 👍

📝 Table of contents

🤔 What's a GitHub Actions?

GitHub Actions makes it easy to automate all your software workflows, now with world-class CI/CD. Build, test, and deploy your code right from GitHub, make code reviews, branch management, and issue triaging work the way you want... and absolutely free for Open Source public repositories!

GitHub Actions

❤️ Why would you love it too?

  • Linux, macOS, Windows, ARM, and containers. Hosted runners for every major OS make it easy to build and test all your projects. Run directly on a VM or inside a container. Use your own VMs, in the cloud or on-prem, with self-hosted runners.
  • Matrix builds. Save time with matrix workflows that simultaneously test across multiple operating systems and versions of your runtime.
  • Any language. GitHub Actions supports Node.js, Python, Java, Ruby, PHP, Go, Rust, .NET and more. Build, test, and deploy applications in your language of choice.
  • Live logs. See your workflow run in realtime with color and emoji. It’s one click to copy a link that highlights a specific line number to share a CI/CD failure.
  • Built in secret store. Automate your software development practices with workflow files embracing the Git flow by codifying it in your repository.
  • Multi-container testing. Test your web service and its DB in your workflow by simply adding some docker-compose to your workflow file.

↑ Table of contents

📚 Preparation stage

Okay, well, I hope there are fewer questions now. So, what are we going to deploy and where? As you may have already understood from the title of the article, it will be:

  • 11ty (or Eleventy), as a static website generator
  • Droplet on DigitalOcean, as a remote virtual server
  • Ubuntu 18.04 LTS, as a server operating system

Let's take a project, like this, as a basis:

.
├── .eleventy.js
├── .gitignore
├── .github
│   └── workflows
│       └── ssh_deploy.yml
├── package.json
└── src
    ├── _includes
    │   ├── css
    │   │   └── style.css
    │   └── layouts
    │       └── base.njk
    ├── images
    │   └── logo.svg
    └── index.njk
Enter fullscreen mode Exit fullscreen mode

☝️ Tip: As usual, you can grab production ready project code from this GitHub repository → https://github.com/koddr/example-github-actions

Sounds easy, let's see how it really is 👀

↑ Table of contents

11ty

✅ 11ty aka Eleventy

Eleventy is a simpler static site generator, which was created to be a JavaScript alternative to Jekyll. It’s zero-config, by default, but has flexible configuration options. 11ty is not a JavaScript framework, that means zero boilerplate client-side JavaScript and works with multiple template languages:

template languages

Of all templates engines, I like to work with Nunjucks. It's very similar to jinja2, but supported by Mozilla. Also, I'd like all CSS styles to be optimized, minimized and included in the final HTML document. CleanCSS package will help me in this.

My working Eleventy config looks like this:

// .eleventy.js

const CleanCSS = require("clean-css"); // npm i --save-dev clean-css

module.exports = function (eleventyConfig) {
  // Copy all images to output folder
  eleventyConfig.addPassthroughCopy("src/images");

  // Optimized, minimized and included CSS to final HTML
  eleventyConfig.addFilter("cssmin", function (code) {
    return new CleanCSS({}).minify(code).styles;
  });

  return {
    dir: { 
      input: "src",                  // input folder name
      output: "dist",                // output folder name
    },
    passthroughFileCopy: true,       // allows to copy files to output folder
    htmlTemplateEngine: "njk",       // choose Nunjucks template engine
    templateFormats: ["njk", "css"],
  };
};
Enter fullscreen mode Exit fullscreen mode

Basic layout template:

<!-- src/_includes/layouts/base.njk -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{{ title }}</title>
    {% set css %}{% include "css/style.css" %}{% endset %}
    <style>
      {{ css | cssmin | safe }}
    </style>
  </head>
  <body>
    {{ content | safe }}
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Example index page:

<!-- src/index.njk -->

---
layout: layouts/base.njk
title: "Hello, World!"
---

<main>
  <section>
    <img src="/images/logo.svg" alt="logo" />
    <article>
      <h1>{{ title }}</h1>
      <p>It works via GitHub Actions 👍</p>
    </article>
  </section>
</main>
Enter fullscreen mode Exit fullscreen mode

And CSS styles:

/* src/_includes/css/style.css */

:root {
  --font-face: sans-serif;
  --font-size: 18px;
  --black: #444444;
  --gray: #fafafa;
}

* {
  box-sizing: border-box;
}

body {
  font-family: var(--font-face);
  font-size: var(--font-size);
  color: var(--black);
  background-color: var(--gray);
}

h1 {
  font-size: calc(var(--font-size) * 2.5);
  margin-bottom: var(--font-size);
}

main {
  display: grid;
  grid-template-columns: max-content;
  row-gap: 24px;
  align-items: center;
  justify-content: center;
  text-align: center;
}

main > * > img {
  width: 320px;
}
Enter fullscreen mode Exit fullscreen mode

↑ Table of contents

✅ Droplet on DigitalOcean

I don't think anybody's gonna be interested in watching yet another unusable in real life "Hello, World!" example, right? So, I decided to show you how to configure this remote virtual server configuration:

  1. Nginx with Brotli module and best practice config
  2. Redirect from www to non-www and from http to https
  3. Certbot with automatically renew SSL certificates for a domain
  4. UFW firewall with protection rules

And yes, it would be a crime in 2020 not to use Brotli (by Google) compression format on your web server! 😉

Create droplet

  • Enter to your DigitalOcean account

Don't have an account? Join DigitalOcean by my referral link (your profit is $100 and I get $25). This is my bonus for you! 🎁

  • Click to green button Create on top and choose Droplets
  • Choose Ubuntu 18.04 LTS, plan and droplet's region:

do droplet 1

  • Click to New SSH key button at the Authentication section:

do droplet 2

☝️ Tip: I recommend to create new SSH key for each new droplet, because it's more secure, than use same key for every droplets!

  • Follow instruction (on right at this form) and generate SSH key
  • Re-check droplet's options and click to Create Droplet
  • Go to Networking section (on left menu) and add your domain:

do droplet 3

  • Finally, add two A records to this domain (for @ and www)

Great! 👌 You're ready to setup your remote virtual server.

Setup remote virtual server

I won't bore you with the boring console commands listings. Because I have an amazing GitHub repository, which allows you to automate routine things by configuring GNU/Linux servers (by Ansible playbooks) 👇

GitHub logo koddr / useful-playbooks

🚚 Useful Ansible playbooks for easily deploy your website or webapp to absolutely fresh remote virtual server and automation many processes. Only 3 minutes from the playbook run to complete setup server and start it.

All I need to do is download all needed playbooks to my local machine and run it (with some extra vars):

# Configure VDS
ansible-playbook \
                  new_server-playbook.yml \
                  --user <USER> \
                  --extra-vars "host=<HOST>"

# Install Brotli module for Nginx
ansible-playbook \
                  install_brotli-playbook.yml \
                  --user <USER> \
                  --extra-vars "host=<HOST>"

# Get SSL for domain
ansible-playbook \
                  create_ssl-playbook.yml \
                  --user <USER> \
                  --extra-vars "host=<HOST> domain=<DOMAIN>"
Enter fullscreen mode Exit fullscreen mode

Now, you just have to wait 5-10 minutes. As a result, all tasks (I mentioned above) have been successfully resolved.

There you go! It just works!

↑ Table of contents

✅ Private SSH key

Let's create a private SSH key for our virtual server, that will allow us to login without entering the root password. Also, you will need this key to configure GitHub Actions for deploy via SSH (it will be covered later in this article).

☝️ Please note: creating key must be done on your LOCAL computer!

  • Open terminal and run the following command:
ssh-keygen
Enter fullscreen mode Exit fullscreen mode

☝️ Tip: Windows users can install and use PuTTY for it.

  • You will be prompted to save and name the key. For general understanding, let's call it gha_rsa and place it in the folder ~/.ssh of your local computer (~/.ssh/gha_rsa)
  • Next, you will be asked to create and confirm a passphrase for the key
  • This will generate two files, called gha_rsa and gha_rsa.pub

Continue on your virtual server

  • Copy the contents of the gha_rsa.pub file (on local computer):
cat ~/.ssh/gha_rsa.pub
Enter fullscreen mode Exit fullscreen mode
  • Login to your remote virtual server and create a file ~/.ssh/authorized_keys with contents of the gha_rsa.pub file:
sudo nano ~/.ssh/authorized_keys
Enter fullscreen mode Exit fullscreen mode
  • Paste the SSH key to ~/.ssh/authorized_keys file
  • Hit Ctrl + O to save changes and Ctrl + X to exit from editor

Return to your local computer to complete the process

Okay! 👌 Let's immediately add the setting to using the SSH key for fast login to your remote server from local computer.

  • Open local SSH config file:
sudo nano ~/.ssh/config
Enter fullscreen mode Exit fullscreen mode
  • Add following content to bottom of ~/.ssh/config file (don't forget to replace SERVER_SHORTCUT, SERVER_IP and SERVER_USER with your own values):
Host SERVER_SHORTCUT
  HostName SERVER_IP
  Port 22
  User SERVER_USER
  IdentityFile ~/.ssh/gha_rsa
  AddKeysToAgent yes
Enter fullscreen mode Exit fullscreen mode
  • Hit Ctrl + O to save changes and Ctrl + X to exit from editor
  • Now, you can easily login to your remote virtual server, like this:
ssh SERVER_SHORTCUT
Enter fullscreen mode Exit fullscreen mode

🎉 Congratulations, you're now fully ready to start configure GitHub Actions!

↑ Table of contents

plugins for GitHub Actions

🔍 Helpful plugins for GitHub Actions

God bless the Open Source community! 🙏

Today, GitHub Actions marketplace already has a lot of helpful plugins (called in this place as action) for any case of life.

☝️ Please note: some of the actions have nothing to do with GitHub. Look the source code of such actions before using them!

But for now, only two will be of use to us:

GitHub logo TartanLlama / actions-eleventy

GitHub Action for generating a static website with Eleventy

GitHub logo appleboy / scp-action

GitHub Action that copy files and artifacts via SSH.

↑ Table of contents

⚙️ GitHub Actions config

Here's config file for our workflow. Take a look for yourself, but I'll explain some of the not quite obvious settings below.

# .github/workflows/ssh_deploy.yml

name: Deploy Eleventy via SSH

on:
  push:
    branches: [master]
  pull_request:
    branches: [master]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, 
      # so your workflow can access it
      - uses: actions/checkout@master

      # Build your static website with Eleventy
      - name: Build Eleventy
        uses: TartanLlama/actions-eleventy@master
        with:
          args: --output html
          install_dependencies: true

      # Copying files and artifacts via SSH
      - name: Copying files to server
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.REMOTE_HOST }}
          username: ${{ secrets.REMOTE_USER }}
          key: ${{ secrets.SSH_KEY }}
          passphrase: ${{ secrets.SSH_KEY_PASSPHRASE }}
          rm: true
          source: "html/"
          target: "${{ secrets.REMOTE_DIR }}"
Enter fullscreen mode Exit fullscreen mode

Basic settings

  • on — this setting tells you what to do to run workflow (in our case, it will run when you make push or pull request to the master branch of your repository)
  • runs-on — allows you to define the system on which workflow will be launched (in our case, it latest Ubuntu)
  • steps — all tasks (steps) of our GitHub Actions workflow will be defined in this section and will be executed one after another (downright).

Settings for Build Eleventy step

  • args: --output html — define an output folder to specify in SSH copy settings a correct folder, that corresponds to a folder on the remote server (in our case, it's /var/www/<domain>/html).
  • install_dependencies: true — since we use a third-party NPM package clean-css, this setting allow workflow to build project with these dev-dependencies in mind

Settings for Copying files to server step

  • rm: true — to maintain cleanliness, I recommend enabling this setting so that the destination folder on the remote server is completely cleaned before downloading new files
  • source: "html/" — defines the folder, that will be uploaded on the remote server (the mechanism of effective data compression will be used, so the uploading process will be as fast as possible... even on large projects)
  • target: "${{ secrets.REMOTE_DIR }}" — full path to the folder on the remote server, where the files from source will be uploaded

🤔 But wait! What are ${{ secrets }} variables and where will they come from? Don't worry, now you'll understand everything. Just keep reading further.

💭 Understanding the GitHub secrets

Secrets are environment variables, that are encrypted and only exposed to selected actions. Anyone with collaborator (access to your repository) can use these secrets only in a workflow as vars, like ${{ secrets.MY_SECRET }}.

Go to Settings and next to Secrets section in your repository:

GitHub secrets 1

You can create a new secret, by clicking New secret button:

GitHub secrets 2

Please, create the same names secrets, but with your own values:

  1. REMOTE_DIR — remote folder would be /var/www/<domain>/html (don't forget to define your domain, instead of <domain> placeholder)
  2. REMOTE_HOST — your remote virtual server IP address
  3. REMOTE_USER — name of remote virtual server user, in the settings of which (~/.ssh/authorized_keys), we had previously added a PUBLIC part (~/.ssh/gha_rsa.pub) of the SSH key
  4. SSH_KEY — the contents of a PRIVATE part (~/.ssh/gha_rsa) of the SSH key, that we generated in Private key for SSH section of this article
  5. SSH_KEY_PASSPHRASE — the passphrase of the SSH key, that we entered, when generating the SSH key

↑ Table of contents

🚀 Deploy to server via SSH

Just git push changes to your repository, wait for GitHub Actions and catch success status of the running job (at Actions menu):

Deploy to server via SSH

Okay! 🔥 Visit to your brand new 11ty website:

final result

↑ Table of contents

💬 Questions for better understanding

  1. Why is it considered good practice to use GitHub Secrets?
  2. What happens, if you do not specify the install_dependencies setting in the GitHub Action config for Eleventy build step?
  3. Why do you need a step, that uses action actions/checkout@master?
  4. How can you change the name for a step? And for the workflow?
  5. How many steps can you set in the GitHub Action config? Find the limits on the Internet by yourself.
  6. What happens (or doesn't happen), if rm is set to false in the settings for a step of copying files to the remote virtual server via SSH?
  7. How easy is it now to deploy new servers with script to automate, which I gave in the article in section Droplet on DigitalOcean?
  8. Can you configure the triggering action by scheduler (for example, by CRON)? Find answer in the GitHub Actions docs.

↑ Table of contents

✏️ Exercises for independent execution

  • Try to repeat everything you have seen in the article with your project. Please, write about your results in the comments to this article!
  • Find interesting actions in the GitHub Actions marketplace and test them.

↑ Table of contents

Photos/Images by

  • GitHub Actions promo website (link)
  • 11ty website (link)
  • DigitalOcean dashboard (link)
  • GitHub repository settings (link)
  • True web artisans snippets-deploy repository (link)

P.S.

If you want more articles (like this) on this blog, then post a comment below and subscribe to me. Thanks! 😻

❗️ You can support me on Boosty, both on a permanent and on a one-time basis. All proceeds from this way will go to support my OSS projects and will energize me to create new products and articles for the community.

support me on Boosty

And of course, you can help me make developers' lives even better! Just connect to one of my projects as a contributor. It's easy!

My main projects that need your help (and stars) 👇

  • 🔥 gowebly: A next-generation CLI tool that makes it easy to create amazing web applications with Go on the backend, using htmx, hyperscript or Alpine.js and the most popular CSS frameworks on the frontend.
  • create-go-app: Create a new production-ready project with Go backend, frontend and deploy automation by running one CLI command.

Other my small projects: yatr, gosl, json2csv, csv2api.

Top comments (14)

Collapse
 
ravgeetdhillon profile image
Ravgeet Dhillon • Edited

Hi. Can you please tell what is SERVER_SHORTCUT?

Collapse
 
koddr profile image
Vic Shóstak • Edited

Hello. It's name of your SSH connection.

For example, I always use this one:

Host digitalocean_myproject_server
...
Enter fullscreen mode Exit fullscreen mode

And, next on console to enter:

ssh digitalocean_myproject_server
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ravgeetdhillon profile image
Ravgeet Dhillon

Awesome. Thanks for a great tutorial!

Collapse
 
koonfoon profile image
koonfoon

Great tutorial, thank you.
But can the "passphrase:" be empty?
I has a ssh key that do not need passphrase.

Collapse
 
koddr profile image
Vic Shóstak

Hi! Good question, but IDK.
Try to ask this here: github.com/appleboy/scp-action

Collapse
 
koonfoon profile image
koonfoon

I had tried myself.
it still work.
👍

Collapse
 
sm0ke profile image
Sm0ke

Super nice. Thanks for sharing.

Collapse
 
koddr profile image
Vic Shóstak

No problem! Thank you for reading 😎

Collapse
 
wobsoriano profile image
Robert

Awesome! Bookmarking this.

Collapse
 
koddr profile image
Vic Shóstak

Thanks, you're welcome 👍

Collapse
 
davidyaonz profile image
David Yao

Thanks for sharing.

Collapse
 
koddr profile image
Vic Shóstak

You're welcome! 👍

Collapse
 
mariusty profile image
mariusty

Great!
Thanks for such a detailed tutorial

Collapse
 
koddr profile image
Vic Shóstak

Thanks for reply! You're welcome 😉