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 and zero downtime. 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.
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 we will showcase how we 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 (we use ovh for servers)
- A domain name that point to your server (we 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;
}
}
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 package*.json ./
# Install dependencies
RUN npm install
# Bundle app source code
COPY . .
COPY ./.git ./.git
RUN npm run build
FROM nginx:1.27.0-alpine3.19-slim
WORKDIR /etc/nginx/conf.d
COPY /home/node/app/build /home/node/app
COPY ./server.nginx ./default.conf
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 thenode:22.11.0-alpine
image, which is a lightweight version of Node.js that includes npm.RUN apk add git
: This line installs thegit
package, which is needed to clone the repository.USER node
: This line switches to thenode
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 thepackage.json
andpackage-lock.json
files into the image.RUN npm install
: This line installs the dependencies specified in thepackage.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 thenpm 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 thenginx: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 theserver.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
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
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:
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
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 we 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
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 }}
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 }}
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 [email protected] --domain docs.next-hat.com
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 our discord server if you have any questions or need help with setting up your CI/CD pipeline with GitHub Actions and Nanocl. We are here to help you succeed in your software development journey.