Skip to main content

GitLab CI Pipeline - Containerize and Deploy Static Website Project with CI Pipeline in different Project via Diploy Keys

1343 words·
GitLab GitLab CI CI Pipeline Deploy Keys Apache Unprivileged Container
Table of Contents

Overview
#

In this tutorial I’m using the following setup based on Ubuntu 22.04 servers, GitLab is dockerized:

192.168.70.4 # GitLab
192.168.70.5 # GitLab Runner
192.168.70.6 # Deployment Server with Docker installed

Project 1: Static Website
#

Base Structure
#

The first project hosting the static website is named simple-static-website and is located in the static-websites group.

# The URL looks like this
https://gitlab.jklug.work/static-websites/simple-static-website

# Clone with SSH URL
git@gitlab.jklug.work:static-websites/simple-static-website.git

File and Folderstructure
#

The file and folderstructure looks like this:

simple-static-website
├── public
│   ├── css
│   │   └── styles.css
│   ├── index.html
│   └── js
│       └── scripts.js
└── README.md

Static Website
#

HTML File
#

  • public/index.html
<!DOCTYPE html>
<html>

<head>
    <title>jklug.work</title>
    <link rel="stylesheet" type="text/css" href="css/styles.css">
</head>

<body>
    <h1>Some HTML</h1>
    <p>Hi there, click me.</p>
    <!-- Add a button -->
    <button id="myButton">Click Me</button>

    <!-- Link to the JavaScript file -->
    <script src="js/scripts.js"></script>
</body>

</html>

CSS File
#

  • public/css/styles.css
/* General styling for the body */
body {
    font-family: Arial, sans-serif;
    background-color: #f0f0f0;
    margin: 0;
    padding: 0;

    /* Flexbox centering */
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100vh; /* Full viewport height */
}

/* Header styling */
h1 {
    color: #333;
    margin: 0 0 10px 0; /* Remove default margin and add spacing */
}

/* Paragraph styling */
p {
    color: #666;
    font-size: 18px;
    margin: 10px 0; /* Add some spacing */
}

/* Button styling */
button {
    background-color: #d2691e;
    color: white;
    border: none;
    padding: 10px 20px;
    font-size: 16px;
    border-radius: 5px;
    cursor: pointer;
    margin-top: 20px; /* Add space above the button */
}

/* Button hover effect */
button:hover {
    background-color: #8b4513;
}

JavaScript File
#

  • public/js/scripts.js
// Function to handle button clicks
function handleButtonClick() {
    const paragraph = document.querySelector("p");
    // Toggle text between text
    if (paragraph.textContent === "Hi there, click me.") {
        paragraph.textContent = "Button was clicked.";
    } else {
        paragraph.textContent = "Hi there, click me.";
    }
}

// Attach event listener when the DOM is ready
document.addEventListener("DOMContentLoaded", () => {
    const button = document.querySelector("#myButton");
    button.addEventListener("click", handleButtonClick);
});



Project 2: GitLab CI Pipeline
#

Base Structure
#

The project that hosts the CI pipeline for the deployment of the simple-static-website repository is also located in the static-websites group and named simple-static-website-pipeline.

# The URL looks like this
https://gitlab.jklug.work/static-websites/simple-static-website-pipeline

File and Folderstructure
#

simple-static-website-pipeline
├── Dockerfile
├── .gitlab-ci.yml
└── README.md

CI Pipeline Manifest
#

  • .gitlab-ci.yml
### Variables
variables:
  DEPLOY_IP: "192.168.70.6"  # Deployment server IP
  DEPLOY_USER: "gitlab-deployment"  # Deployment server SSH user
  DEPLOY_PORT_HOST: 8080  # Host port
  DEPLOY_PORT_CONT: 80  # Container port
  CONTAINER_NAME: "simple-static-website"  # Name of the deployed container

  # Repository URL of the "simple-static-website" project
  GIT_REPO_URL: "git@gitlab.jklug.work:static-websites/simple-static-website.git"


### Stages
stages:
  - build
  - deploy


### Build Container Image
build_image:
  image: docker:stable
  stage: build
  services:
    - docker:dind
  variables:
    DOCKER_TLS_CERTDIR: ""
  before_script:
    # Install Git
    - apk add --no-cache git
    # SSH setup for repository access
    - mkdir -p ~/.ssh
    - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa
    - ssh-keyscan gitlab.jklug.work >> ~/.ssh/known_hosts
    # Login to GitLab Container Registry using predefined CI/CD variables
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
  script:
    # Clone the "simple-static-website" repository into "static-website" directory
    - git clone $GIT_REPO_URL static-website
    # Build the Docker image, with the static website files
    - docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" .
    # Push the built Docker image to the GitLab Container Registry
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
  rules: 
    # Rule: Run this job only for main branch and if the Dockerfile exists
    - if: $CI_COMMIT_BRANCH == "main"
      exists:
        - Dockerfile


### Deploy Container to Virtual Machine
deploy_container:
  stage: deploy
  image: alpine:latest
  before_script:
    # Update the package index, install the OpenSSH client for SSH connections
    - apk update && apk add openssh-client
    # If the private SSH key file ($ID_RSA) exists, set secure permissions (read/write for the owner only)
    - if [ -f "$ID_RSA" ]; then chmod og= $ID_RSA; fi
  script:
    #  SSH into the deployment server, log in to the GitLab Container registry
    - ssh -i $ID_RSA -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_IP "docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY"
     # SSH into the deployment server, pull the image from the registry
    - ssh -i $ID_RSA -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_IP "docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
    # SSH into the deployment server, remove the existing container (if it exists)
    - ssh -i $ID_RSA -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_IP "docker container rm -f $CONTAINER_NAME || true"
    # SSH into the deployment server, run the new container
    - ssh -i $ID_RSA -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_IP "docker run -d -p $DEPLOY_PORT_HOST:$DEPLOY_PORT_CONT --restart=unless-stopped --name $CONTAINER_NAME $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
  rules:
    # Rule: Run this job only for main branch
    - if: $CI_COMMIT_BRANCH == "main"

Dockerfile
#

  • Dockerfile
# Use the Alpine base image
FROM alpine:latest

# Install Apache2
RUN apk update && apk add apache2 && rm -rf /var/cache/apk/*

# Copy website files to the document root
COPY static-website/public/ /var/www/localhost/htdocs/

# Set ownership and permissions for Apache directories
RUN chown -R apache:apache /var/www && \
    chown -R apache:apache /run/apache2 && \
    chown -R apache:apache /var/log/apache2 && \
    chmod -R 770 /var/run/apache2 && \
    chmod -R 770 /var/log/apache2 && \
    chown -R apache:apache /etc/apache2

# Start Apache2 using non-root user
USER apache

# Expose the default Apache port
EXPOSE 80

# Start Apache
ENTRYPOINT ["/usr/sbin/httpd", "-D", "FOREGROUND"]

Note: The build job clones the simple-static-website repository into a folder named static-website, therefore the COPY command in the Dockerfile copies the files from the static-website/public/ directory. See code snippets below:

# Deployment Job:
  script:
    # Clone the "simple-static-website" repository into "static-website" directory
    - git clone $GIT_REPO_URL static-website

# Dockerfile:
# Copy website files to the document root
COPY static-website/public/ /var/www/localhost/htdocs/



Create & Add Deploy Keys
#

Generate SSH Key Pair
#

Generate a SSH key pair named gitlab-ci-key and gitlab-ci-key.pub in the current directory:

# Generate a new SSH key pair
ssh-keygen -t rsa -b 4096 -C "gitlab-ci-key" -f gitlab-ci-key -N ""

Add Public Key to Website Project
#

  • Go to: (Project) “Settings” > “Repository”

  • Expand the “Deploy keys” section

  • Click “Add new key”

  • Define a title like CI Pipeline Key

  • Paske the value of the public key gitlab-ci-key.pub

  • Click “Add key”


Add Private Key to Pipeline Project
#

  • Go to: (Project) “Settings” > “CI/CD”

  • Expand the “Variables” section

  • Click “Add variable”

  • Select type: “Variable (default)”

  • Unflag “Protect variable”

  • Define a key name like SSH_PRIVATE_KEY

  • Paste the value of the private key gitlab-ci-key

  • Click “Add variable”



Deployment Server Prerequisites
#

Create SSH Key Pair
#

Create a SSH key pair on the server where GitLab is deployed:

# Create SSH key pair
ssh-keygen -t rsa -b 2048

Create a User for the Container Deployment
#

On the deployment server, create a new user “gitlab-deployment” that is used by the deploy_container job of the GitLab CI pipeline:

# Create user for the GitLab deployment
sudo adduser gitlab-deployment

# Add the user to the Docker group
sudo usermod -aG docker gitlab-deployment

Add Public Key & Fingerprint
#

Copy the public SSH key to the authorized keys file of the Deployment servers:

# Copy the public SSH key: Default "id_rsa.pub" key
ssh-copy-id gitlab-deployment@192.168.70.6

# Copy the public SSH key: Define specific key
ssh-copy-id -i ~/.ssh/filename.pub gitlab-deployment@192.168.70.6

SSH into the deployment server to add the fingerprint:

# SSH into deployment server
ssh gitlab-deployment@192.168.70.6

DNS Entry
#

Make sure the deployment server can resolve the DNS names of GitLab and the GitLab Registry:

# Add the following DNS entry
192.168.70.4 gitlab.jklug.work gitlab-registry.jklug.work

Add SSH Key as CI/CD Variable
#

Add the previously created private SSH key as variable to the simple-static-website-pipeline project:

  • Go to: (Project) “Settings” > “CI/CD”

  • Expand the “Variables” section

  • Select “Type”: “File”

  • Define “Key”: ID_RSA

  • Paste the private SSH key as “Value”

  • Click “Add variable” to add a variable for your private SSH Key.

Note: Remove the “Protect variable” option so that it can be used in different branches.



Verify the Deployment
#

SSH into the deployment server:

# List containers
docker ps

# Shell output:
CONTAINER ID   IMAGE                                                                            COMMAND                  CREATED              STATUS              PORTS                                     NAMES
34098f4edbf4   gitlab-registry.jklug.work/static-websites/simple-static-website-pipeline:main   "/usr/sbin/httpd -D …"   About a minute ago   Up About a minute   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp   simple-static-website
# Curl the container
curl localhost:8080

# Shell output:
<!DOCTYPE html>
<html>

<head>
    <title>jklug.work</title>
    <link rel="stylesheet" type="text/css" href="css/styles.css">
</head>