DEV Community

Cover image for Automating deployment with GitHub Actions and Nanocl
Leo Vernisse
Leo Vernisse

Posted on

Automating deployment with GitHub Actions and Nanocl

Learn how to automate your deployment with GitHub Actions and Nanocl. This guide walks you through setting up seamless deployments, making it easier to deploy your applications with minimal effort. Whether you're new to CI/CD or an experienced developer, this post will show you how to streamline your workflow using powerful open-source tools.

nanocl-meme

Introduction

Continuous Integration and Continuous Deployment (CI/CD) are essential practices in modern software development. They help automate the process of building, testing, and deploying applications, making it easier to deliver high-quality software quickly and efficiently. GitHub Actions is a powerful tool that allows you to automate your CI/CD pipeline directly from your GitHub repository. Nanocl is a containers and virtual machine orchestrator that simplifies the deployment process by providing a unified interface for managing your infrastructure.

In this blog i will showcase how i did setup our CI/CD pipeline with Github Actions and Nanocl for the deployment of next-hat and and ntex.rs documentation that use docusaurus.

Prerequisites

Before we get started you need a few things:

  • A GitHub account (you can sign up for free at github.com)
  • A project to deploy (it can be a static website, a web application, or anything that can run in a container)
  • A dedicated server or a VPS (i use ovh for servers)
  • A domain name that point to your server (i use ovh for domain names)
  • Docker installed on your local machine and server (you can find the installation instructions here)
  • Nanocl installed on your server (you can find the installation instructions here)

Creating the container image

We will take a look on how to create the container image for the deployment of the documentation of next-hat and ntex.rs that use docusaurus. If you already know how to setup a docker container image you can skip this section.

Creating a Nginx Configuration File

We use nginx as a web server to serve the static files generated by docusaurus. The server.nginx file contains the configuration for the nginx server. You can replace it with your own configuration file to match your use case.

server {
  listen       80;
  listen  [::]:80;
  rewrite ^/(.*)/$ /$1 permanent;

  gzip on;
  gzip_vary on;
  gzip_proxied any;
  gzip_comp_level 8;
  gunzip on;
  gzip_types application/javascript image/* text/css;
  gzip_disable "MSIE [1-6]\.";

  root /home/node/app;
  error_page 404 /404.html;
  try_files $uri.html $uri/index.html =404;

  ## All static files will be cached.
  location ~* ^.+\.(?:css|webp|cur|js|jpe?g|gif|htc|ico|png|html|xml|otf|ttf|eot|woff|woff2|svg)$ {
    access_log off;
    expires 1y;
    add_header Cache-Control max-age=31536000;

    ## No need to bleed constant updates. Send the all shebang in one
    ## fell swoop.
    tcp_nodelay off;

    ## Set the OS file cache.
    open_file_cache max=3000 inactive=120s;
    open_file_cache_valid 45s;
    open_file_cache_min_uses 2;
    open_file_cache_errors off;
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's break down the server.nginx configuration file:

  • server { ... }: This block defines the configuration for the Nginx server.
  • listen 80;: This line specifies that the server should listen on port 80.
  • rewrite ^/(.*)/$ /$1 permanent;: This line redirects requests with a trailing slash to the same URL without the slash.
  • gzip on;: This line enables gzip compression for responses.
  • root /home/node/app;: This line specifies the root directory for serving static files.
  • error_page 404 /404.html;: This line specifies the error page to use for 404 errors.
  • try_files $uri.html $uri/index.html =404;: This line specifies the files to try when a request is made.
  • location ~* ^.+\.(?:css|webp|cur|js|jpe?g|gif|htc|ico|png|html|xml|otf|ttf|eot|woff|woff2|svg)$ { ... }: This block specifies the configuration for serving static files with caching enabled.

This configuration file optimizes the Nginx server for serving static files and improves performance by enabling gzip compression and caching.

Creating a Dockerfile

A Dockerfile is a text document that contains all the commands needed to build a Docker image. It specifies the base image to use, the commands to run, and the files to copy into the image. In this guide, we will create a Dockerfile for a docusaurus website. You can replace the commands with the ones needed for your project.

Create a new file in your project directory called Dockerfile. This file will contain the configuration for your Docker image. Here is an example Dockerfile for a docusaurus website:

FROM node:22.11.0-alpine AS builder

RUN apk add git

USER node

# Create app directory (with user `node`)
RUN mkdir -p /home/node/app
# Set is as cwd
WORKDIR /home/node/app

# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY --chown=node package*.json ./

# Install dependencies
RUN npm install

# Bundle app source code
COPY --chown=node . .
COPY --chown=node ./.git ./.git

RUN npm run build

FROM nginx:1.27.0-alpine3.19-slim

WORKDIR /etc/nginx/conf.d

COPY --from=builder /home/node/app/build /home/node/app

COPY ./server.nginx ./default.conf
Enter fullscreen mode Exit fullscreen mode

Let's break down the Dockerfile:

  • FROM node:22.11.0-alpine AS builder: This line specifies the base image to use for the build stage. We use the node:22.11.0-alpine image, which is a lightweight version of Node.js that includes npm.
  • RUN apk add git: This line installs the git package, which is needed to clone the repository.
  • USER node: This line switches to the node user, which is a non-root user created by the Node.js image.
  • RUN mkdir -p /home/node/app: This line creates a directory for the application code.
  • WORKDIR /home/node/app: This line sets the working directory to the application directory.
  • COPY --chown=node package*.json .: This line copies the package.json and package-lock.json files into the image.
  • RUN npm install: This line installs the dependencies specified in the package.json file.
  • COPY --chown=node . .: This line copies the application code into the image.
  • COPY --chown=node ./.git ./.git: This line copies the .git directory into the image.
  • RUN npm run build: This line builds the application using the npm run build command.
  • FROM nginx:1.27.0-alpine3.19-slim: This line specifies the base image to use for the final image. We use the nginx:1.27.0-alpine3.19-slim image, which is a lightweight version of Nginx.
  • WORKDIR /etc/nginx/conf.d: This line sets the working directory to the Nginx configuration directory.
  • COPY --from=builder /home/node/app/build /home/node/app: This line copies the static files generated by the build stage into the Nginx configuration directory.
  • COPY ./server.nginx ./default.conf: This line copies the server.nginx file into the Nginx configuration directory.

This Dockerfile creates a multi-stage build that first builds the application using Node.js and then copies the static files into an Nginx image. This approach reduces the size of the final image and improves performance by separating the build dependencies from the runtime dependencies.

Building and Running the Docker Image Locally

Before deploying your application to your server, you should test the Docker image locally to ensure that it works as expected. You can build and run the Docker image on your local machine using the following commands:

docker build -t my-image .
docker run -p 8080:80 my-image
Enter fullscreen mode Exit fullscreen mode

The docker build command builds the Docker image using the Dockerfile in the current directory and tags it with the name my-image. The docker run command runs the Docker image on port 8080, mapping it to port 80 inside the container. You can access the application by opening a web browser and navigating to http://localhost:8080.

If everything works as expected, you should see a production version of your docusaurus website running in the browser. You can now proceed to deploy your application to your server using GitHub Actions and Nanocl.

Setting up Nanocl

Nanocl is a powerful tool that simplifies the deployment process by providing a unified interface for managing your infrastructure. You can use Nanocl to deploy your applications to your server with minimal effort. In this guide, we will show you how to set up Nanocl on your server and deploy your application using a simple configuration file.

First, you need to install Nanocl on your server. You can find the installation instructions here. Once you have installed Nanocl, you can create a configuration file for your application. This file will contain the settings for your application, such as the image name, port number, and environment variables.

By default once installed Nanocl is only accessible via /run/nanocl/nanocl.sock. You can use a proxy rule to expose it to the public internet, however, exposing it to the public without a self signed SSL/TLS certificate is not recommended.
It could allow an attacker to take control of your server.
Hopefully, we have a pre-configured rule that you can apply to expose the Nanocl Daemon.

On your dedicated server or VPS, run the following command to apply the rule:

nanocl state apply -fs https://nhnr.io/v0.16/sys/enable-remote-nanocld.yml
Enter fullscreen mode Exit fullscreen mode

It will expose the Nanocl Daemon to the public internet with a self signed SSL/TLS certificate on the port 9943.

Setting up Github Secrets

GitHub Secrets allow you to securely store and use sensitive information in your GitHub repository. You can use GitHub Secrets to store your server credentials, API keys, and other sensitive information needed to deploy your application. In this guide, we will use GitHub Secrets to store the credentials needed to deploy your application to your server using Nanocl.

To get started, go to your GitHub repository and click on the Settings tab. Then, click on the Secrets and variables link in the left sidebar. Click on the Actions link.

You should see a page like this:

github-secrets

Click on the New repository secret button to create a new secret. You can create a secret for each of the following values:

  • NANOCL_HOST: The hostname or IP address of your server with the 9943 port. Example: https://example.com:9943
  • NANOCL_CERT: The content of the self signed SSL/TLS certificate used to secure the connection to the Nanocl Daemon.
  • NANOCL_CERT_KEY: The content of the private key used to secure the connection to the Nanocl Daemon.

You can find the content of the certificate and private key by running the following commands on your server:

nanocl secret inspect cert.client.nanocl.io
Enter fullscreen mode Exit fullscreen mode

This command will output the content of the certificate and private key. You can copy and paste the content into the GitHub Secrets page.

Creating a Statefile Configuration

A Statefile is a configuration file that contains the settings for your application. It specifies the image name, port number, and environment variables needed to deploy your application. You can create a Statefile for your application and use it to deploy your application to your server using Nanocl.
This is the Statefile i use for the deployment of the next-hat documentation:

ApiVersion: v0.16

Args:
- Name: version
  Kind: String

Cargoes:
- Name: nh-doc
  Container:
    Image: ghcr.io/next-hat/documentation:${{ Args.version }}

Resources:
- Name: http.docs.next-hat.com
  Kind: ncproxy.io/rule
  Data:
    Rules:
    - Domain: docs.next-hat.com
      Network: Public
      # Secret created for the certbot job below
      # You can remove this line if you don't want https
      Ssl: cert.docs.next-hat.com
      Locations:
      - Path: /
        Target:
          Key: nh-doc.global.c
          Port: 80
    - Domain: docs.next-hat.com
      Network: Public
      Locations:
      - Path: /
        Target:
          Url: https://docs.next-hat.com
          Redirect: Temporary
Enter fullscreen mode Exit fullscreen mode

This Statefile must be placed in the root of your project directory.

Setting up GitHub Actions

GitHub Actions allow you to automate your workflow directly from your GitHub repository. You can create custom workflows that run on specific events, such as pushing code to your repository or creating a pull request. In this guide, we will create a workflow that builds and deploys your application to your server using Nanocl.
You can find the full source code here

Building and Publishing the Docker Image

To get started, create a new file in your repository called .github/workflows/build-and-publish.yml. This file will contain the configuration for your GitHub Actions workflow. And will be used to build your docker image and publish it to github container registry. Only when we are merging to master branch.

name: Build and publish docker image

on:
  push:
    branches:
      - master

jobs:
  deploy:
    name: Build and publish docker image
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract version from package.json
        id: extract_version
        run: |
          version=$(jq -r '.version' package.json)
          echo "PACKAGE_VERSION=$version" >> $GITHUB_ENV

      - name: Check if version already exists
        id: check_version
        run: |
          VERSION=${{ env.PACKAGE_VERSION }}
          IMAGE_NAME=ghcr.io/${{ github.repository_owner }}/my-image
          if docker manifest inspect $IMAGE_NAME:$VERSION > /dev/null 2>&1; then
            echo "Version $VERSION already exists."
            exit 1
          fi
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: |
            ghcr.io/${{ github.repository_owner }}/documentation:latest
            ghcr.io/${{ github.repository_owner }}/documentation:${{ env.PACKAGE_VERSION }}
Enter fullscreen mode Exit fullscreen mode

This workflow will run every time you merge code in the master branch of your repository. It will build a docker image from your code, tag it with the latest version from your package.json file, and push it to the GitHub Container Registry. You can replace documentation with your image name.

Deploying the Application with Nanocl

Next, create a new file in your repository called .github/workflows/deploy.yml. This file will contain the configuration for your GitHub Actions workflow. And will be used to deploy your application to your server using Nanocl.

name: Deploy

on:
  workflow_run:
    workflows: ["Build and publish Docker image"]
    types:
      - completed

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout repository
      uses: actions/checkout@v3

    - name: Install nanocl cli
      run: |
        wget https://github.com/next-hat/nanocl/releases/download/nanocl-0.16.2/nanocl_0.16.2_amd64.deb
        sudo dpkg -i nanocl_0.16.1_amd64.deb
        rm nanocl_0.16.1_amd64.deb

    - name: Deploy to production
      run: |
        VERSION=$(jq -r '.version' package.json)
        nanocl version
        echo $VERSION
        nanocl state apply -ys Statefile.yml -- --version $VERSION
      env:
        HOST: ${{ secrets.NANOCL_HOST }}
        CERT: ${{ secrets.NANOCL_CERT }}
        CERT_KEY: ${{ secrets.NANOCL_CERT_KEY }}
Enter fullscreen mode Exit fullscreen mode

This workflow will run every time the Build and publish Docker image workflow is completed. It will deploy your application to your server using Nanocl. You can replace Statefile.yml with the path to your Statefile configuration file if you have a different name of path.

Now you have set up your CI/CD pipeline with GitHub Actions and Nanocl. Every time you push code to your repository, GitHub Actions will build and publish your Docker image to the GitHub Container Registry. Once the image is published, it will trigger the deployment workflow, which will deploy your application to your server using Nanocl.

Enable public SSL/TLS with Let's Encrypt

Additionally you can enable public SSL/TLS with Let's Encrypt. You can use the following command to enable public SSL/TLS with Let's Encrypt:

nanocl state apply -fs https://nhnr.io/v0.16/sys/certbot.yml -- --email contact@next-hat.com --domain docs.next-hat.com
Enter fullscreen mode Exit fullscreen mode

Conclusion

By following the steps outlined in this guide, you can automate the deployment of your applications to your server with minimal effort and zero downtime. This will help you deliver high-quality software quickly and efficiently, making it easier to manage your infrastructure and deploy your applications with confidence.

Now that you know how to automate your deployment with GitHub Actions and Nanocl, why not give it a try with your own project? Let us know how it goes or if you have any questions!

You can join my discord server if you have any questions or need help with setting up your CI/CD pipeline with GitHub Actions and Nanocl. I'm here to help you succeed in your software development journey.

Top comments (0)