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
GitLab CI Pipeline #
File and Folder Structure #
GitLab-Repository
├── Dockerfile
├── .gitlab-ci.yml
├── php-example # PHP example site
│ ├── css
│ │ └── styles.css
│ └── index.php
└── 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: 8000 # Container port
CONTAINER_NAME: "php-example" # Name of the deployed container
# Define the image name, tagging it with the GitLab CI registry and the current commit SHA
IMAGE_SHA: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA
### 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 specified Dockerfile in the Dockerfiles directory
- docker build --pull -t $IMAGE_SHA -f Dockerfile .
# Push the built Docker image to the GitLab Container Registry
- docker push $IMAGE_SHA
rules:
# Rule: Run this job only for the main branch and if the specified Dockerfile exists
- if: $CI_COMMIT_BRANCH == "main"
exists:
- Dockerfile
### Deploy Container to Virtual Machine
deploy_container:
stage: deploy
image: alpine:latest
needs:
- build_image # Run this job only if the 'build_image' job succeeds
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 $IMAGE_SHA"
# 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 $IMAGE_SHA"
rules:
# Rule: Run this job only for main branch
- if: $CI_COMMIT_BRANCH == "main"
Dockerfile #
Alpine Version #
- Dockerfile
# Start with the lightweight Alpine Linux image with PHP
FROM php:8.2-alpine
# Set the working directory in the container
WORKDIR /var/www/html
# Create a non-root user and group
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Copy the PHP application from the GitLab repository to the container
COPY php-example /var/www/html
# Change ownership of the application files to the non-root user
RUN chown -R appuser:appgroup /var/www/html
# Expose the default PHP development server port
EXPOSE 8000
# Switch to the non-root user
USER appuser
# Start PHP's built-in development server
ENTRYPOINT ["php", "-S", "0.0.0.0:8000"]
Debian Version #
- Dockerfile
# Debian 12 based Apache base image
FROM php:8.1.31-apache-bookworm
# Set the working directory in the container
WORKDIR /var/www/html
# Create a non-root user and group
RUN useradd --system --no-create-home appuser
# Copy the PHP application from the GitLab repository to the container
COPY php-example /var/www/html
# Change ownership of the application files to the non-root user
RUN chown -R appuser:appuser /var/www/html
# Expose the default PHP development server port
EXPOSE 8000
# Switch to the non-root user
USER appuser
# Start PHP's built-in development server
ENTRYPOINT ["php", "-S", "0.0.0.0:8000"]
PHP Example Site #
PHP File: index.php #
- php-example/index.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>jklug.work</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
</head>
<body>
<h1>PHP Example:</h1>
<form method="POST" action="">
<label for="name">Enter your name:</label>
<input type="text" id="name" name="name" required>
<button type="submit">Submit</button>
</form>
<?php
// Check if the form has been submitted
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Get the submitted name from the form
$name = htmlspecialchars($_POST['name']); // Prevent XSS
echo "<h2>Hello, $name!</h2>";
}
?>
</body>
</html>
CSS: styles.css #
- php-example/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 */
}
h2 {
color: #333;
}
/* 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;
}
Verify Deployment #
Alpine Version #
Verify the container:
# List Docker containers
docker ps
# Shell output:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
43d82a348954 gitlab-registry.jklug.work/php/php-example/main:38dbfad2323f6f5b04e7902272a80f3519ed78dd "docker-php-entrypoi…" 3 seconds ago Up 3 seconds 0.0.0.0:8080->8000/tcp, [::]:8080->8000/tcp php-example
Verify the container user:
# Access the container terminal
docker exec -it 43d82a348954 sh
# List details of current user
id
# Shell output:
uid=100(appuser) gid=101(appgroup) groups=101(appgroup)
Verify the container image:
# List docker images
docker images
# Shell output:
REPOSITORY TAG IMAGE ID CREATED SIZE
gitlab-registry.jklug.work/php/php-example/main 38dbfad2323f6f5b04e7902272a80f3519ed78dd 0363ec56ccfd 2 minutes ago 104MB
Verify the Python Flask application:
# Curl the container
curl localhost:8080
# Shell output:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>jklug.work</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
</head>
Debian Version #
Verify the container:
# List Docker containers
docker ps
# Shell output:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5024a38f1d2e gitlab-registry.jklug.work/php/php-example/main:06cc4b04291ea8a0b5c0f70184b68226fe8e3e0c "php -S 0.0.0.0:8000" 8 seconds ago Up 7 seconds 80/tcp, 0.0.0.0:8080->8000/tcp, [::]:8080->8000/tcp php-example
Verify the container user:
# Access the container terminal
docker exec -it 5024a38f1d2e sh
# List details of current user
id
# Shell output:
uid=999(appuser) gid=999(appuser) groups=999(appuser)
Verify the container image:
# List docker images
docker images
# Shell output:
REPOSITORY TAG IMAGE ID CREATED SIZE
gitlab-registry.jklug.work/php/php-example/main 06cc4b04291ea8a0b5c0f70184b68226fe8e3e0c 7cf8609306f5 41 seconds ago 500MB
Verify the Python Flask application:
# Curl the container
curl localhost:8080
# Shell output:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>jklug.work</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
</head>
Local Test #
To run the PHP site locally on a Linux server, install PHP and a webserver like Apache2 and place the PHP site into the Apache web root /var/www/html/
.
The file and folder structure should look like this:
/var/www/
└── html
├── css
│ └── styles.css
└── index.php
Install PHP & Apache #
# Install PHP and dependencies (Debian based distributions)
sudo apt install php libapache2-mod-php php-mysql -y
# Verify PHP installation / check version
php -v
# Shell output:
PHP 8.3.6 (cli) (built: Sep 30 2024 15:17:17) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.3.6, Copyright (c) Zend Technologies
with Zend OPcache v8.3.6, Copyright (c), by Zend Technologies
Install Apache to enable local PHP testing:
# Instapp Apache
sudo apt install apache2 -y
# Remove the default html file
sudo rm /var/www/html/index.html
Verify PHP Project #
To test the PHP website, open the server IP in a browser, for example 192.168.30.16:80
Links #
GitLab CI pipeline that containerizes and deploys a simple PHP site via Alpine container.