Files
web-log/container/site/content/posts/jenkins-deploy-web-log/index.md
T
2025-07-05 15:36:34 +00:00

319 lines
14 KiB
Markdown

+++
categories = ["software"]
tags = ["automation","docker","jenkins","nginx"]
date = 2025-04-06T10:18:00-06:00
description = ""
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`
```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 port `80` and port `443`. 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, the `root` is set to `/var/www/web-log/public`, which is the host directory where the Jenkins build agent container will copy the built Hugo site.
```nginx
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:
```shell
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
```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 .`
```shell
[+] 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`
```shell
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:
```yaml
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.
```env
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.
```groovy
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
```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.
- **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 the `docker-compose.yml` file. This maps to `/var/www/web-log` on the host.
- **Stage: checkout**
- `checkout scm` Retrieves the source code from gitea (scm is configured in the Jenkins pipeline)
- **Stage: hugo build**
- `hugo --environment production --minify` builds and minifies the hugo site. Sets the `environment` to `production` with the `--environment` flag. Outputs to `public` directory 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 the `public` directory in the workspace to the deployment directory.
- `chmod -R 755 ${DEPLOY_DIR}` → Sets permissions for readability and execution.
- **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" >}})
```yaml
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.