+++ categories = ["software"] tags = ["automation","docker","jenkins","nginx"] date = 2025-03-30T18:00:00-06:00 description = "" draft = false slug = "jenkins-deploy-website" title = "🤵🏻 Automating Personal Website Deployment" author = "nicholas" +++ I have a simple [personal website](https://nicholas.uuard.com/) that currently serves as a landing page containing links that direct visitors to my other websites. I want to use Jenkins to automate the deployment of this site. In my [last post on this topic]({{< relref "posts/jenkins" >}}), I set up a Jenkins web UI and build agent in container using Docker. I will use this container to automate the process of deploying my website. ## The plan I will configure Jenkins to monitor the `main` branch of my [website repository](https://git.uuard.com/Ward/personal-website) and copy the repo to a host directory that is bind-mounted to both my Jenkins and Nginx containers. This host directory will be bind-mounted to Nginx's document root so the website is automatically published to the web server. ## Jenkins I am using a pipeline definition based on a **pipeline script from SCM**. Instead of storing the pipeline script in Jenkins directly, I will use the script defined in the repository. This has the benefit of being much easier to maintain, as I only need to make changes to the repository. It fully automates the process by eliminating the step where I must configure the pipeline script before I push changes. Below is the Jenkinsfile I used to test my setup. ### Jenkins pipeline configuration - First, I need to add a new pipeline item to Jenkins. {{< image src="images/jenkins-new-pipeline.jpg" caption="New pipeline 'personal-website-pipeline'" >}} - Next I configure the pipeline with the following settings: {{< image src="images/jenkins-pipeline-configuration.jpg" caption="person-website-pipeline settings" >}} I have formatted the relevant settings in YAML below: ```yaml Definition: Pipeline script from SCM SCM: Git Repositories: - Repository URL: https://git.uuard.com/Ward/personal-website.git - Credentials: gitea Branches to build: - Branch Specifier: */main Script Path: Jenkinsfile ``` #### Jenkins pipeline configuration details - **Pipeline Definition** - The pipeline is defined as a **Pipeline script from SCM**, meaning the Jenkinsfile is stored in the repository rather than inside Jenkins itself. - **Source Code Management (SCM) Settings** - **SCM: Git** → Jenkins fetches the pipeline from a Git repository. - **Repository URL: `https://git.uuard.com/Ward/personal-website.git`** → This is the Git repository where the Jenkinsfile and source code are stored. - **Credentials: `gitea`** → Authentication is handled using stored credentials for Gitea. - **Branch to Build** - **Branch Specifier: `*/main`** → Jenkins monitors and builds from the `main` branch. - **Script Path** - **`Jenkinsfile`** → Jenkins looks for the pipeline script inside the repository at this path. ### Jenkinsfile pipeline definition ```Jenkinsfile pipeline { agent any environment { DEPLOY_DIR = '/var/jenkins_home/deployments/personal-website' } stages { stage('Checkout') { steps { checkout scm } } stage('copy to nginx server') { steps { sh """ rm -rf ${DEPLOY_DIR}/* cp -r * ${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 any` → Runs on any available Jenkins agent. I am currently building on the built-in node (agent). - **Environment Variables** - `DEPLOY_DIR = '/var/jenkins_home/deployments/personal-website'`: Sets the deployment directory path. This path is inside the Docker volume `jenkins-data`. The `var/jenkins_home/deployments/personal-website` directory has been bind-mounted to a directory on the host machine, which is also shared as a bind-mount to the document root of Nginx web server running in a separate Docker container. - **Stage: Checkout** - `checkout scm` Retrieves the source code from gitea (scm is configured in the Jenkins pipeline) - **Stage: Copy to Nginx Server** - Runs shell commands in `sh """ ... """` block: - `rm -rf ${DEPLOY_DIR}/*` → Deletes existing files in the deployment directory. - `cp -r * ${DEPLOY_DIR}/` → Copies all files from 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 Container Configuration I need to modify the `docker-compose.yml` file to reflect my description above of how the whole system should work. I add a bind-mount to the **host** directory `nginx-personal-website/html` (which my webserver will also have bind-mounted) to the **container** directory at `var/jenkins_home/deployments/personal-website`. In the pipeline definition above I set the `DEPLOY_DIR` to map to this container directory. ```yaml services: jenkins: image: jenkins/jenkins:jdk17 container_name: jenkins hostname: jenkins restart: always volumes: - jenkins-data:/var/jenkins_home/ - ../nginx-personal-website/html:/var/jenkins_home/deployments/personal-website ports: - "2376:2376" - "8882:8080" - "50000:50000" volumes: jenkins-data: external: true ``` ## Nginx I will use an Nginx container for my web server. The container will simply serve whatever content is in the document root, in this case `/usr/share/nginx/html`. This is the behavior of Nginx, and I can change this by supplying a different configuration. For now, the default configuration is enough. I can add custom configuration later. ### Nginx Container ```yaml services: nginx: image: nginx:alpine container_name: nginx-personal-website restart: unless-stopped ports: - "8800:80" volumes: - ./html:/usr/share/nginx/html:ro #- ./conf:/etc/nginx:ro ``` ## Testing As soon as I push a change to the `main` branch of my repo, Jenkins should execute the pipeline script defined in the `Jenkinsfile`. {{< image src="images/jenkins-pipeline-run-overview.jpg" caption="Jenkins test build - success" >}} ## Improvements - Git Hook Polling is bad and Jenkins agrees. In the tooltip for selecting `Poll SCM` build trigger: > Note that this is going to be an expensive operation for CVS, as every polling requires > Jenkins to scan the entire workspace and verify it with the server. Consider setting up a > "push" trigger to avoid this overhead, as described in this document So instead I select **Trigger builds remotely (e.g., from scripts)** {{< image src="images/jenkins-trigger-build-remotely.jpg" caption="trigger builds remotely" >}} To accomplish triggering builds remotely, I will implement a hook that executes a script after I push to the repository. The script will send a post request to Jenkins which will trigger the build. This script goes in the `gitea/data/git/repositories///hooks/post-receive.d` directory. The way this works is Gitea first automatically runs a built-in `post-receive` hook, which is a script that iterates through all of the files inside the `post-receive.d` directory and executes them as well. Here is the script I use to trigger a build: ```bash #!/bin/bash JENKINS_URL="https://build.uuard.com" JENKINS_JOB="/job/personal-website-pipeline/build" JENKINS_USER="nicholas" JENKINS_API_TOKEN="" JENKINS_BUILD_URL="$JENKINS_URL$JENKINS_JOB" curl -X POST -u "$JENKINS_USER:$JENKINS_API_TOKEN" "$JENKINS_BUILD_URL" exit 0 ``` When Jenkins receives the HTTP POST request at `https://build.uuard.com/job/personal-website-pipeline/build` with valid authentication it will initiate the build. Done.