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/healthwithin 60s, rollback triggers automatically.
- 
Cache-busting frontend β Docker build injects a unique BUILD_IDintoindex.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;"]
β‘οΈ 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"]
β‘οΈ 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:
β‘οΈ 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}"
          '''
        }
      }
    }
  }
}
π’ 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}"
β‘οΈ 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/;
  }
}
Then:
sudo ln -s /etc/nginx/sites-available/chattingo /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
β images:
β¨ 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)