🚀 DevOps - GitLab deployment¶
Overview¶
This procedure automatically deploys the main branch to a Linux server after a push.
The GitLab runner connects to the server over SSH and executes a deployment script. The server then fetches the latest commit from GitLab, validates the Docker Compose configuration, rebuilds the application images, and recreates the affected containers.
Two separate SSH key pairs are used:
- a CI deployment key for the GitLab runner to access the Linux server;
- a GitLab deploy key for the Linux server to access the private repository.
The deployment uses git fetch and git reset --hard instead of git pull to ensure that the server matches origin/main exactly. Any tracked modifications made directly on the server will be discarded.
This procedure does not provide automatic rollback or zero-downtime deployment.
Requirements¶
Define the project and deployment settings before running the setup commands:
# define the deployment settings
PROJECT="opendata"
DEPLOY_USER="debian"
DEPLOY_HOST="gpt.kurrant.com"
DEPLOY_PORT="22"
REPOSITORY_SSH="git@gitlab.com:kurrant/luwa/opendata.git"
Configure server access to GitLab¶
The server needs a read-only GitLab deploy key because it fetches the private repository over SSH.
Run these commands as the deployment user, not as root.
Generate the GitLab deploy key¶
# verify the project variable
[ -n "${PROJECT:-}" ] || { echo "project is not set" >&2; exit 1; }
# create the ssh directory
mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"
# generate the repository deploy key
ssh-keygen \
-t ed25519 \
-N "" \
-C "deploy-${PROJECT}-server-to-gitlab" \
-f "$HOME/.ssh/gitlab_${PROJECT}_deploy_key"
# protect the key files
chmod 600 "$HOME/.ssh/gitlab_${PROJECT}_deploy_key"
chmod 644 "$HOME/.ssh/gitlab_${PROJECT}_deploy_key.pub"
# display the public key
cat "$HOME/.ssh/gitlab_${PROJECT}_deploy_key.pub"
The private key must remain on the Linux server.
Register the deploy key in GitLab¶
Open:
project → settings → repository → deploy keys → add new key
Configure the key:
title: staging/prod - PROJECT deployment server
key: paste the content of gitlab_deploy_key.pub
grant write permissions: unchecked
expiration date: optional but recommended
Read access is sufficient because the server only runs git fetch.
Register the GitLab host key¶
Collect the GitLab host key:
# collect the gitlab host key
ssh-keyscan -t ed25519 gitlab.com 2>/dev/null > /tmp/gitlab_known_hosts
# display the collected fingerprint
ssh-keygen -lf /tmp/gitlab_known_hosts
Compare the fingerprint with the fingerprint published by GitLab before installing it.
# install the verified gitlab host key
touch "$HOME/.ssh/known_hosts"
cat /tmp/gitlab_known_hosts >> "$HOME/.ssh/known_hosts"
chmod 600 "$HOME/.ssh/known_hosts"
rm -f /tmp/gitlab_known_hosts
Do not accept an unverified host key automatically.
Test GitLab authentication¶
# test repository authentication
ssh \
-T \
-i "$HOME/.ssh/gitlab_${PROJECT}_deploy_key" \
-o IdentitiesOnly=yes \
-o StrictHostKeyChecking=yes \
git@gitlab.com
Clone the repository¶
# verify the project variable
[ -n "${PROJECT:-}" ] || { echo "project is not set" >&2; exit 1; }
# define the application directory
APP_DIR="${HOME}/${PROJECT}"
# clone the repository when required
if [ ! -d "$APP_DIR/.git" ]; then
GIT_SSH_COMMAND="ssh -i $HOME/.ssh/gitlab_${PROJECT}_deploy_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" \
git clone "$REPOSITORY_SSH" "$APP_DIR"
fi
# configure the repository ssh key
git -C "$APP_DIR" config core.sshCommand \
"ssh -i $HOME/.ssh/gitlab_${PROJECT}_deploy_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes"
Verify that the repository can be updated:
# test the repository fetch
git -C "$APP_DIR" fetch origin main
Configure CI access to the server¶
The GitLab runner needs a separate SSH key pair to connect to the Linux server.
Generate this key on a trusted administration workstation.
Generate the CI deployment key¶
# verify the project variable
[ -n "${PROJECT:-}" ] || { echo "project is not set" >&2; exit 1; }
# define the local key path
KEY_FILE="./gitlab_ci_${PROJECT}_server"
# generate the ci deployment key
ssh-keygen \
-t ed25519 \
-N "" \
-C "gitlab-ci-${PROJECT}-runner-to-server" \
-f "$KEY_FILE"
This creates:
gitlab_ci_opendata_server
gitlab_ci_opendata_server.pub
The private key will become SSH_PRIVATE_KEY.
The public key must be installed on the Linux server.
Install the public key on the server¶
# install the ci public key
ssh-copy-id \
-i "${KEY_FILE}.pub" \
-p "$DEPLOY_PORT" \
"$DEPLOY_USER@$DEPLOY_HOST"
Test the connection:
# test the ci ssh key
ssh \
-i "$KEY_FILE" \
-p "$DEPLOY_PORT" \
-o IdentitiesOnly=yes \
"$DEPLOY_USER@$DEPLOY_HOST"
Use one CI key per environment where possible. Do not reuse the server-to-GitLab deploy key.
Generate SSH_KNOWN_HOSTS¶
SSH_KNOWN_HOSTS contains the Linux server's public SSH host key. It is not the CI public key and is not a private secret.
First, display the server's ED25519 host-key fingerprint directly on the server:
# display the server host fingerprint
sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
Then collect the same key from a trusted administration workstation:
# collect the server host key
ssh-keyscan \
-p "$DEPLOY_PORT" \
-t ed25519 \
"$DEPLOY_HOST" \
2>/dev/null > ssh_known_hosts
# display the collected fingerprint
ssh-keygen -lf ssh_known_hosts
Compare both fingerprints. They must be identical.
The hostname and port used by ssh-keyscan must match DEPLOY_HOST and DEPLOY_PORT exactly.
Do not run ssh-keyscan inside the CI job because this would trust whichever server responds during the pipeline.
Configure GitLab CI/CD variables¶
Open:
project → settings → ci/cd → variables
Protect the main branch before enabling protected variables.
SSH_PRIVATE_KEY¶
key: SSH_PRIVATE_KEY
type: file
value: full content of gitlab_ci_opendata_server
visibility: visible
protect variable: enabled
environment scope: staging
expand variable reference: disabled
The private key must:
- include the
BEGINandENDlines; - use Unix LF line endings;
- end with a final newline;
- never be printed in the pipeline log.
For a File variable, $SSH_PRIVATE_KEY contains the path to GitLab's temporary private-key file.
SSH_KNOWN_HOSTS¶
key: SSH_KNOWN_HOSTS
type: file
value: full content of ssh_known_hosts
visibility: visible
protect variable: enabled
environment scope: staging
expand variable reference: disabled
For a File variable, $SSH_KNOWN_HOSTS contains the path to GitLab's temporary known-hosts file.
Server variables¶
Add these regular variables:
key: DEPLOY_HOST
value: server.example.com
type: variable
protect variable: enabled
environment scope: staging
key: DEPLOY_PORT
value: 22
type: variable
protect variable: enabled
environment scope: staging
key: DEPLOY_USER
value: debian
type: variable
protect variable: enabled
environment scope: staging
Configure the GitLab pipeline¶
Create .gitlab-ci.yml in the repository:
stages:
- deploy
deploy_staging:
stage: deploy
image: alpine:3.22
timeout: 15m
variables:
GIT_STRATEGY: "none"
PROJECT: "opendata"
before_script:
- apk add --no-cache openssh-client
- test -n "${DEPLOY_HOST:-}" || { echo "deploy host is not set" >&2; exit 1; }
- test -n "${DEPLOY_PORT:-}" || { echo "deploy port is not set" >&2; exit 1; }
- test -n "${DEPLOY_USER:-}" || { echo "deploy user is not set" >&2; exit 1; }
- test -s "${SSH_PRIVATE_KEY:-}" || { echo "ssh private key is missing or empty" >&2; exit 1; }
- test -s "${SSH_KNOWN_HOSTS:-}" || { echo "ssh known hosts is missing or empty" >&2; exit 1; }
- export SSH_KEY_FILE="$HOME/.ssh/deploy_key_${PROJECT}"
- install -m 700 -d "$HOME/.ssh"
- install -m 644 "$SSH_KNOWN_HOSTS" "$HOME/.ssh/known_hosts"
- chmod 400 "$SSH_PRIVATE_KEY"
- ssh-keygen -y -f "$SSH_PRIVATE_KEY" >/dev/null || { echo "ssh private key is invalid" >&2; exit 1; }
script:
- >
ssh
-p "$DEPLOY_PORT"
-i "$SSH_PRIVATE_KEY"
-o BatchMode=yes
-o ConnectTimeout=15
-o IdentitiesOnly=yes
-o StrictHostKeyChecking=yes
"$DEPLOY_USER@$DEPLOY_HOST"
"/usr/local/bin/deploy-${PROJECT}"
environment:
name: prod
deployment_tier: production
resource_group: opendata-prod
rules:
- if: '$CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"'
when: on_success
- when: never
GIT_STRATEGY: "none" is intentional because the job does not need a local checkout. The Linux server fetches the repository itself.
The original ~/.ssh/deploy_key file is no longer needed. The pipeline uses the temporary file referenced by $SSH_PRIVATE_KEY directly.
The pipeline runs only for pushes to main. Manual, scheduled, merge-request, and tag pipelines will not deploy with these rules.
Create the server deployment script¶
Run this setup command after defining PROJECT.
# verify the project variable
[ -n "${PROJECT:-}" ] || { echo "project is not set" >&2; exit 1; }
# create the deployment script
sudo tee "/usr/local/bin/deploy-${PROJECT}" >/dev/null <<EOF
#!/usr/bin/env bash
set -Eeuo pipefail
APP_DIR="/home/debian/${PROJECT}"
ENV_FILE="\${APP_DIR}/.env"
COMPOSE_FILE="\${APP_DIR}/compose.yml"
echo "deploying ${PROJECT}"
cd "\${APP_DIR}"
echo "fetching main branch"
git fetch --prune origin main
echo "updating working tree"
git reset --hard origin/main
echo "validating docker compose configuration"
docker compose \
--project-directory "\${APP_DIR}" \
--env-file "\${ENV_FILE}" \
-f "\${COMPOSE_FILE}" \
config >/dev/null
echo "building and starting containers"
docker compose \
--project-directory "\${APP_DIR}" \
--env-file "\${ENV_FILE}" \
-f "\${COMPOSE_FILE}" \
up -d --build --remove-orphans
echo "container status"
docker compose \
--project-directory "\${APP_DIR}" \
--env-file "\${ENV_FILE}" \
-f "\${COMPOSE_FILE}" \
ps
echo "deployment completed"
EOF
# set the script permissions
sudo chmod 755 "/usr/local/bin/deploy-${PROJECT}"
Test the server deployment¶
Execute the script as the deployment user:
# test the deployment script
sudo \
-u "$DEPLOY_USER" \
-H \
"/usr/local/bin/deploy-${PROJECT}"
Verify:
- the repository is updated to
origin/main; - the Compose configuration is valid;
- images are rebuilt;
- containers are running or healthy;
- the application responds correctly.
Security notes¶
Keep the following files and values private:
gitlab_ci_opendata_server
~/.ssh/gitlab_opendata_deploy_key
.env
The following values are public and can be distributed:
gitlab_ci_opendata_server.pub
~/.ssh/gitlab_opendata_deploy_key.pub
ssh_known_hosts