Quentin Delcourt

Deploy an Eleventy website using GitLab CI

Here's how to use GitLab CI to easily deploy a static website to a virtual private server.

When choosing a hosting solution for a static website, there are free and very powerful solutions like Netlify or GitHub. The former especially excels in providing very easy and lighting fast solution to deploy a website in matter of seconds.

But what if you want to develop on a good old VPS or metal server, going the self-hosted route? You might already need your server for other applications and use it as well for static content hosting.

Wen you used a few automation tools, there is no way back to that time where we were uploading manually our files using (S)FTP.

Fortunately, you can avoid the chores of manual upload by using a tool such as GitLab CI. This allows you to, for example, trigger a copy of the changed files whenever the master branch is updated. Most of GitLab CI configuration is not done via the GitLab app but rather using a YAML file (it seems most things these days are configured using YAML files).

This is the GitLab CI configuration (.gitlab-ci.yml) I created to automatically push changes to the server.

stages:
  - build
  - deploy

eleventy:
  stage: build
  image: node:lts-buster
  cache:
    key: "$CI_JOB_NAME"
    paths:
      - node_modules/
  script:
    - npm install
    - npm run build
  artifacts:
    paths:
      - dist/
  only:
    - master

rsync:
  stage: deploy
  image: instrumentisto/rsync-ssh
  script:
  - mkdir "${HOME}/.ssh"
  - echo "${SSH_HOST_KEY}" > "${HOME}/.ssh/known_hosts"
  - echo "${SSH_PRIVATE_KEY}" > "${HOME}/.ssh/id_rsa"
  - chmod 700 "${HOME}/.ssh/id_rsa"
  - rsync -avH dist/* -e ssh ${SSH_USER}@${SSH_HOST}:${WEBROOT_PATH}
  only:
  - master

There are two main things happening here:

  1. Eleventy generates the static files.
  2. The changes are copied to the server.

Defining the order of stages

stages:
  - build
  - deploy

The block above will tell GitLab CI to execute all jobs from the build stage first, then the deploy stage. You can declare several jobs in the configuration file and assign each of these jobs to a particular stage.

Build stage

The build stage is for installing all node.js dependencies and run Eleventy to generate the static files.

We define a "eleventy" job and assign it to the "build" stage:

eleventy:
  stage: build

image is to attach a specific container image to the job. Here we'll use a long term support node version:

  image: node:lts-buster

The cache is really important to make the deploy faster: it allows us to keep the node_modules folder from one deployment to the other. Here we use the job's name as key for the cache, so that only this job retrieves the cache, it will not be used on other cases:

  cache:
    key: "$CI_JOB_NAME"
    paths:
      - node_modules/

Now we define the job's work by defining a set of bash instructions to be executed. We install the node modules and run eleventy:

  script:
    - npm install
    - npm run build

Our files are generated, we need to store them somewhere to send them on the server. To accomplish that, GitLab CI provides the "artifacts" system. We can define the files that will be saved in the artifact and reuse them in another job:

  artifacts:
    paths:
      - dist/

Finally, we ask GitLab CI to run this job only when a commit is pushed to the master branch:

  only:
  - master

Deploy stage

To deploy the files on my server I use a combination of Rsync and SSH. The good news is that there is a Docker container that has these two dependencies installed directly, no need to install them at runtime 😎 So let's declare a new job and, for this one, we'll use this new image:

rsync:
  stage: deploy
  image: instrumentisto/rsync-ssh

The first part of the job is to create an SSH configuration within the new container:

  script:
  - mkdir "${HOME}/.ssh"
  - echo "${SSH_HOST_KEY}" > "${HOME}/.ssh/known_hosts"
  - echo "${SSH_PRIVATE_KEY}" > "${HOME}/.ssh/id_rsa"
  - chmod 700 "${HOME}/.ssh/id_rsa"

All of these information are very sensitive, that is why we don't store them directly in the .yml, which will be commited in the repository. Instead we can make use of GitLab CI's environment variables. To define these variables, you can go in your repository's settings on GitLab and add the necessary variables there.

The second part of the script takes care of using rsync to send the changes over SSH:

- rsync -avH dist/* -e ssh ${SSH_USER}@${SSH_HOST}:${WEBROOT_PATH}

The dist folder will be available to this job, as it was declared as an artifact in the eleventy job.

Finally, we once again ask GitLab CI to run this job only whenever a change is pushed on the master branch:

  only:
  - master

And... voilà! Now we can push our changes on master and they will be automatically deployed on the server, nothing more to do! 🎊 🎉

You can find a complete copy of this script on gist:

https://gist.github.com/kant312/da097ad3f91ecc110792df54a95ed82f