14 KiB
+++ categories = ["software"] tags = ["automation","docker","jenkins","nginx"] date = 2025-04-06T10:18:00-06:00 description = "Automating Hugo blog builds and versioned Nginx deployments with Git submodules, Docker, and a custom Jenkins agent." draft = false slug = "jenkins-deploy-web-log" title = "🚀 Hugo Site Deployment - Automating and Version Tracking" author = "nicholas" +++
I am using the static site generator Hugo to generate the web log site that currently hosts this content. Currently, my site is hosted on my local network using hugo server:
Hugo provides its own webserver which builds and serves the site. While hugo server is high performance, it is a webserver with limited options.
This has worked great since I have been using the embedded webserver for development purposes -- each time I make changes to the watched files, Hugo rebuilds and live-reloads the site. This is convenient for development purposes, so I will continue to use it in this way.
For production, though, since I will be publishing the site to the public internet, I will use a more configurable webserver, Nginx, to serve the site.
I will also begin to use Git in my workflow, which will require some configuration on its own, including Git LFS, and hooks. I will create a Jenkins agent capable of building a hugo site. For this I will build a custom Docker image based on the Jenkins inbound agent.
Git
Currently, my site is not edited using any kind of version control. I simply save files to a directory and the built-in Hugo server serves the site to the local machine. I will develop a new workflow so that changes are tracked and managed via Git. This will introduce a little complexity, but worth the benefits:
- Changes made to the project will be tracked, allowing for easy rollback to previous versions and a clear history of changes.
- Storing the site in a Git repository protects against data loss/corruption, as the original code will always be accessible remotely or among a local clone.
Git LFS
My site contains many images and videos, which are relatively large files and are therefore not handled well by Git by default. I will use Git LFS to manage these file types so that my repo does not become slow. For each file type I wish to be managed by Git LFS (i.e. .jpg, .mp4, .webm, etc.), I run git lfs track "*.jpg" in the repository root. This creates an entry in .gitattributes, a file which Git uses to match and intercept committed files to store them in the repository as pointers, instead of the actual image or video. The actual data is stored outside of the repo, in the LFS data storage I have configured on Gitea.
.gitattributes
*.png filter=lfs diff=lfs merge=lfs -text
*.PNG filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.JPG filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text
*.MP4 filter=lfs diff=lfs merge=lfs -text
*.webm filter=lfs diff=lfs merge=lfs -text
Git Submodules
Since my repo contains a Hugo theme that is itself a git repo, which was added to the repo as a submodule, I will need to be sure to include the theme repo when I clone from the repository, using the flag --recurse-submodules, e.g. git clone --recurse-submodules https://git.uuard.com/nicholas/web-log.git
This will be relevant when I write configure a Jenkins pipeline to clone and build the site. It will need to include some way of cloning the submodules.
NGINX Webserver
I run a NGINX webserver on my host machine, whose current primary purpose is to serve as a reverse proxy for my varied local docker services. I will modify the configuration so that I can serve my web log site.
I need to create a file in /etc/nginx/sites-available/ and symlink it to /etc/nginx/sites-enabled/. This is a typical pattern for managing multiple sites that allows me to organize the configurations, one configuration file per site.
web-log Site Configuration
- This configuration file will serve my web log at
log.nicholas.uuard.com. Nginx listens on both port80and port443. The first server block listens on port 80, and will perform a redirect request from the URL over HTTP (port 80) to HTTPS (port 443). In the second server block, therootis set to/var/www/web-log/public, which is the host directory where the Jenkins build agent container will copy the built Hugo site.
server {
listen 80;
server_name log.nicholas.uuard.com www.log.nicholas.uuard.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name log.nicholas.uuard.com www.log.nicholas.uuard.com;
server_tokens off;
ssl_certificate /etc/letsencrypt/live/uuard.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/uuard.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
root /var/www/web-log/public;
index index.html index.htm;
gzip on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
location / {
try_files $uri $uri/ =404;
}
}
Now I can symlink the file to the sites-enabled directory. Nginx loads configuration files from the sites-enabled directory at startup or when the configuration is reloaded, so I will need to to reload the nginx service:
sudo ln -s /etc/nginx/sites-available/web-log /etc/nginx/sites-enabled/
sudo service nginx reload
Custom Jenkins Agent Image
Since I will be building my site on a Jenkins agent, it will need to be able to run hugo commands. I will create a custom Dockerfile to build a new image from the jenkins/inbound-agent base image that includes the hugo binary.
Dockerfile
FROM jenkins/inbound-agent:latest-jdk17
USER root
# set hugo version, architecture
ENV DEBIAN_FRONTEND=noninteractive \
HUGO_VERSION=0.145.0 \
HUGO_ARCH=Linux-64bit
# install wget, hugo; cleanup
RUN apt-get update && \
apt-get install -y --no-install-recommends wget && \
wget -q https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_${HUGO_ARCH}.tar.gz && \
tar -xzf hugo_${HUGO_VERSION}_${HUGO_ARCH}.tar.gz && \
mv hugo /usr/bin/hugo && \
chmod +x /usr/bin/hugo && \
rm hugo_${HUGO_VERSION}_${HUGO_ARCH}.tar.gz && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
Build Custom Jenkins Agent
Now I need to build the image:
docker build -t jenkins-agent-hugo .
[+] Building 6.9s (6/6) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 689B 0.0s
=> [internal] load metadata for docker.io/jenkins/inbound-agent:latest-jdk17 0.5s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> CACHED [1/2] FROM docker.io/jenkins/inbound-agent:latest-jdk17@sha256:6b4 0.0s
=> [2/2] RUN apt-get update && apt-get install -y --no-install-recommend 5.7s
=> exporting to image 0.5s
=> => exporting layers 0.5s
=> => writing image sha256:ae480b018a3928e5c0dede760431eff64a2cabec46e266ab3 0.0s
=> => naming to docker.io/library/jenkins-agent-hugo 0.0s
This will create a docker image named jenkins-agent-hugo. I verify this by listing the docker images:
docker image list.
| REPOSITORY | TAG | IMAGE ID | CREATED | SIZE |
|---|---|---|---|---|
| jenkins-agent-hugo | latest | ae480b018a39 | 2 minutes ago | 336MB |
Jenkins Node (agent)
I have created the Docker image necessary to run a containerized build agent that will be able to build and deploy the Hugo website. Before I can run the container though, I need to create and configure a Jenkins node that corresponds to the agent. This will allow me to connect the Jenkins orchestrator to the container agent.
- Name: "hugo"
- Launch method: Launch agent by connecting it to the controller
- Home directory:
/home/jenkins - Labels: "hugo"
I save the configuration and navigate to the Agent status page where Jenkins provides a command to run the agent. I do not run this command, but extract the secret value for later use in the agent container.
This is an example of the command Jenkins provides. I need the string that follows ... -secret
curl -sO https://build.uuard.com/jnlpJars/agent.jar;java -jar agent.jar -url https://build.uuard.com/ -secret f1f50517be8c46348997c91fb05c5ad73cb13a761a4cce0e5dfcfc6ffb5d8afe -name hugo -webSocket -workDir "/home/jenkins"
Jenkins Pipeline
I set up a new Jenkins pipeline item named web-log-pipeline with basically the same configuration as in my [other post]({{< relref "posts/jenkins-deploy-website/#improvements---git-hook" >}}) that covers this. One difference is in the Advanced sub-modules behaviors. I want to enable the Recursively update submodules option so that the checkout scm step of the Build stage of the pipeline will also clone the theme submodule.
{{< image src="images/jenkins-update-submodules.jpg" caption="recursively update submodules" >}}
That is all.
Docker Compose
As usual, I will configure my docker container with a compose file:
services:
jenkins-agent-hugo:
container_name: jenkins-agent-hugo
hostname: jenkins-agent-hugo
image: jenkins-agent-hugo
init: true
command: ["-url", "${AGENT_URL}", "${AGENT_SECRET}", "${AGENT_NAME}"]
restart: unless-stopped
volumes:
- /var/www/:/mnt/jenkins_deployments/
This file references environment variables stored in .env
The AGENT_SECRET value came from the step above.
AGENT_URL=https://build.uuard.com
AGENT_SECRET=4897df9u4jf...
AGENT_NAME=hugo
The container is configured for a bind-mount that mounts the host directory /var/www/ to the container directory /mnt/jenkins_deployments/.
The Jenkins agent will be configured to build to /mnt/jenkins_deployments/web-log/public, which maps to the Nginx site directory on the host: /var/www/web-log/public.
Once the container is running, I can check its status in Jenkins and verify that the agent can build hugo sites.
{{< image src="images/jenkins-hugo-connected.jpg" caption="Jenkins 'hugo' agent status" >}}
I can visit the Script Console and run the following groovy script. This will print out what version of Hugo is installed.
def hugoVersion = "hugo version".execute().text.trim()
println "Hugo version: ${hugoVersion}"
{{< image src="images/jenkins-hugo-version.jpg" caption="hugo version confirmed" >}}
This confirms that my agent has hugo installed. I will be able to run builds using this agent.
Configuring Jenkins Agent
I will include this Jenkinsfile in the repository. The Jenkins pipeline will be configured according to this document.
Jenkinsfile
pipeline {
agent { label 'hugo' }
environment {
DEPLOY_DIR = '/mnt/jenkins_deployments/web-log'
}
stages {
stage('checkout') {
steps {
checkout scm
}
}
stage('hugo build') {
steps {
sh 'hugo --environment production --minify'
}
}
stage('deploy') {
steps {
sh """
find ${DEPLOY_DIR} -mindepth 1 -delete
cp -r ${WORKSPACE}/public ${DEPLOY_DIR}/
chmod -R 755 ${DEPLOY_DIR}
"""
}
}
}
post {
failure {
echo 'Build failed!'
}
}
}
Jenkinsfile pipeline definition details
-
Pipeline Declaration
- Defines a declarative pipeline with
pipeline {}block.
- Defines a declarative pipeline with
-
Agent
agent { label 'hugo' }→ Runs on any agent with the label 'hugo'.
-
Environment Variables
DEPLOY_DIR = '/mnt/jenkins_deployments/web-log': Sets the deployment directory path, which corresponds to the bind-mount defined in thedocker-compose.ymlfile. This maps to/var/www/web-logon the host.
-
Stage: checkout
checkout scmRetrieves the source code from gitea (scm is configured in the Jenkins pipeline)
-
Stage: hugo build
hugo --environment production --minifybuilds and minifies the hugo site. Sets theenvironmenttoproductionwith the--environmentflag. Outputs topublicdirectory in the workspace.
-
Stage: deploy
- Runs shell commands in
sh """ ... """block:find ${DEPLOY_DIR} -mindepth 1 -delete→ Deletes existing files in the deployment directory.cp -r ${WORKSPACE}/public ${DEPLOY_DIR}/→ Copies all files from thepublicdirectory in the workspace to the deployment directory.chmod -R 755 ${DEPLOY_DIR}→ Sets permissions for readability and execution.
- Runs shell commands in
-
Post Actions
post { failure { echo "Build Failed" } }→ Displays"Build Failed"if any stage fails.
Jenkins Pipeline configuration
I use mostly the same configuration as here:
[Jenkins Pipeline Configuration]({{< relref "posts/jenkins-deploy-website/#jenkins-pipeline-configuration" >}})
Definition: Pipeline script from SCM
SCM: Git
Repositories:
- Repository URL: https://git.uuard.com/nicholas/web-log.git
- Credentials: gitea
Branches to build:
- Branch Specifier: */master
Script Path: Jenkinsfile
Git Hook
To be fully automated, I need to configure a Git hook that will trigger the Jenkins agent to build and deploy the site. Since I cover this in a [previous post]({{< relref "posts/jenkins-deploy-website/#improvements---git-hook" >}}), I do not repeat myself here. There is nothing really different about this hook except the POST request URL defined in the script. The pipeline has a different name web-log-pipeline and therefore a different URL.
Result
Jenkins successfully deployed the Hugo site.
{{< image src="images/jenkins-build-hugo-success.jpg" caption="Successful hugo site deployment" >}}
Done.