✨ A practical guide to GitHub Actions: build & deploy a static 11ty website to remote virtual server after push
Tags: webdev, devops, tutorial, github
Published: 2020-06-01 19:56:32

## 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! 👍

<a name="toc"></a>
### 📝 Table of contents

- [What's GitHub Actions?](#ch-1)
  - [Why would you love it?](#ch-1-1)
- [Preparation stage](#ch-2)
  - [11ty aka Eleventy](#ch-2-1)
  - [Droplet on DigitalOcean](#ch-2-2)
  - [Private SSH key](#ch-2-3)
- [Helpful plugins for GitHub Actions](#ch-3)
- [GitHub Actions config](#ch-4)
  - [Understanding the GitHub secrets](#ch-4-1)
- [Deploy to remote server via SSH](#ch-5)
- [Questions for better understanding](#ch-6)
- [Exercises for independent execution](#ch-7)

<a name="ch-1"></a>
## 🤔 What's a GitHub Actions?

[`GitHub Actions`](https://github.com/features/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](https://dev-to-uploads.s3.amazonaws.com/i/2cwrmst5la9ylq3i1mqo.png)

<a name="ch-1-1"></a>
### ❤️ 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](#toc)

<a name="ch-2"></a>
## 📚 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`](https://www.11ty.dev/) (or Eleventy), as a static website generator
- Droplet on [`DigitalOcean`](https://m.do.co/c/b41859fa9b6e), as a remote virtual server
- Ubuntu 18.04 LTS, as a server operating system

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

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

> ☝️ **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](#toc)

![11ty](https://dev-to-uploads.s3.amazonaws.com/i/yp5g3mh90834nt5bwpqc.png)

<a name="ch-2-1"></a>
### ✅ 11ty aka Eleventy

Eleventy is a simpler static site generator, which was created to be a JavaScript alternative to [`Jekyll`](https://jekyllrb.com/). 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](https://dev-to-uploads.s3.amazonaws.com/i/rpvplqt2pa0q97nuwo0b.png)

Of all templates engines, I like to work with [`Nunjucks`](https://github.com/mozilla/nunjucks). It's very similar to [`jinja2`](https://jinja.palletsprojects.com), but supported by Mozilla. Also, I'd like all CSS styles to be optimized, minimized and included in the final HTML document. [`CleanCSS`](https://github.com/jakubpawlowicz/clean-css) package will help me in this.

My working Eleventy config looks like this:

```js
// .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"],
  };
};
```

Basic layout template:

```html
<!-- 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>
```

Example index page:

```html
<!-- 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>
```

And CSS styles:

```css
/* 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;
}
```

[↑ Table of contents](#toc)

<a name="ch-2-2"></a>
### ✅ 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`](https://github.com/google/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](https://m.do.co/c/b41859fa9b6e)** (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](https://dev-to-uploads.s3.amazonaws.com/i/cublvejcpnlwyggfr3y3.png)

- Click to _New SSH key_ button at the _Authentication_ section:

![do droplet 2](https://dev-to-uploads.s3.amazonaws.com/i/p59lz5qdppban6j9fsd5.png)

> ☝️ **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](https://dev-to-uploads.s3.amazonaws.com/i/75106k06ivgx271nej7x.png)

- 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 truewebartisans/useful-playbooks no-readme %}

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

```bash
# 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>"
```

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](#toc)

<a name="ch-2-3"></a>
### ✅ 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:

```console
ssh-keygen
```

> ☝️ **Tip**: Windows users can install and use [`PuTTY`](https://www.digitalocean.com/docs/droplets/how-to/add-ssh-keys/create-with-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):

```console
cat ~/.ssh/gha_rsa.pub
```

- Login to your remote virtual server and create a file `~/.ssh/authorized_keys` with contents of the `gha_rsa.pub` file:

```console
sudo nano ~/.ssh/authorized_keys
```

- 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:

```console
sudo nano ~/.ssh/config
```

- 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):

```console
Host SERVER_SHORTCUT
  HostName SERVER_IP
  Port 22
  User SERVER_USER
  IdentityFile ~/.ssh/gha_rsa
  AddKeysToAgent yes
```

- 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:

```console
ssh SERVER_SHORTCUT
```

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

[↑ Table of contents](#toc)

![plugins for GitHub Actions](https://dev-to-uploads.s3.amazonaws.com/i/3zwom4tui8zynm88tlwt.png)

<a name="ch-3"></a>
## 🔍 Helpful plugins for GitHub Actions

God bless the **Open Source** community! 🙏

Today, GitHub Actions [marketplace](https://github.com/marketplace?type=actions) 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 TartanLlama/actions-eleventy no-readme %}
{% github appleboy/scp-action no-readme %}

[↑ Table of contents](#toc)

<a name="ch-4"></a>
## ⚙️ 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.

```yaml
# .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 }}"
```

#### 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.

<a name="ch-4-1"></a>
### 💭 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](https://dev-to-uploads.s3.amazonaws.com/i/oyov95wp8jppmxx8lcoy.png)

You can create a new secret, by clicking _New secret_ button:

![GitHub secrets 2](https://dev-to-uploads.s3.amazonaws.com/i/b9xa98afkglh5cpw0q93.png)

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](#ch-2-3) 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](#toc)

<a name="ch-5"></a>
## 🚀 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](https://dev-to-uploads.s3.amazonaws.com/i/u5y9v8jdp1ejys5rcpzl.png)

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

![final result](https://dev-to-uploads.s3.amazonaws.com/i/mi4f4y99cmp3bt2uu9c0.png)

[↑ Table of contents](#toc)

<a name="ch-6"></a>
## 💬 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`](#ch-2-2)?
8. Can you configure the triggering action by scheduler (for example, by `CRON`)? _Find answer in the GitHub Actions [docs](https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#onschedule)_.

[↑ Table of contents](#toc)

<a name="ch-7"></a>
## ✏️ 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](#toc)

<a name=""></a>
## Photos/Images by

- GitHub Actions promo website ([link](https://github.com/features/actions))
- 11ty website ([link](https://www.11ty.dev))
- DigitalOcean dashboard ([link](https://www.digitalocean.com))
- GitHub repository settings ([link](https://github.com))
- True web artisans snippets-deploy repository ([link](https://github.com/truewebartisans/snippets-deploy))

<a name=""></a>
## P.S.

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

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 projects that need your help (and stars) 👇

- 🔥 [gowebly][gowebly_url]: A next-generation CLI tool for easily build amazing web applications with Go on the backend, using htmx & hyperscript and the most popular atomic/utility-first CSS frameworks on the frontend.
- ✨ [create-go-app][cgapp_url]: Create a new production-ready project with Go backend, frontend and deploy automation by running one CLI command.
- 🏃 [yatr][yatr_url]: Yet Another Task Runner allows you to organize and automate your routine operations that you normally do in Makefile (or else) for each project.
- 📚 [gosl][gosl_url]: The Go Snippet Library provides snippets collection for working with routine operations in your Go programs with a super user-friendly API and the most efficient performance.
- 🏄‍♂️ [csv2api][csv2api_url]: The parser reads the CSV file with the raw data, filters the records, identifies fields to be changed, and sends a request to update the data to the specified endpoint of your REST API.
- 🚴 [json2csv][json2csv_url]: The parser can read given folder with JSON files, filtering and qualifying input data with intent & stop words dictionaries and save results to CSV files by given chunk size.

<!-- Footer links -->

[gowebly_url]: https://github.com/gowebly
[cgapp_url]: https://github.com/create-go-app
[yatr_url]: https://github.com/koddr/yatr
[gosl_url]: https://github.com/koddr/gosl
[csv2api_url]: https://github.com/koddr/csv2api
[json2csv_url]: https://github.com/koddr/json2csv