DEV Community

Cover image for πŸš€ Zero-Downtime Deployments with Jenkins, Docker & Hostinger VPS (Gradle + React + MySQL)
Tanvir Mulla
Tanvir Mulla

Posted on

πŸš€ Zero-Downtime Deployments with Jenkins, Docker & Hostinger VPS (Gradle + React + MySQL)

Hackathons are fun, but stressful β€” especially when your site must stay live until the judging ends.
Here’s a complete copy-resistant, step-by-step deployment guide you can drop into your repo and follow.

It’s designed around:

  • Gradle backend (Spring Boot)
  • React frontend (Nginx)
  • MySQL DB
  • Multi-stage Docker builds
  • Jenkins CI/CD
  • Hostinger VPS (Ubuntu)
  • Blue/Green-style release strategy (atomic swap + health-gated rollback)

By the end, you’ll have a live, SSL-enabled site with short downtime, safe rollback, and reproducible builds.


πŸ”‘ What’s Unique in This Approach

  • Deterministic image tags β†’ YYYYmmddHHMM-<gitshort> (e.g., 202509041530-1a2b3c4).
  • Atomic release swap β†’ Jenkins deploys to /opt/chattingo/releases/<TAG>, then atomically swaps to /opt/chattingo/current.
  • Health-gated rollback β†’ If the new release fails /actuator/health within 60s, rollback triggers automatically.
  • Cache-busting frontend β†’ Docker build injects a unique BUILD_ID into index.html.
  • Minimal downtime β†’ Users never see a broken app.
  • Reproducible & transparent β†’ All scripts, configs, and Jenkinsfile are in the repo.

πŸ› οΈ Prerequisites

Before starting, fill in these placeholders with your values:

  • YOUR_DOCKERHUB_USER β†’ DockerHub username
  • YOUR_DOMAIN β†’ domain you own (for SSL)
  • YOUR_VPS_IP β†’ Hostinger VPS IP
  • SSH_USER β†’ VPS user (e.g., ubuntu)
  • GIT_BRANCH β†’ branch used for deployment (e.g., devops-implementation)
  • JENKINS_CRED_IDS β†’ Jenkins credentials (dockerhub-user/pass, vps-ssh)

πŸ—οΈ Docker & Compose Setup

1. Frontend Dockerfile

Injects build ID for cache busting:

FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
ARG BUILD_ID
RUN if [ -f public/index.html ]; then sed -i "s/__BUILD_ID__/${BUILD_ID}/g" public/index.html || true; fi
RUN npm run build

FROM nginx:stable-alpine
COPY --from=build /app/build /usr/share/nginx/html
COPY nginx.frontend.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Enter fullscreen mode Exit fullscreen mode

➑️ Add __BUILD_ID__ in your public/index.html to avoid stale caches.


2. Backend Dockerfile (Gradle multi-stage)

FROM gradle:8.3-jdk17 AS builder
WORKDIR /app
COPY build.gradle settings.gradle ./ 
COPY gradle ./gradle
COPY src ./src
RUN gradle clean build -x test --no-daemon

FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
EXPOSE 8080
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=5 \
  CMD curl -f http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java","-jar","/app/app.jar"]
Enter fullscreen mode Exit fullscreen mode

➑️ No need to install Gradle on Jenkins β€” build happens inside the container.


3. docker-compose.yml (release template)

version: "3.8"
services:
  db:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpass}
      MYSQL_DATABASE: ${MYSQL_DATABASE:-chattingo}
    volumes:
      - db_data:/var/lib/mysql
    restart: unless-stopped

  backend:
    image: YOUR_DOCKERHUB_USER/chattingo-backend:PLACEHOLDER_TAG
    environment:
      SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/${MYSQL_DATABASE}
      SPRING_DATASOURCE_USERNAME: root
      SPRING_DATASOURCE_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    depends_on:
      - db
    ports:
      - "8080:8080"
    restart: unless-stopped

  frontend:
    image: YOUR_DOCKERHUB_USER/chattingo-frontend:PLACEHOLDER_TAG
    depends_on:
      - backend
    ports:
      - "3001:80"
    restart: unless-stopped

volumes:
  db_data:
Enter fullscreen mode Exit fullscreen mode

➑️ Jenkins replaces PLACEHOLDER_TAG with your unique $TAG.


πŸ”„ Jenkins Pipeline (CI/CD)

Your Jenkinsfile:

pipeline {
  agent any
  environment {
    IMAGE_PREFIX = "YOUR_DOCKERHUB_USER"
    VPS_SSH = "vps-ssh"
    BRANCH = "GIT_BRANCH"
  }
  stages {
    stage('Checkout') { steps { checkout scm } }
    stage('Set TAG') {
      steps {
        script {
          env.TAG = sh(
            script: "echo $(date +%Y%m%d%H%M)-$(git rev-parse --short HEAD)", 
            returnStdout: true
          ).trim()
        }
      }
    }
    stage('Build Frontend Image') {
      steps {
        sh "docker build --build-arg BUILD_ID=${TAG} -t ${IMAGE_PREFIX}/chattingo-frontend:${TAG} ./frontend"
      }
    }
    stage('Build Backend Image') {
      steps {
        sh "docker build -t ${IMAGE_PREFIX}/chattingo-backend:${TAG} ./backend"
      }
    }
    stage('Push Images') {
      steps {
        withCredentials([usernamePassword(credentialsId: 'dockerhub-creds', usernameVariable: 'USER', passwordVariable: 'PASS')]) {
          sh '''
            echo $PASS | docker login -u $USER --password-stdin
            docker push ${IMAGE_PREFIX}/chattingo-frontend:${TAG}
            docker push ${IMAGE_PREFIX}/chattingo-backend:${TAG}
          '''
        }
      }
    }
    stage('Deploy to VPS') {
      steps {
        sshagent (credentials: [env.VPS_SSH]) {
          sh '''
            scp docker-compose.yml ${SSH_USER}@${YOUR_VPS_IP}:/opt/chattingo/releases/${TAG}/docker-compose.yml
            ssh ${SSH_USER}@${YOUR_VPS_IP} "sudo /usr/local/bin/deploy_release.sh ${TAG}"
          '''
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

🟒 Atomic Deployment Script (on VPS)

Save as /usr/local/bin/deploy_release.sh:

#!/usr/bin/env bash
set -euo pipefail
TAG="$1"
RELEASE_DIR="/opt/chattingo/releases/${TAG}"
CURRENT_DIR="/opt/chattingo/current"
PREV_DIR="/opt/chattingo/previous"
TIMEOUT=60
HEALTH_URL="https://YOUR_DOMAIN/actuator/health"

if [ ! -d "${RELEASE_DIR}" ]; then exit 2; fi

# Swap logic
[ -d "${CURRENT_DIR}" ] && mv "${CURRENT_DIR}" "${PREV_DIR}"
cp -r "${RELEASE_DIR}" "${CURRENT_DIR}"
cd "${CURRENT_DIR}"
docker compose up -d --remove-orphans

# Health gating
SECONDS=0
until curl -fsS "${HEALTH_URL}" >/dev/null; do
  sleep 5
  [ $SECONDS -ge $TIMEOUT ] && {
    docker compose down || true
    [ -d "${PREV_DIR}" ] && mv "${PREV_DIR}" "${CURRENT_DIR}" && cd "${CURRENT_DIR}" && docker compose up -d
    exit 3
  }
done

rm -rf "${PREV_DIR}"
Enter fullscreen mode Exit fullscreen mode

➑️ This ensures instant rollback if your site fails health checks.


🌐 Nginx Reverse Proxy

Place at /etc/nginx/sites-available/chattingo:

server {
  listen 80;
  server_name YOUR_DOMAIN;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl;
  server_name YOUR_DOMAIN;

  ssl_certificate /etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem;

  location / {
    proxy_pass http://127.0.0.1:3001/;
  }
  location /api/ {
    proxy_pass http://127.0.0.1:8080/api/;
  }
}
Enter fullscreen mode Exit fullscreen mode

Then:

sudo ln -s /etc/nginx/sites-available/chattingo /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Enter fullscreen mode Exit fullscreen mode

βœ… images:

Tital
Pipeline

Output

Output

✨ Thanks for reading! If this guide helped you,
πŸ‘‰ Like ❀️ the post to support my work
πŸ‘‰ Comment πŸ’¬ your thoughts, questions, or improvements
πŸ‘‰ Share πŸ”— with friends or teammates working on CI/CD or hackathon projects

Let’s keep building and learning together πŸš€

Top comments (0)