Welcome to my blog! With Grok now free for everyone, I experimented with it to help me word this post in Gen Z slang. It’s so funny and cringe-worthy. XD.
Yo fam! Just a Gen Z dev out here messing around with servers and making this guide because why not? Finna deploy some containers and automate all the things, fr fr. Let’s get this bread! ๐ฅ
The Blueprint (What We’re Building) ๐๏ธ
We’re setting up two servers:
Server 1 (The Manager):
- Docker (the foundation)
- Portainer (the pretty UI)
- Jenkins (the worker bee)
Server 2 (The WordPress Server):
- Docker
- WordPress + MySQL stack
- Jenkins user (for automation)
The mission is to automate WordPress to update to the latest version from a jenkins pipeline.
Had some issues with the pipeline, so in this blog, you’ll see the errors and corrections as well.
Server 1: The Control Center Setup ๐ฎ
1. Docker Installation – The Foundation Arc ๐ช
# Update your system (keeping it fresh)
sudo apt update && sudo apt upgrade -y
# Get the prerequisites (the whole squad)
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
# Add Docker's GPG key (security check)
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Add Docker repo (the source of power)
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list
# Install Docker (the real MVP)
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Get that VIP access
sudo usermod -aG docker $USER
Log out and back in (yes bestie, you need to so that the usermod to take effect) ๐
2. Portainer Installation – The UI Glow Up โจ
Create a docker-compose.yml file.
I created mine in a specific folder : mkdir -p /docker/portainer
Then use your favourite editor : nano docker-compose.yml
version: '3'
services:
portainer:
image: portainer/portainer-ce:latest
container_name: portainer
restart: always
security_opt:
- no-new-privileges:true
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- portainer_data:/data
ports:
- 9000:9000
volumes:
portainer_data:
Deploy it:
docker compose up -d
Setup steps:
1. Visit http://server1-ip:9000
(I’m using a private IP, mine is 192.168.1.110).
2. Create your admin account (make it secure bestie).
3. Choose Add Environment then choose Docker Standalone.
4. You’re in! Time to add Jenkins to the party
3. Jenkins Installation – The Automation Queen ๐
We will deploy Jenkins via portainer :
1. Go to Stacks in Portainer
2. Add Stack (+ button in blue)
3. Name it ‘jenkins’ and paste this compose file for Jenkins
version: '3'
services:
jenkins:
image: jenkins/jenkins:lts
container_name: jenkins
privileged: true
user: root
restart: unless-stopped
ports:
- 8080:8080
- 50000:50000
volumes:
- jenkins_home:/var/jenkins_home
- /var/run/docker.sock:/var/run/docker.sock
volumes:
jenkins_home:
4. Deploy the stack (at the end of the page)
Start the Jenkins Setup:
1. Get that initial password:
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
2. Visit http://server1-ip:8080
(mine is 192.168.1.110)
3. Install suggested plugins
4. Create admin user (secure it like your spotify password)
Install this extra plugin (essential for ssh):
- SSH Agent
Go in: Dashboard > Manage Jenkins > Plugins
Now in available plugins, search for the plugin mentioned above and install it :
Server 2: The WordPress Server Setup ๐
1. Docker Installation
Just like Server 1, download Docker as well.
2. Jenkins User Setup – The Access Manager ๐
# Create jenkins user
sudo useradd -m -s /bin/bash jenkins
# Add jenkins user to docker group
sudo usermod -aG docker jenkins
# Set a password
sudo passwd jenkins
# Switch to jenkins user
su - jenkins
# Generate SSH key pair(no need to put a passphrase)
ssh-keygen -t rsa -b 4096 -f ~/.ssh/jenkins_key
# Add public key to authorized_keys
cat ~/.ssh/jenkins_key.pub >> ~/.ssh/authorized_keys
# Set proper permissions
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chmod 600 ~/.ssh/jenkins_key
# Copy the private key (you'll need this for Jenkins)
cat ~/.ssh/jenkins_key
3. WordPress Stack Setup – The Content Kingdom ๐
You can install portainer on this server but for now, we’ll stick to a docker-compose.yml directly.
Ight bet, let’s break down this docker-compose file – it’s literally the blueprint for our WordPress setup, no cap:
version: '3.1'
services:
wordpress:
image: wordpress
restart: always
ports:
- 8080:80
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: exampleuser
WORDPRESS_DB_PASSWORD: examplepass
WORDPRESS_DB_NAME: exampledb
volumes:
- wordpress:/var/www/html
db:
image: mysql:8.0
restart: always
environment:
MYSQL_DATABASE: exampledb
MYSQL_USER: exampleuser
MYSQL_PASSWORD: examplepass
MYSQL_ROOT_PASSWORD: 1234
volumes:
- db:/var/lib/mysql
volumes:
wordpress:
db:
โ ๏ธ Don’t forget to change your Database and WordPress name, username, password and root password(mine is only for an example).
The tea on what’s happening here:
- We’re setting up two containers that are besties fr fr:
- WordPress (the main character) – handles your website
- MySQL (the memory keeper) – stores all your data
- The WordPress container is giving us:
- Port 8080 so we can access our site (HTTP traffic and all that)
- Some environment variables spilling the tea about the database connection
- A volume to save all our files (ain’t nobody trying to lose their uploads)
- The MySQL container is coming through with:
- Version 8.0 because we stay updated
- Its own storage (volume) for all that precious data
- A whole setup for our database user and passwords
- The volumes part? That’s where we store the goods:
wordpress
: keeps your themes, plugins, and uploads safedb
: saves all your database info- Both stay intact even if you delete the containers (we love a backup)
To get this party started:
# Create a directory for our WordPress stack
mkdir -p /docker/wordpress-stack && cd /docker/wordpress-stack
# Save the compose file
nano docker-compose.yml # paste that code above
# Fire it up bestie
docker compose up -d
Once it’s running, hit up http://server2-ip:8080
(my server2 IP is 192.168.1.147) and you’ll see the WordPress setup screen. It’s giving main character energy fr fr! ๐
Pro tip: Change them passwords before going live bestie, we ain’t trying to get hacked! ๐
IT WORKS !!
Back to Server 1: Jenkins Credentials Setup ๐
1. SSH Credentials
- Go to Manage Jenkins โ Credentials โ System โ Global credentials โ Add Credentials
- Set up SSH key:
- Kind: SSH Username with private key
- ID: sshkey-jenkins
- Description: SSH key for Server 2 access
- Username: jenkins
- Private Key: Enter directly
- Paste the private key from Server 2 (the one we copied earlier)
2. MySQL Credentials
- Go to Manage Jenkins โ Credentials โ System โ Global credentials โ Add Credentials
- Set up MySQL user:
- Kind: Username with password
- ID: mysql-credentials
- Description: MySQL access for WordPress
- Username: exampleuser (same as in docker-compose)
- Password: examplepass (same as in docker-compose)
The Pipeline – The Main Character ๐
Ight, let’s set up that pipeline in Jenkins real quick:
1. Create that new pipeline:
- Hit up Jenkins main page
- Click “New Item” at the top left
- Give it a name (like “wordpress-update-pipeline”)
- Choose “Pipeline” project
- Click “OK” (we valid)
2. In the pipeline config:
- Scroll down to the “Pipeline” section
- Under “Definition” choose “Pipeline script”
- Drop our pipeline code in the “Script” box (the one we have below)
- Don’t forget to change
REMOTE_IP
to your Server 2’s IP - Hit “Save” and we out
Tip: In General, I like to check these 2 boxes:
Create a new Pipeline in Jenkins and use this code (the automation bestie):
pipeline {
agent any
environment {
SSH_CREDENTIALS = credentials('sshkey-jenkins')
MYSQL_CREDENTIALS = credentials('mysql-credentials')
REMOTE_USER = 'jenkins'
REMOTE_IP = '192.168.1.147'
WORDPRESS_CONTAINER = 'wordpress-stack-wordpress-1'
WORDPRESS_DB_CONTAINER = 'wordpress-stack-db-1'
WORDPRESS_DB = 'exampledb'
}
stages {
stage('Backup WordPress') {
steps {
sshagent(['sshkey-jenkins']) {
script {
def retryCount = 3
def retryDelay = 10
for (int i = 0; i < retryCount; i++) {
try {
sh '''
echo 'Stopping WordPress container...'
ssh ${REMOTE_USER}@${REMOTE_IP} "docker stop ${WORDPRESS_CONTAINER}"
echo 'Dumping WordPress database...'
ssh ${REMOTE_USER}@${REMOTE_IP} "docker exec ${WORDPRESS_DB_CONTAINER} mysqldump -u \${MYSQL_CREDENTIALS_USR} -p\${MYSQL_CREDENTIALS_PSW} ${WORDPRESS_DB} > ~/wordpress_backup.sql"
echo 'Backing up WordPress files...'
ssh ${REMOTE_USER}@${REMOTE_IP} "tar -czf ~/wordpress_files_backup.tar.gz -C /var/www/html ."
echo 'Copying backups to Jenkins workspace...'
scp ${REMOTE_USER}@${REMOTE_IP}:~/wordpress_backup.sql .
scp ${REMOTE_USER}@${REMOTE_IP}:~/wordpress_files_backup.tar.gz .
'''
break
} catch (Exception e) {
if (i < retryCount - 1) {
echo "Error during backup, retrying in ${retryDelay} seconds..."
sleep(retryDelay)
} else {
echo "Maximum retries reached, aborting backup."
throw e
}
}
}
}
}
}
}
stage('Verify WordPress Image') {
steps {
script {
env.CURRENT_IMAGE_ID = sh(
script: "ssh -v ${REMOTE_USER}@${REMOTE_IP} \"docker inspect --format='{{.Image}}' ${WORDPRESS_CONTAINER}\"",
returnStdout: true
).trim()
env.LATEST_IMAGE_ID = sh(
script: "ssh -v ${REMOTE_USER}@${REMOTE_IP} \"docker inspect --format='{{.Id}}' wordpress:latest\"",
returnStdout: true
).trim()
echo "Current WordPress Image ID: ${env.CURRENT_IMAGE_ID}"
echo "Latest WordPress Image ID: ${env.LATEST_IMAGE_ID}"
}
}
}
stage('Pull Latest WordPress and Restart') {
when {
expression { env.CURRENT_IMAGE_ID != env.LATEST_IMAGE_ID }
}
steps {
sshagent(['sshkey-jenkins']) {
script {
try {
sh '''
echo 'Pulling the latest WordPress image and restarting containers...'
ssh ${REMOTE_USER}@${REMOTE_IP} "
cd /docker/wordpress-stack/
docker compose down
docker pull wordpress:latest
docker compose up -d
"
# Wait for containers to start
sleep 30
'''
// Additional verification after starting containers
def containerStatus = sh(
script: "ssh ${REMOTE_USER}@${REMOTE_IP} \"docker ps | grep wordpress\"",
returnStatus: true
)
if (containerStatus != 0) {
error("WordPress container failed to start")
}
} catch (Exception e) {
echo "Failed to restart WordPress: ${e.message}"
currentBuild.result = 'FAILURE'
throw e
}
}
}
}
}
stage('Health Check') {
when {
expression { env.CURRENT_IMAGE_ID != env.LATEST_IMAGE_ID }
}
steps {
script {
def healthStatus = sh(
script: "curl -o /dev/null -s -w '%{http_code}' http://${REMOTE_IP}:8080/",
returnStatus: true
)
if (healthStatus != 0) {
echo "Health check failed. Rolling back..."
currentBuild.result = 'FAILURE'
error("Health check failed")
} else {
echo "Health check passed. Upgrade successful!"
}
}
}
}
stage('Rollback on Failure') {
when {
expression { currentBuild.result == 'FAILURE' }
}
steps {
sshagent(['sshkey-jenkins']) {
sh '''
echo 'Performing rollback...'
ssh ${REMOTE_USER}@${REMOTE_IP} "
echo 'Stopping current WordPress container...'
docker compose down
echo 'Rolling back to previous WordPress version...'
docker run -d --name wordpress --restart always \
-v /path/to/wordpress/html:/var/www/html \
wordpress:5.8.3
echo 'Rollback completed!'
"
'''
}
}
}
}
post {
success {
script {
if (env.CURRENT_IMAGE_ID == env.LATEST_IMAGE_ID) {
echo """*************************************************************
WordPress is already at the latest version (${env.LATEST_IMAGE_ID})
No upgrade was necessary at this time.
*************************************************************"""
} else {
echo """*************************************************************
WordPress successfully upgraded to the latest version!
Previous Image ID: ${env.CURRENT_IMAGE_ID}
New Image ID: ${env.LATEST_IMAGE_ID}
*************************************************************"""
}
}
}
failure {
echo """*************************************************************
WordPress upgrade FAILED
Rollback has been completed.
Please check the build logs for detailed error information.
*************************************************************"""
}
}
}
โ ๏ธThe environment part is the most important in a pipeline (rename your variables if needed )
3. Test run that thing:
- Click “Build Now” on the left
- Watch the magic happen in “Stage View”
- If it’s red, check them logs by clicking the stage
- If it’s blue, you’re literally slaying rn
Now check Console Output to see if there’s any errors :
I had this error :
Here’s what’s happening:
- The pipeline starts correctly and sets up SSH credentials
- When trying to execute the SSH command to stop the WordPress container, it fails with “Host key verification failed”
Here are a few ways to fix this:
- The most secure approach is to add the host key to Jenkins’ known_hosts file. You can add this stage before your backup.
- Alternatively, you can modify your SSH commands to temporarily disable strict host checking (less secure, but useful for testing)
But I prefer the first fix (more secure). Add this part after the environment function(in the stages {}) :
stage('Setup SSH Known Hosts') {
steps {
sh '''
mkdir -p ~/.ssh
ssh-keyscan -H 192.168.1.147 >> ~/.ssh/known_hosts
'''
}
}
Let’s try building it again.
Now we got this error :
mysqldump: Error: 'Access denied; you need (at least one of) the PROCESS privilege(s) for this operation' when trying to dump tablespaces
I had to add –no-tablespaces in the mysqldump command :
ssh ${REMOTE_USER}@${REMOTE_IP} "docker exec ${WORDPRESS_DB_CONTAINER} mysqldump -u \${MYSQL_CREDENTIALS_USR} -p\${MYSQL_CREDENTIALS_PSW} ${WORDPRESS_DB} > ~/wordpress_backup.sql --no-tablespaces"
I also had to start the WordPress container :
docker start wordpress-stack-wordpress-1
Let’s try building it again.
New error, had to a new function, because of this error :
And now it works :
Here’s the updated pipeline with added pre-health check and more fixes :
pipeline {
agent any
environment {
SSH_CREDENTIALS = credentials('sshkey-jenkins')
MYSQL_CREDENTIALS = credentials('mysql-credentials')
REMOTE_USER = 'jenkins'
REMOTE_IP = '192.168.1.147'
WORDPRESS_CONTAINER = 'wordpress-stack-wordpress-1'
WORDPRESS_DB_CONTAINER = 'wordpress-stack-db-1'
WORDPRESS_DB = 'exampledb'
WORDPRESS_PATH = '/docker/wordpress-stack'
}
stages {
stage('Setup SSH Known Hosts') {
steps {
sh '''
mkdir -p ~/.ssh
ssh-keyscan -H ${REMOTE_IP} >> ~/.ssh/known_hosts
'''
}
}
stage('Pre-backup Health Check') {
steps {
script {
def maxAttempts = 3
def waitTime = 10
def healthy = false
for (int i = 0; i < maxAttempts && !healthy; i++) {
try {
def healthStatus = sh(
script: "curl -o /dev/null -s -w '%{http_code}' http://${REMOTE_IP}:8080/",
returnStdout: true
).trim()
if (healthStatus == '200') {
healthy = true
echo "WordPress is up and running!"
} else {
throw new Exception("WordPress returned HTTP ${healthStatus}")
}
} catch (Exception e) {
if (i < maxAttempts - 1) {
echo "Health check failed, attempt ${i + 1}/${maxAttempts}. Waiting ${waitTime} seconds before retry..."
// If WordPress is down, try to start it
sshagent(['sshkey-jenkins']) {
sh """
ssh ${REMOTE_USER}@${REMOTE_IP} '
cd ${WORDPRESS_PATH}
docker compose up -d
'
"""
}
sleep(waitTime)
} else {
error "WordPress health check failed after ${maxAttempts} attempts: ${e.message}"
}
}
}
}
}
}
stage('Backup WordPress') {
steps {
sshagent(['sshkey-jenkins']) {
script {
def retryCount = 3
def retryDelay = 10
for (int i = 0; i < retryCount; i++) {
try {
sh '''
echo 'Ensuring backup directory exists...'
ssh ${REMOTE_USER}@${REMOTE_IP} "[ ! -d ~/wordpress_backups ] && mkdir -p ~/wordpress_backups || echo 'Backup directory already exists'"
echo 'Backing up WordPress files...'
ssh ${REMOTE_USER}@${REMOTE_IP} "docker exec ${WORDPRESS_CONTAINER} tar -czf /tmp/wordpress_files_backup.tar.gz -C /var/www/html . && docker cp ${WORDPRESS_CONTAINER}:/tmp/wordpress_files_backup.tar.gz ~/wordpress_backups/"
echo 'Dumping WordPress database...'
ssh ${REMOTE_USER}@${REMOTE_IP} "docker exec ${WORDPRESS_DB_CONTAINER} mysqldump -u \${MYSQL_CREDENTIALS_USR} -p\${MYSQL_CREDENTIALS_PSW} ${WORDPRESS_DB} > ~/wordpress_backups/wordpress_backup.sql --no-tablespaces"
echo 'Copying backups to Jenkins workspace...'
scp ${REMOTE_USER}@${REMOTE_IP}:~/wordpress_backups/wordpress_backup.sql .
scp ${REMOTE_USER}@${REMOTE_IP}:~/wordpress_backups/wordpress_files_backup.tar.gz .
echo 'Stopping WordPress container...'
ssh ${REMOTE_USER}@${REMOTE_IP} "docker stop ${WORDPRESS_CONTAINER}"
'''
break
} catch (Exception e) {
if (i < retryCount - 1) {
echo "Error during backup, retrying in ${retryDelay} seconds..."
sleep(retryDelay)
} else {
echo "Maximum retries reached, aborting backup."
throw e
}
}
}
}
}
}
}
stage('Verify WordPress Image') {
steps {
sshagent(['sshkey-jenkins']) {
script {
// Store current image ID before potential update
sh "ssh ${REMOTE_USER}@${REMOTE_IP} \"docker inspect ${WORDPRESS_CONTAINER} --format='{{.Image}}' > ~/current_image_id\""
env.CURRENT_IMAGE_ID = sh(
script: "ssh ${REMOTE_USER}@${REMOTE_IP} \"docker inspect --format='{{.Image}}' ${WORDPRESS_CONTAINER}\"",
returnStdout: true
).trim()
env.LATEST_IMAGE_ID = sh(
script: "ssh ${REMOTE_USER}@${REMOTE_IP} \"docker inspect --format='{{.Id}}' wordpress:latest\"",
returnStdout: true
).trim()
echo "Current WordPress Image ID: ${env.CURRENT_IMAGE_ID}"
echo "Latest WordPress Image ID: ${env.LATEST_IMAGE_ID}"
// If no update needed, restart the stopped container
if (env.CURRENT_IMAGE_ID == env.LATEST_IMAGE_ID) {
sh "ssh ${REMOTE_USER}@${REMOTE_IP} \"docker start ${WORDPRESS_CONTAINER}\""
}
}
}
}
}
stage('Pull Latest WordPress and Restart') {
when {
expression { env.CURRENT_IMAGE_ID != env.LATEST_IMAGE_ID }
}
steps {
sshagent(['sshkey-jenkins']) {
script {
try {
sh '''
echo 'Pulling the latest WordPress image and restarting containers...'
ssh ${REMOTE_USER}@${REMOTE_IP} "
cd ${WORDPRESS_PATH}
docker compose down
docker pull wordpress:latest
docker compose up -d
"
# Wait for containers to start
sleep 30
'''
// Additional verification after starting containers
def containerStatus = sh(
script: "ssh ${REMOTE_USER}@${REMOTE_IP} \"docker ps | grep ${WORDPRESS_CONTAINER}\"",
returnStatus: true
)
if (containerStatus != 0) {
error("WordPress container failed to start")
}
} catch (Exception e) {
echo "Failed to restart WordPress: ${e.message}"
currentBuild.result = 'FAILURE'
throw e
}
}
}
}
}
stage('Health Check') {
when {
expression { env.CURRENT_IMAGE_ID != env.LATEST_IMAGE_ID }
}
steps {
script {
def maxRetries = 5
def retryDelay = 10
def healthy = false
for (int i = 0; i < maxRetries && !healthy; i++) {
def healthStatus = sh(
script: "curl -o /dev/null -s -w '%{http_code}' http://${REMOTE_IP}:8080/",
returnStatus: true
)
if (healthStatus == 0) {
healthy = true
echo "Health check passed!"
} else {
if (i < maxRetries - 1) {
echo "Health check failed, retrying in ${retryDelay} seconds..."
sleep(retryDelay)
} else {
echo "Health check failed after ${maxRetries} attempts. Rolling back..."
currentBuild.result = 'FAILURE'
error("Health check failed")
}
}
}
}
}
}
stage('Rollback on Failure') {
when {
expression { currentBuild.result == 'FAILURE' }
}
steps {
sshagent(['sshkey-jenkins']) {
script {
// Get the stored previous image ID
def previousImageId = sh(
script: "ssh ${REMOTE_USER}@${REMOTE_IP} 'cat ~/current_image_id'",
returnStdout: true
).trim()
sh """
echo 'Performing rollback...'
ssh ${REMOTE_USER}@${REMOTE_IP} "
cd ${WORDPRESS_PATH}
echo 'Stopping current containers...'
docker compose down
echo 'Restoring database...'
docker compose up -d db
sleep 10
docker exec ${WORDPRESS_DB_CONTAINER} mysql -u \${MYSQL_CREDENTIALS_USR} -p\${MYSQL_CREDENTIALS_PSW} ${WORDPRESS_DB} < ~/wordpress_backups/wordpress_backup.sql
echo 'Restoring WordPress files and container...'
docker compose up -d wordpress
docker cp ~/wordpress_backups/wordpress_files_backup.tar.gz ${WORDPRESS_CONTAINER}:/tmp/
docker exec ${WORDPRESS_CONTAINER} tar -xzf /tmp/wordpress_files_backup.tar.gz -C /var/www/html
echo 'Rollback completed!'
"
"""
}
}
}
}
}
post {
success {
script {
if (env.CURRENT_IMAGE_ID == env.LATEST_IMAGE_ID) {
echo """*************************************************************
WordPress is already at the latest version (${env.LATEST_IMAGE_ID})
No upgrade was necessary at this time.
*************************************************************"""
} else {
echo """*************************************************************
WordPress successfully upgraded to the latest version!
Previous Image ID: ${env.CURRENT_IMAGE_ID}
New Image ID: ${env.LATEST_IMAGE_ID}
*************************************************************"""
}
}
}
failure {
echo """*************************************************************
WordPress upgrade FAILED
Rollback has been completed.
Please check the build logs for detailed error information.
*************************************************************"""
}
always {
echo "Pipeline completed at ${new Date().format('yyyy-MM-dd HH:mm:ss')}"
}
}
}
Here’s the console output of the fixed pipeline if you wish to see how it worked :
The End Game ๐
You’ve just:
- Set up two whole servers (period)
- Made Jenkins and Portainer besties
- Created a pipeline that actually works
- Automated WordPress updates (work smarter not harder)
Remember:
- Keep them passwords strong (no “password123” allowed)
- Test your backups (trust issues are good actually)
- Monitor them logs (stay woke)
- Update your documentation (because future you will thank you)
Now go touch grass while your automation does the work! ๐ฑ