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 • Updated on

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


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 →

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

↑ Table of contents


✅ 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

  // 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">
    <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 %}
      {{ css | cssmin | safe }}
    {{ content | safe }}
Enter fullscreen mode Exit fullscreen mode

Example index page:

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

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

    <img src="/images/logo.svg" alt="logo" />
      <h1>{{ title }}</h1>
      <p>It works via GitHub Actions 👍</p>
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 truewebartisans / 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:
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

Continue on your virtual server

  • Copy the contents of the file (on local computer):
cat ~/.ssh/
Enter fullscreen mode Exit fullscreen mode
  • Login to your remote virtual server and create a file ~/.ssh/authorized_keys with contents of the 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):
  HostName SERVER_IP
  Port 22
  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:
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

    branches: [master]
    branches: [master]

    runs-on: ubuntu-latest
      # 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
          args: --output html
          install_dependencies: true

      # Copying files and artifacts via SSH
      - name: Copying files to server
        uses: appleboy/scp-action@master
          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/ 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)


If you want more — write a comment below & follow me. Thx! 😘

Top comments (14)

ravgeetdhillon profile image
Ravgeet Dhillon • Edited

Hi. Can you please tell what is SERVER_SHORTCUT?

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
ravgeetdhillon profile image
Ravgeet Dhillon

Awesome. Thanks for a great tutorial!

koonfoon profile image

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

koddr profile image
Vic Shóstak

Hi! Good question, but IDK.
Try to ask this here:

koonfoon profile image

I had tried myself.
it still work.

sm0ke profile image

Super nice. Thanks for sharing.

koddr profile image
Vic Shóstak

No problem! Thank you for reading 😎

wobsoriano profile image

Awesome! Bookmarking this.

koddr profile image
Vic Shóstak

Thanks, you're welcome 👍

davidyaonz profile image
David Yao

Thanks for sharing.

koddr profile image
Vic Shóstak

You're welcome! 👍

mariusty profile image

Thanks for such a detailed tutorial

koddr profile image
Vic Shóstak

Thanks for reply! You're welcome 😉