Skip to main content

GitLab CI Pipeline - Deploy Static Website via Nginx Container, Multi-branch Deployment, Run container without Root Privileges

1374 words·
GitLab GitLab CI CI Pipeline Multi-branch Nginx Unprivileged Container Docker Git
Table of Contents
GitHub Repository Available



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 # Main / Production Deployment 
192.168.70.7 # Dev Deployment

GitHub Repository:
https://github.com/jueklu/gitlab-ci-container-deployment



Prerequisites
#

Create SSH Key
#

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 servers, create a new user “gitlab-deployment” for 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
ssh-copy-id gitlab-deployment@192.168.70.7

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

SSH into the deployment servers to add the fingerprint:

# SSH into deployment server
ssh gitlab-deployment@192.168.70.6
ssh gitlab-deployment@192.168.70.7

DNS Entry
#

Make sure the deployment servers 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



GitLab Repository
#

Create Dev Branch
#

Create the second dev branch:

# Create a new "dev" branch
git checkout -b dev

# Verify the current branch
git branch

# Shell output:
* dev
  main
# Push changes to remote repository
git push origin dev

File & Folder Structure
#

The file and folder structure of both the main and thedev branch of the GitLab repository look like this:

GitLab-Repository
├── Dockerfile
├── .gitlab-ci.yml
├── README.md
└── website
    └── index.html

SSH Key / CI/CD Variable
#

Add the previously created private SSH key to the GitLab Repository:

  • Go to: (Project)

  • Select “Settings” > “CI/CD”

  • Expand the “Variables” section

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

Note: Remove the “Protect variable” option.


Dockerfile
#

The same Dockerfile is used for both branches:

  • Dockerfile
# Alpine-based NGINX image with unprivileged (non-root) default user
FROM nginxinc/nginx-unprivileged:stable-alpine

# Temporarily switch to "root" user to perform permission adjustments
USER root

# Set variable for the document root
ARG DOCROOT=/usr/share/nginx/html

# Copy website files into the document root and set ownership to the "nobody" user
COPY --chown=nobody:nobody website/ ${DOCROOT}

# Adjust permissions
RUN find ${DOCROOT} -type d -print0 | xargs -0 chmod 755 && \
    find ${DOCROOT} -type f -print0 | xargs -0 chmod 644 && \
    chmod 755 ${DOCROOT}

# Switch back to the "nginx" user for running the container
USER nginx

# Expose port 8080 for the container
EXPOSE 8080

HTML File
#

Use different HTML files for the dev and the production / main branch:


Main / Production Branch
#

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

<head>
	<title>jklug.work</title>
</head>

<body>
  <h1>Deploy website via Nginx container</h1>
  <p>Main / production branch</p>
</body>

</html>

Dev Branch
#

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

<head>
	<title>jklug.work</title>
</head>

<body>
  <h1>Deploy website via Nginx container</h1>
  <p>Dev branch</p>
</body>

</html>



GitLab CI Pipeline
#

Overview
#

The following pipeline consists of two stages:

Stage 1: build

  • Creates a Nginx container that hosts an index.html file and pushes the Nginx container to the GitLab container registry.

Stage 2: deploy

  • Uses an alpine container to connect to a remote server, pulls the Nginx container from stage 1, removes - if existing - the old Nginx container and starts a new Nginx container from the pulled image.

CI Pipeline Manifest: Main Branch
#

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


### Stages
stages:
  - build
  - deploy


### Build Container Image
build_image:
  image: docker:stable
  stage: build
  services:
    - docker:dind
  variables:
    DOCKER_TLS_CERTDIR: ""
  before_script:
    # Login to GitLab Container Registry using predefined CI/CD variables
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
  script:
    # Build the Docker image from the Dockerfile in the current directory
    - 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: # Main / Production Branch
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"

CI Pipeline Manifest: Dev Branch
#

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


### Stages
stages:
  - build
  - deploy


### Build Container Image
build_image:
  image: docker:stable
  stage: build
  services:
    - docker:dind
  variables:
    DOCKER_TLS_CERTDIR: ""
  before_script:
    # Login to GitLab Container Registry using predefined CI/CD variables
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
  script:
    # Build the Docker image from the Dockerfile in the current directory
    - 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 dev branch and if the Dockerfile exists
    - if: $CI_COMMIT_BRANCH == "dev"
      exists:
        - Dockerfile


### Deploy Container to Virtual Machine: # Dev Branch
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 dev branch
    - if: $CI_COMMIT_BRANCH == "dev"



Verify the Deployment
#

Main Branch
#

SSH into the main branch deployment server:

# Curl the main / production deployment
curl 192.168.70.6:8080

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

<head>
        <title>jklug.work</title>
</head>

<body>
  <h1>Deploy website via Nginx container</h1>
  <p>Main / production branch</p>
</body>

</html>

Verify the container:

# List containers on main / production deployment server:
docker ps

# Shell output:
dfd27fe14cf2   gitlab-registry.jklug.work/root/00-nginx-container:main   "/docker-entrypoint.…"   7 minutes ago   Up 7 minutes   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   static-website-production

Dev Branch
#

# Curl the dev deployment
curl 192.168.70.7:8080

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

<head>
        <title>jklug.work</title>
</head>

<body>
  <h1>Deploy website via Nginx container</h1>
  <p>Dev branch</p>
</body>

</html>

Verify the container:

# List containers on dev deployment server:
docker ps

# Shell output:
CONTAINER ID   IMAGE                                                    COMMAND                  CREATED         STATUS         PORTS                                       NAMES
1bcc2bc7fb41   gitlab-registry.jklug.work/root/00-nginx-container:dev   "/docker-entrypoint.…"   2 minutes ago   Up 2 minutes   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   static-website-dev



Links #

jueklu/gitlab-ci-container-deployment

Dockerfile
0
0