fix: I dunno, I'm making git server changes
This commit is contained in:
57
.github/workflows/build-and-push-image.yml
vendored
57
.github/workflows/build-and-push-image.yml
vendored
@@ -1,57 +0,0 @@
|
|||||||
name: Build and Push Images to Dockerhub and GHCR
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["main"]
|
|
||||||
release:
|
|
||||||
types: ["published"]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
service: [backend, frontend]
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: "${{ secrets.DH_USER }}"
|
|
||||||
password: "${{ secrets.DH_PASS }}"
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: "${{ secrets.GHCR_USER }}"
|
|
||||||
password: "${{ secrets.GHCR_PAT }}"
|
|
||||||
|
|
||||||
- name: Docker Metadata
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
context: workflow
|
|
||||||
images: |
|
|
||||||
ericgullickson/motovaultpro-${{ matrix.service }}
|
|
||||||
ghcr.io/ericgullickson/motovaultpro-${{ matrix.service }}
|
|
||||||
tags: |
|
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
|
||||||
type=ref,event=tag
|
|
||||||
labels: |
|
|
||||||
org.opencontainers.image.title=MotoVaultPro ${{ matrix.service }}
|
|
||||||
org.opencontainers.image.description=MotoVaultPro ${{ matrix.service }} service
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: ./${{ matrix.service }}
|
|
||||||
platforms: linux/amd64
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
448
.gitlab-ci.yml
448
.gitlab-ci.yml
@@ -1,32 +1,50 @@
|
|||||||
# MotoVaultPro GitLab CI/CD Pipeline
|
# MotoVaultPro GitLab CI/CD Pipeline - Blue-Green Deployment
|
||||||
# GitLab 18.6+ with shell executor
|
# GitLab 18.6+ with separate build and production runners
|
||||||
# See docs/CICD-DEPLOY.md for complete documentation
|
# See docs/CICD-DEPLOY.md for complete documentation
|
||||||
# v1.6
|
# v2.0 - Blue-Green with Auto-Rollback
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- validate
|
- validate
|
||||||
- build
|
- build
|
||||||
- deploy
|
- deploy-prepare
|
||||||
|
- deploy-switch
|
||||||
- verify
|
- verify
|
||||||
|
- rollback
|
||||||
|
- notify
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
# Use stable clone path instead of runner-specific path
|
# Registry configuration
|
||||||
GIT_CLONE_PATH: $CI_BUILDS_DIR/motovaultpro
|
REGISTRY: registry.motovaultpro.com
|
||||||
DEPLOY_PATH: $CI_BUILDS_DIR/motovaultpro
|
REGISTRY_MIRRORS: ${REGISTRY}/mirrors
|
||||||
DOCKER_COMPOSE_FILE: docker-compose.yml
|
IMAGE_TAG: ${CI_COMMIT_SHORT_SHA}
|
||||||
DOCKER_COMPOSE_PROD_FILE: docker-compose.prod.yml
|
BACKEND_IMAGE: ${REGISTRY}/motovaultpro/backend:${IMAGE_TAG}
|
||||||
|
FRONTEND_IMAGE: ${REGISTRY}/motovaultpro/frontend:${IMAGE_TAG}
|
||||||
|
|
||||||
# Fix permissions after every job - docker creates files as root
|
# Deployment configuration
|
||||||
|
GIT_CLONE_PATH: ${CI_BUILDS_DIR}/motovaultpro
|
||||||
|
DEPLOY_PATH: ${CI_BUILDS_DIR}/motovaultpro
|
||||||
|
COMPOSE_FILE: docker-compose.yml
|
||||||
|
COMPOSE_BLUE_GREEN: docker-compose.blue-green.yml
|
||||||
|
|
||||||
|
# Health check configuration
|
||||||
|
HEALTH_CHECK_TIMEOUT: "60"
|
||||||
|
|
||||||
|
# Default after_script to fix permissions
|
||||||
default:
|
default:
|
||||||
after_script:
|
after_script:
|
||||||
- echo "Fixing file permissions..."
|
- echo "Fixing file permissions..."
|
||||||
- sudo chown -R gitlab-runner:gitlab-runner "$DEPLOY_PATH" 2>/dev/null || true
|
- sudo chown -R gitlab-runner:gitlab-runner "$DEPLOY_PATH" 2>/dev/null || true
|
||||||
# Keep data directories owned by container user
|
|
||||||
- sudo chown -R 1001:1001 "$DEPLOY_PATH/data/backups" "$DEPLOY_PATH/data/documents" 2>/dev/null || true
|
- sudo chown -R 1001:1001 "$DEPLOY_PATH/data/backups" "$DEPLOY_PATH/data/documents" 2>/dev/null || true
|
||||||
|
|
||||||
# Validate Stage - Check prerequisites
|
# ============================================
|
||||||
|
# Stage 1: VALIDATE
|
||||||
|
# Check prerequisites before starting pipeline
|
||||||
|
# ============================================
|
||||||
validate:
|
validate:
|
||||||
stage: validate
|
stage: validate
|
||||||
|
tags:
|
||||||
|
- production
|
||||||
|
- shell
|
||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
script:
|
script:
|
||||||
@@ -34,129 +52,385 @@ validate:
|
|||||||
- echo "Validating deployment prerequisites..."
|
- echo "Validating deployment prerequisites..."
|
||||||
- echo "=========================================="
|
- echo "=========================================="
|
||||||
- echo "Checking Docker..."
|
- echo "Checking Docker..."
|
||||||
- 'docker info > /dev/null 2>&1 || (echo "ERROR: Docker not accessible" && exit 1)'
|
- docker info > /dev/null 2>&1 || (echo "ERROR - Docker not accessible" && exit 1)
|
||||||
- echo "OK Docker is accessible"
|
- echo "OK - Docker is accessible"
|
||||||
- echo "Checking Docker Compose..."
|
- echo "Checking Docker Compose..."
|
||||||
- 'docker compose version > /dev/null 2>&1 || (echo "ERROR: Docker Compose not available" && exit 1)'
|
- docker compose version > /dev/null 2>&1 || (echo "ERROR - Docker Compose not available" && exit 1)
|
||||||
- echo "OK Docker Compose is available"
|
- echo "OK - Docker Compose is available"
|
||||||
- echo "Checking deployment path..."
|
- echo "Checking deployment path..."
|
||||||
- 'test -d "$DEPLOY_PATH" || (echo "ERROR: DEPLOY_PATH not found" && exit 1)'
|
- test -d "$DEPLOY_PATH" || (echo "ERROR - DEPLOY_PATH not found" && exit 1)
|
||||||
- echo "OK Deployment path exists"
|
- echo "OK - Deployment path exists"
|
||||||
|
- echo "Checking registry access..."
|
||||||
|
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$REGISTRY" || true
|
||||||
|
- echo "OK - Registry authentication configured"
|
||||||
|
- echo "Determining target stack..."
|
||||||
|
- |
|
||||||
|
STATE_FILE="$DEPLOY_PATH/config/deployment/state.json"
|
||||||
|
if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then
|
||||||
|
ACTIVE_STACK=$(jq -r '.active_stack // "blue"' "$STATE_FILE")
|
||||||
|
if [ "$ACTIVE_STACK" = "blue" ]; then
|
||||||
|
echo "TARGET_STACK=green" >> deploy.env
|
||||||
|
else
|
||||||
|
echo "TARGET_STACK=blue" >> deploy.env
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "TARGET_STACK=green" >> deploy.env
|
||||||
|
fi
|
||||||
|
cat deploy.env
|
||||||
- echo "=========================================="
|
- echo "=========================================="
|
||||||
- echo "Validation complete"
|
- echo "Validation complete"
|
||||||
- echo "=========================================="
|
- echo "=========================================="
|
||||||
|
artifacts:
|
||||||
|
reports:
|
||||||
|
dotenv: deploy.env
|
||||||
|
|
||||||
# Build Stage - Build Docker images
|
# ============================================
|
||||||
|
# Stage 2: BUILD
|
||||||
|
# Build and push images to GitLab Container Registry
|
||||||
|
# Runs on dedicated build server (shell executor)
|
||||||
|
# ============================================
|
||||||
build:
|
build:
|
||||||
stage: build
|
stage: build
|
||||||
|
tags:
|
||||||
|
- build
|
||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
script:
|
script:
|
||||||
|
- echo "Authenticating with GitLab Container Registry..."
|
||||||
|
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$REGISTRY"
|
||||||
- echo "=========================================="
|
- echo "=========================================="
|
||||||
- echo "Building Docker images..."
|
- echo "Building Docker images..."
|
||||||
|
- echo "Commit - ${CI_COMMIT_SHORT_SHA}"
|
||||||
|
- echo "Backend - ${BACKEND_IMAGE}"
|
||||||
|
- echo "Frontend - ${FRONTEND_IMAGE}"
|
||||||
- echo "=========================================="
|
- echo "=========================================="
|
||||||
- cd "$DEPLOY_PATH"
|
|
||||||
- echo "Building images..."
|
# Build backend
|
||||||
- docker compose -f $DOCKER_COMPOSE_FILE build --no-cache
|
- echo "Building backend..."
|
||||||
|
- |
|
||||||
|
docker build \
|
||||||
|
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||||
|
--cache-from ${REGISTRY}/motovaultpro/backend:latest \
|
||||||
|
-t ${BACKEND_IMAGE} \
|
||||||
|
-t ${REGISTRY}/motovaultpro/backend:latest \
|
||||||
|
-f backend/Dockerfile \
|
||||||
|
.
|
||||||
|
|
||||||
|
# Build frontend
|
||||||
|
- echo "Building frontend..."
|
||||||
|
- |
|
||||||
|
docker build \
|
||||||
|
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||||
|
--build-arg VITE_AUTH0_DOMAIN=${VITE_AUTH0_DOMAIN:-motovaultpro.us.auth0.com} \
|
||||||
|
--build-arg VITE_AUTH0_CLIENT_ID=${VITE_AUTH0_CLIENT_ID:-yspR8zdnSxmV8wFIghHynQ08iXAPoQJ3} \
|
||||||
|
--build-arg VITE_AUTH0_AUDIENCE=${VITE_AUTH0_AUDIENCE:-https://api.motovaultpro.com} \
|
||||||
|
--build-arg VITE_API_BASE_URL=/api \
|
||||||
|
--cache-from ${REGISTRY}/motovaultpro/frontend:latest \
|
||||||
|
-t ${FRONTEND_IMAGE} \
|
||||||
|
-t ${REGISTRY}/motovaultpro/frontend:latest \
|
||||||
|
-f frontend/Dockerfile \
|
||||||
|
frontend
|
||||||
|
|
||||||
|
# Push images
|
||||||
|
- echo "Pushing images to registry..."
|
||||||
|
- docker push ${BACKEND_IMAGE}
|
||||||
|
- docker push ${FRONTEND_IMAGE}
|
||||||
|
- docker push ${REGISTRY}/motovaultpro/backend:latest
|
||||||
|
- docker push ${REGISTRY}/motovaultpro/frontend:latest
|
||||||
|
|
||||||
- echo "=========================================="
|
- echo "=========================================="
|
||||||
- echo "Build complete"
|
- echo "Build complete"
|
||||||
- echo "=========================================="
|
- echo "=========================================="
|
||||||
|
|
||||||
# Deploy Stage - Inject secrets and deploy services
|
# ============================================
|
||||||
deploy:
|
# Stage 3: DEPLOY-PREPARE
|
||||||
stage: deploy
|
# Pull images, start inactive stack, run health checks
|
||||||
|
# ============================================
|
||||||
|
deploy-prepare:
|
||||||
|
stage: deploy-prepare
|
||||||
|
tags:
|
||||||
|
- production
|
||||||
|
- shell
|
||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
|
needs:
|
||||||
|
- job: validate
|
||||||
|
artifacts: true
|
||||||
|
- job: build
|
||||||
environment:
|
environment:
|
||||||
name: production
|
name: production
|
||||||
url: https://motovaultpro.com
|
url: https://motovaultpro.com
|
||||||
script:
|
script:
|
||||||
- echo "=========================================="
|
- echo "=========================================="
|
||||||
- echo "Deploying MotoVaultPro..."
|
- echo "Preparing deployment to ${TARGET_STACK} stack..."
|
||||||
- echo "=========================================="
|
- echo "=========================================="
|
||||||
- cd "$DEPLOY_PATH"
|
- cd "$DEPLOY_PATH"
|
||||||
- echo "Step 1/8 Initializing data directories..."
|
|
||||||
|
# Authenticate with registry
|
||||||
|
- echo "Authenticating with registry..."
|
||||||
|
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$REGISTRY"
|
||||||
|
|
||||||
|
# Inject secrets
|
||||||
|
- echo "Step 1/5 - Injecting secrets..."
|
||||||
|
- chmod +x scripts/inject-secrets.sh
|
||||||
|
- ./scripts/inject-secrets.sh
|
||||||
|
|
||||||
|
# Initialize data directories
|
||||||
|
- echo "Step 2/5 - Initializing data directories..."
|
||||||
- sudo mkdir -p data/backups data/documents
|
- sudo mkdir -p data/backups data/documents
|
||||||
- sudo chown -R 1001:1001 data/backups data/documents
|
- sudo chown -R 1001:1001 data/backups data/documents
|
||||||
- sudo chmod 755 data/backups data/documents
|
- sudo chmod 755 data/backups data/documents
|
||||||
- echo "Step 2/8 Injecting secrets..."
|
|
||||||
- chmod +x scripts/inject-secrets.sh
|
# Pull new images
|
||||||
- ./scripts/inject-secrets.sh
|
- echo "Step 3/5 - Pulling images..."
|
||||||
- echo "Step 3/8 Stopping existing services..."
|
- docker pull ${BACKEND_IMAGE}
|
||||||
- docker compose -f $DOCKER_COMPOSE_FILE -f $DOCKER_COMPOSE_PROD_FILE down --timeout 30 || true
|
- docker pull ${FRONTEND_IMAGE}
|
||||||
- echo "Step 4/8 Pulling base images..."
|
|
||||||
- docker compose -f $DOCKER_COMPOSE_FILE pull
|
# Start inactive stack
|
||||||
- echo "Step 5/8 Starting database services..."
|
- echo "Step 4/5 - Starting ${TARGET_STACK} stack..."
|
||||||
- docker compose -f $DOCKER_COMPOSE_FILE -f $DOCKER_COMPOSE_PROD_FILE up -d mvp-postgres mvp-redis
|
- |
|
||||||
- echo "Waiting for database to be ready..."
|
export BACKEND_IMAGE=${BACKEND_IMAGE}
|
||||||
- sleep 15
|
export FRONTEND_IMAGE=${FRONTEND_IMAGE}
|
||||||
- echo "Step 6/8 Running database migrations..."
|
docker compose -f $COMPOSE_FILE -f $COMPOSE_BLUE_GREEN up -d \
|
||||||
- docker compose -f $DOCKER_COMPOSE_FILE run --rm mvp-backend npm run migrate || echo "Migration skipped"
|
mvp-frontend-${TARGET_STACK} mvp-backend-${TARGET_STACK}
|
||||||
- echo "Step 7/8 Vehicle catalog data..."
|
|
||||||
# Schema and data now loaded via standard migration system
|
# Wait for stack to be ready
|
||||||
# Migration runner handles table creation and data loading automatically
|
- echo "Step 5/5 - Waiting for stack health..."
|
||||||
- echo "Vehicle catalog loaded via platform feature migration"
|
- sleep 10
|
||||||
- echo "Flushing Redis cache..."
|
|
||||||
- docker exec mvp-redis redis-cli FLUSHALL
|
# Run health check
|
||||||
- echo "Step 8/8 Starting all services..."
|
- echo "Running health check on ${TARGET_STACK} stack..."
|
||||||
- docker compose -f $DOCKER_COMPOSE_FILE -f $DOCKER_COMPOSE_PROD_FILE up -d
|
- chmod +x scripts/ci/health-check.sh
|
||||||
- echo "Waiting for services to initialize..."
|
- ./scripts/ci/health-check.sh ${TARGET_STACK} ${HEALTH_CHECK_TIMEOUT}
|
||||||
- sleep 30
|
|
||||||
|
# Update state with deployment info
|
||||||
|
- |
|
||||||
|
STATE_FILE="config/deployment/state.json"
|
||||||
|
if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then
|
||||||
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
jq --arg stack "$TARGET_STACK" \
|
||||||
|
--arg commit "$CI_COMMIT_SHORT_SHA" \
|
||||||
|
--arg ts "$TIMESTAMP" \
|
||||||
|
'.[$stack].version = $commit | .[$stack].commit = $commit | .[$stack].deployed_at = $ts | .[$stack].healthy = true' \
|
||||||
|
"$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
- echo "=========================================="
|
- echo "=========================================="
|
||||||
- echo "Deployment complete"
|
- echo "Deploy preparation complete"
|
||||||
- echo "=========================================="
|
- echo "=========================================="
|
||||||
|
|
||||||
# Verify Stage - Health checks
|
# ============================================
|
||||||
verify:
|
# Stage 4: DEPLOY-SWITCH
|
||||||
stage: verify
|
# Switch traffic to new stack
|
||||||
|
# ============================================
|
||||||
|
deploy-switch:
|
||||||
|
stage: deploy-switch
|
||||||
|
tags:
|
||||||
|
- production
|
||||||
|
- shell
|
||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
|
needs:
|
||||||
|
- job: validate
|
||||||
|
artifacts: true
|
||||||
|
- job: deploy-prepare
|
||||||
script:
|
script:
|
||||||
- echo "=========================================="
|
- echo "=========================================="
|
||||||
- echo "Verifying deployment..."
|
- echo "Switching traffic to ${TARGET_STACK} stack..."
|
||||||
- echo "=========================================="
|
- echo "=========================================="
|
||||||
- cd "$DEPLOY_PATH"
|
- cd "$DEPLOY_PATH"
|
||||||
- echo "Checking container status..."
|
|
||||||
|
# Switch traffic
|
||||||
|
- chmod +x scripts/ci/switch-traffic.sh
|
||||||
|
- ./scripts/ci/switch-traffic.sh ${TARGET_STACK} instant
|
||||||
|
|
||||||
|
# Update state
|
||||||
- |
|
- |
|
||||||
FAILED=0
|
STATE_FILE="config/deployment/state.json"
|
||||||
for service in mvp-traefik mvp-frontend mvp-backend mvp-postgres mvp-redis; do
|
if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then
|
||||||
status=$(docker inspect --format='{{.State.Status}}' $service 2>/dev/null || echo "not found")
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
if [ "$status" != "running" ]; then
|
jq --arg commit "$CI_COMMIT_SHORT_SHA" \
|
||||||
echo "ERROR: $service is not running (status: $status)"
|
--arg ts "$TIMESTAMP" \
|
||||||
docker logs $service --tail 50 2>/dev/null || true
|
'.last_deployment = $ts | .last_deployment_commit = $commit | .last_deployment_status = "success" | .rollback_available = true' \
|
||||||
FAILED=1
|
"$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
|
||||||
else
|
|
||||||
echo "OK: $service is running"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ $FAILED -eq 1 ]; then
|
|
||||||
echo "One or more services failed to start"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
- echo "Checking backend health..."
|
|
||||||
|
- echo "=========================================="
|
||||||
|
- echo "Traffic switch complete"
|
||||||
|
- echo "=========================================="
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Stage 5: VERIFY
|
||||||
|
# Production health verification after switch
|
||||||
|
# ============================================
|
||||||
|
verify:
|
||||||
|
stage: verify
|
||||||
|
tags:
|
||||||
|
- production
|
||||||
|
- shell
|
||||||
|
only:
|
||||||
|
- main
|
||||||
|
needs:
|
||||||
|
- job: validate
|
||||||
|
artifacts: true
|
||||||
|
- job: deploy-switch
|
||||||
|
script:
|
||||||
|
- echo "=========================================="
|
||||||
|
- echo "Verifying production deployment..."
|
||||||
|
- echo "=========================================="
|
||||||
|
- cd "$DEPLOY_PATH"
|
||||||
|
|
||||||
|
# Wait for Traefik to propagate routing
|
||||||
|
- echo "Waiting for traffic routing to stabilize..."
|
||||||
|
- sleep 5
|
||||||
|
|
||||||
|
# Verify via external endpoint
|
||||||
|
- echo "Checking external endpoint..."
|
||||||
- |
|
- |
|
||||||
HEALTH_OK=0
|
|
||||||
for i in 1 2 3 4 5 6; do
|
for i in 1 2 3 4 5 6; do
|
||||||
if docker exec mvp-backend curl -sf http://localhost:3001/health > /dev/null 2>&1; then
|
if curl -sf https://motovaultpro.com/api/health > /dev/null 2>&1; then
|
||||||
echo "OK: Backend health check passed"
|
echo "OK - External health check passed"
|
||||||
HEALTH_OK=1
|
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
echo "Attempt $i/6: Backend not ready, waiting 10s..."
|
if [ $i -eq 6 ]; then
|
||||||
|
echo "ERROR - External health check failed after 6 attempts"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Attempt $i/6 - Waiting 10s..."
|
||||||
sleep 10
|
sleep 10
|
||||||
done
|
done
|
||||||
if [ $HEALTH_OK -eq 0 ]; then
|
|
||||||
echo "ERROR: Backend health check failed after 6 attempts"
|
# Verify container status
|
||||||
docker logs mvp-backend --tail 100
|
- echo "Checking container status..."
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
- echo "Checking frontend..."
|
|
||||||
- |
|
- |
|
||||||
if docker compose -f $DOCKER_COMPOSE_FILE exec -T mvp-frontend curl -sf http://localhost:3000 > /dev/null 2>&1; then
|
for service in mvp-frontend-${TARGET_STACK} mvp-backend-${TARGET_STACK}; do
|
||||||
echo "OK: Frontend is accessible"
|
status=$(docker inspect --format='{{.State.Status}}' $service 2>/dev/null || echo "not found")
|
||||||
else
|
health=$(docker inspect --format='{{.State.Health.Status}}' $service 2>/dev/null || echo "unknown")
|
||||||
echo "WARNING: Frontend check failed (might need Traefik routing)"
|
if [ "$status" != "running" ] || [ "$health" != "healthy" ]; then
|
||||||
fi
|
echo "ERROR - $service is not healthy (status: $status, health: $health)"
|
||||||
|
docker logs $service --tail 50 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "OK - $service is running and healthy"
|
||||||
|
done
|
||||||
|
|
||||||
- echo "=========================================="
|
- echo "=========================================="
|
||||||
- echo "Deployment verified successfully!"
|
- echo "Deployment verified successfully!"
|
||||||
|
- echo "Version ${CI_COMMIT_SHORT_SHA} is now live"
|
||||||
- echo "=========================================="
|
- echo "=========================================="
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Stage 6: ROLLBACK (on failure)
|
||||||
|
# Automatic rollback if verify stage fails
|
||||||
|
# ============================================
|
||||||
|
rollback:
|
||||||
|
stage: rollback
|
||||||
|
tags:
|
||||||
|
- production
|
||||||
|
- shell
|
||||||
|
only:
|
||||||
|
- main
|
||||||
|
when: on_failure
|
||||||
|
needs:
|
||||||
|
- job: validate
|
||||||
|
artifacts: true
|
||||||
|
- job: deploy-switch
|
||||||
|
- job: verify
|
||||||
|
script:
|
||||||
|
- echo "=========================================="
|
||||||
|
- echo "INITIATING AUTO-ROLLBACK"
|
||||||
|
- echo "=========================================="
|
||||||
|
- cd "$DEPLOY_PATH"
|
||||||
|
|
||||||
|
# Run rollback script
|
||||||
|
- chmod +x scripts/ci/auto-rollback.sh
|
||||||
|
- ./scripts/ci/auto-rollback.sh "Verify stage failed - automatic rollback"
|
||||||
|
|
||||||
|
# Update state
|
||||||
|
- |
|
||||||
|
STATE_FILE="config/deployment/state.json"
|
||||||
|
if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then
|
||||||
|
jq '.last_deployment_status = "rolled_back"' "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- echo "=========================================="
|
||||||
|
- echo "Rollback complete"
|
||||||
|
- echo "=========================================="
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Stage 7: NOTIFY
|
||||||
|
# Send deployment notifications
|
||||||
|
# ============================================
|
||||||
|
notify-success:
|
||||||
|
stage: notify
|
||||||
|
tags:
|
||||||
|
- production
|
||||||
|
- shell
|
||||||
|
only:
|
||||||
|
- main
|
||||||
|
needs:
|
||||||
|
- job: verify
|
||||||
|
script:
|
||||||
|
- echo "Sending success notification..."
|
||||||
|
- cd "$DEPLOY_PATH"
|
||||||
|
- chmod +x scripts/ci/notify.sh
|
||||||
|
- ./scripts/ci/notify.sh success "Version ${CI_COMMIT_SHORT_SHA} deployed successfully" ${CI_COMMIT_SHORT_SHA}
|
||||||
|
|
||||||
|
notify-failure:
|
||||||
|
stage: notify
|
||||||
|
tags:
|
||||||
|
- production
|
||||||
|
- shell
|
||||||
|
only:
|
||||||
|
- main
|
||||||
|
when: on_failure
|
||||||
|
needs:
|
||||||
|
- job: build
|
||||||
|
optional: true
|
||||||
|
- job: deploy-prepare
|
||||||
|
optional: true
|
||||||
|
- job: deploy-switch
|
||||||
|
optional: true
|
||||||
|
- job: verify
|
||||||
|
optional: true
|
||||||
|
script:
|
||||||
|
- echo "Sending failure notification..."
|
||||||
|
- cd "$DEPLOY_PATH"
|
||||||
|
- chmod +x scripts/ci/notify.sh
|
||||||
|
- ./scripts/ci/notify.sh failure "Deployment of ${CI_COMMIT_SHORT_SHA} failed" ${CI_COMMIT_SHORT_SHA}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Manual Jobs
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Manual maintenance migration job
|
||||||
|
maintenance-migration:
|
||||||
|
stage: deploy-prepare
|
||||||
|
tags:
|
||||||
|
- production
|
||||||
|
- shell
|
||||||
|
only:
|
||||||
|
- main
|
||||||
|
when: manual
|
||||||
|
script:
|
||||||
|
- echo "=========================================="
|
||||||
|
- echo "MAINTENANCE MODE MIGRATION"
|
||||||
|
- echo "=========================================="
|
||||||
|
- cd "$DEPLOY_PATH"
|
||||||
|
- chmod +x scripts/ci/maintenance-migrate.sh
|
||||||
|
- ./scripts/ci/maintenance-migrate.sh backup
|
||||||
|
|
||||||
|
# Mirror base images (scheduled or manual)
|
||||||
|
mirror-images:
|
||||||
|
stage: build
|
||||||
|
tags:
|
||||||
|
- build
|
||||||
|
only:
|
||||||
|
- schedules
|
||||||
|
- web
|
||||||
|
when: manual
|
||||||
|
script:
|
||||||
|
- echo "Mirroring base images to GitLab Container Registry..."
|
||||||
|
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$REGISTRY"
|
||||||
|
- chmod +x scripts/ci/mirror-base-images.sh
|
||||||
|
- REGISTRY=${REGISTRY}/mirrors ./scripts/ci/mirror-base-images.sh
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
# Production Dockerfile for MotoVaultPro Backend
|
# Production Dockerfile for MotoVaultPro Backend
|
||||||
|
# Uses mirrored base images from GitLab Container Registry
|
||||||
|
|
||||||
|
# Build argument for registry (defaults to GitLab mirrors, falls back to Docker Hub)
|
||||||
|
ARG REGISTRY_MIRRORS=registry.motovaultpro.com/mirrors
|
||||||
|
|
||||||
# Stage 1: Build stage
|
# Stage 1: Build stage
|
||||||
FROM node:lts-alpine AS builder
|
FROM ${REGISTRY_MIRRORS}/node:20-alpine AS builder
|
||||||
|
|
||||||
# Install build dependencies
|
# Install build dependencies
|
||||||
RUN apk add --no-cache dumb-init git curl
|
RUN apk add --no-cache dumb-init git curl
|
||||||
@@ -27,7 +31,7 @@ ENV DOCKER_BUILD=true
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Stage 2: Production runtime
|
# Stage 2: Production runtime
|
||||||
FROM node:lts-alpine AS production
|
FROM ${REGISTRY_MIRRORS}/node:20-alpine AS production
|
||||||
|
|
||||||
# Install runtime dependencies only (postgresql-client for backup/restore)
|
# Install runtime dependencies only (postgresql-client for backup/restore)
|
||||||
RUN apk add --no-cache dumb-init curl postgresql-client
|
RUN apk add --no-cache dumb-init curl postgresql-client
|
||||||
|
|||||||
21
config/deployment/state.json
Normal file
21
config/deployment/state.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"active_stack": "blue",
|
||||||
|
"inactive_stack": "green",
|
||||||
|
"last_deployment": null,
|
||||||
|
"last_deployment_commit": null,
|
||||||
|
"last_deployment_status": null,
|
||||||
|
"blue": {
|
||||||
|
"version": null,
|
||||||
|
"commit": null,
|
||||||
|
"deployed_at": null,
|
||||||
|
"healthy": false
|
||||||
|
},
|
||||||
|
"green": {
|
||||||
|
"version": null,
|
||||||
|
"commit": null,
|
||||||
|
"deployed_at": null,
|
||||||
|
"healthy": false
|
||||||
|
},
|
||||||
|
"rollback_available": false,
|
||||||
|
"maintenance_mode": false
|
||||||
|
}
|
||||||
116
config/traefik/dynamic/blue-green.yml
Normal file
116
config/traefik/dynamic/blue-green.yml
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# Traefik Dynamic Configuration for Blue-Green Deployment
|
||||||
|
# This file is watched by Traefik and reloaded on changes
|
||||||
|
# Traffic weights are updated by scripts/ci/switch-traffic.sh
|
||||||
|
#
|
||||||
|
# Current active stack is determined by weights:
|
||||||
|
# - blue=100, green=0 -> Blue is active
|
||||||
|
# - blue=0, green=100 -> Green is active
|
||||||
|
# - Gradual: 75/25, 50/50, 25/75 for canary deployments
|
||||||
|
|
||||||
|
http:
|
||||||
|
# ========================================
|
||||||
|
# Routers - Route traffic to weighted services
|
||||||
|
# ========================================
|
||||||
|
routers:
|
||||||
|
# Frontend router with weighted service
|
||||||
|
mvp-frontend-bluegreen:
|
||||||
|
rule: "(Host(`motovaultpro.com`) || Host(`www.motovaultpro.com`)) && !PathPrefix(`/api`)"
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
service: mvp-frontend-weighted
|
||||||
|
priority: 10
|
||||||
|
|
||||||
|
# Backend API router with weighted service
|
||||||
|
mvp-backend-bluegreen:
|
||||||
|
rule: "(Host(`motovaultpro.com`) || Host(`www.motovaultpro.com`)) && PathPrefix(`/api`)"
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
service: mvp-backend-weighted
|
||||||
|
priority: 20
|
||||||
|
|
||||||
|
# Health check router (always routes to active stack)
|
||||||
|
mvp-backend-health:
|
||||||
|
rule: "(Host(`motovaultpro.com`) || Host(`www.motovaultpro.com`)) && Path(`/api/health`)"
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
service: mvp-backend-weighted
|
||||||
|
priority: 30
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Services - Weighted load balancers
|
||||||
|
# ========================================
|
||||||
|
services:
|
||||||
|
# Frontend weighted service
|
||||||
|
# Weights are updated by switch-traffic.sh
|
||||||
|
mvp-frontend-weighted:
|
||||||
|
weighted:
|
||||||
|
services:
|
||||||
|
- name: mvp-frontend-blue-svc
|
||||||
|
weight: 100
|
||||||
|
- name: mvp-frontend-green-svc
|
||||||
|
weight: 0
|
||||||
|
healthCheck: {}
|
||||||
|
|
||||||
|
# Backend weighted service
|
||||||
|
# Weights are updated by switch-traffic.sh
|
||||||
|
mvp-backend-weighted:
|
||||||
|
weighted:
|
||||||
|
services:
|
||||||
|
- name: mvp-backend-blue-svc
|
||||||
|
weight: 100
|
||||||
|
- name: mvp-backend-green-svc
|
||||||
|
weight: 0
|
||||||
|
healthCheck: {}
|
||||||
|
|
||||||
|
# Individual stack services
|
||||||
|
mvp-frontend-blue-svc:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://mvp-frontend-blue:3000"
|
||||||
|
healthCheck:
|
||||||
|
path: /
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
passHostHeader: true
|
||||||
|
|
||||||
|
mvp-frontend-green-svc:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://mvp-frontend-green:3000"
|
||||||
|
healthCheck:
|
||||||
|
path: /
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
passHostHeader: true
|
||||||
|
|
||||||
|
mvp-backend-blue-svc:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://mvp-backend-blue:3001"
|
||||||
|
healthCheck:
|
||||||
|
path: /health
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
passHostHeader: true
|
||||||
|
|
||||||
|
mvp-backend-green-svc:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://mvp-backend-green:3001"
|
||||||
|
healthCheck:
|
||||||
|
path: /health
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
passHostHeader: true
|
||||||
|
|
||||||
|
# Maintenance mode service (optional)
|
||||||
|
mvp-maintenance:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://mvp-maintenance:80"
|
||||||
180
config/traefik/dynamic/middleware.yml
Executable file
180
config/traefik/dynamic/middleware.yml
Executable file
@@ -0,0 +1,180 @@
|
|||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
# Security headers middleware
|
||||||
|
secure-headers:
|
||||||
|
headers:
|
||||||
|
accessControlAllowMethods:
|
||||||
|
- GET
|
||||||
|
- OPTIONS
|
||||||
|
- PUT
|
||||||
|
- POST
|
||||||
|
- DELETE
|
||||||
|
accessControlAllowOriginList:
|
||||||
|
- "https://admin.motovaultpro.com"
|
||||||
|
- "https://motovaultpro.com"
|
||||||
|
accessControlMaxAge: 100
|
||||||
|
addVaryHeader: true
|
||||||
|
browserXssFilter: true
|
||||||
|
contentTypeNosniff: true
|
||||||
|
forceSTSHeader: true
|
||||||
|
frameDeny: true
|
||||||
|
stsIncludeSubdomains: true
|
||||||
|
stsPreload: true
|
||||||
|
stsSeconds: 31536000
|
||||||
|
customRequestHeaders:
|
||||||
|
X-Forwarded-Proto: https
|
||||||
|
|
||||||
|
# CORS middleware for API endpoints
|
||||||
|
cors:
|
||||||
|
headers:
|
||||||
|
accessControlAllowCredentials: true
|
||||||
|
accessControlAllowHeaders:
|
||||||
|
- "Authorization"
|
||||||
|
- "Content-Type"
|
||||||
|
- "X-Requested-With"
|
||||||
|
- "X-Tenant-ID"
|
||||||
|
accessControlAllowMethods:
|
||||||
|
- "GET"
|
||||||
|
- "POST"
|
||||||
|
- "PUT"
|
||||||
|
- "DELETE"
|
||||||
|
- "OPTIONS"
|
||||||
|
accessControlAllowOriginList:
|
||||||
|
- "https://admin.motovaultpro.com"
|
||||||
|
- "https://motovaultpro.com"
|
||||||
|
accessControlMaxAge: 100
|
||||||
|
|
||||||
|
# API authentication middleware
|
||||||
|
api-auth:
|
||||||
|
forwardAuth:
|
||||||
|
address: "http://admin-backend:3001/auth/verify"
|
||||||
|
authResponseHeaders:
|
||||||
|
- "X-Auth-User"
|
||||||
|
- "X-Auth-Roles"
|
||||||
|
- "X-Tenant-ID"
|
||||||
|
authRequestHeaders:
|
||||||
|
- "Authorization"
|
||||||
|
- "X-Tenant-ID"
|
||||||
|
trustForwardHeader: true
|
||||||
|
|
||||||
|
# Platform API authentication middleware
|
||||||
|
platform-auth:
|
||||||
|
forwardAuth:
|
||||||
|
address: "http://admin-backend:3001/auth/verify-platform"
|
||||||
|
authResponseHeaders:
|
||||||
|
- "X-Service-Name"
|
||||||
|
- "X-Auth-Scope"
|
||||||
|
authRequestHeaders:
|
||||||
|
- "X-API-Key"
|
||||||
|
- "Authorization"
|
||||||
|
trustForwardHeader: true
|
||||||
|
|
||||||
|
# Rate limiting middleware
|
||||||
|
rate-limit:
|
||||||
|
rateLimit:
|
||||||
|
burst: 100
|
||||||
|
average: 50
|
||||||
|
period: 1m
|
||||||
|
|
||||||
|
# Request/response size limits
|
||||||
|
size-limit:
|
||||||
|
buffering:
|
||||||
|
maxRequestBodyBytes: 26214400 # 25MB
|
||||||
|
maxResponseBodyBytes: 26214400 # 25MB
|
||||||
|
|
||||||
|
# IP whitelist for development (optional)
|
||||||
|
local-ips:
|
||||||
|
ipWhiteList:
|
||||||
|
sourceRange:
|
||||||
|
- "127.0.0.1/32"
|
||||||
|
- "10.0.0.0/8"
|
||||||
|
- "172.16.0.0/12"
|
||||||
|
- "192.168.0.0/16"
|
||||||
|
|
||||||
|
# Advanced security headers for production
|
||||||
|
security-headers-strict:
|
||||||
|
headers:
|
||||||
|
accessControlAllowCredentials: false
|
||||||
|
accessControlAllowMethods:
|
||||||
|
- GET
|
||||||
|
- POST
|
||||||
|
- OPTIONS
|
||||||
|
accessControlAllowOriginList:
|
||||||
|
- "https://admin.motovaultpro.com"
|
||||||
|
- "https://motovaultpro.com"
|
||||||
|
browserXssFilter: true
|
||||||
|
contentTypeNosniff: true
|
||||||
|
customRequestHeaders:
|
||||||
|
X-Forwarded-Proto: https
|
||||||
|
customResponseHeaders:
|
||||||
|
X-Frame-Options: DENY
|
||||||
|
X-Content-Type-Options: nosniff
|
||||||
|
Referrer-Policy: strict-origin-when-cross-origin
|
||||||
|
Permissions-Policy: "geolocation=(), microphone=(), camera=()"
|
||||||
|
forceSTSHeader: true
|
||||||
|
frameDeny: true
|
||||||
|
stsIncludeSubdomains: true
|
||||||
|
stsPreload: true
|
||||||
|
stsSeconds: 31536000
|
||||||
|
|
||||||
|
# Circuit breaker for reliability
|
||||||
|
circuit-breaker:
|
||||||
|
circuitBreaker:
|
||||||
|
expression: "NetworkErrorRatio() > 0.3 || ResponseCodeRatio(500, 600, 0, 600) > 0.3"
|
||||||
|
checkPeriod: 30s
|
||||||
|
fallbackDuration: 10s
|
||||||
|
recoveryDuration: 30s
|
||||||
|
|
||||||
|
# Request retry for resilience
|
||||||
|
retry-policy:
|
||||||
|
retry:
|
||||||
|
attempts: 3
|
||||||
|
initialInterval: 100ms
|
||||||
|
|
||||||
|
# Timeout middleware
|
||||||
|
timeout:
|
||||||
|
timeout: 30s
|
||||||
|
|
||||||
|
# Compress responses for performance
|
||||||
|
compression:
|
||||||
|
compress: {}
|
||||||
|
|
||||||
|
# Health check middleware chain
|
||||||
|
health-check-chain:
|
||||||
|
chain:
|
||||||
|
middlewares:
|
||||||
|
- compression
|
||||||
|
- secure-headers
|
||||||
|
- timeout
|
||||||
|
|
||||||
|
# API middleware chain
|
||||||
|
api-chain:
|
||||||
|
chain:
|
||||||
|
middlewares:
|
||||||
|
- compression
|
||||||
|
- security-headers-strict
|
||||||
|
- cors
|
||||||
|
- rate-limit
|
||||||
|
- api-auth
|
||||||
|
- retry-policy
|
||||||
|
- timeout
|
||||||
|
|
||||||
|
# Platform API middleware chain
|
||||||
|
platform-chain:
|
||||||
|
chain:
|
||||||
|
middlewares:
|
||||||
|
- compression
|
||||||
|
- security-headers-strict
|
||||||
|
- rate-limit
|
||||||
|
- platform-auth
|
||||||
|
- circuit-breaker
|
||||||
|
- retry-policy
|
||||||
|
- timeout
|
||||||
|
|
||||||
|
# Public frontend middleware chain
|
||||||
|
frontend-chain:
|
||||||
|
chain:
|
||||||
|
middlewares:
|
||||||
|
- compression
|
||||||
|
- secure-headers
|
||||||
|
- timeout
|
||||||
@@ -21,7 +21,8 @@ providers:
|
|||||||
exposedByDefault: false
|
exposedByDefault: false
|
||||||
# Network auto-discovery - Traefik will use the networks it's connected to
|
# Network auto-discovery - Traefik will use the networks it's connected to
|
||||||
file:
|
file:
|
||||||
filename: /etc/traefik/middleware.yml
|
# Watch directory for dynamic configuration (blue-green routing, middleware)
|
||||||
|
directory: /etc/traefik/dynamic
|
||||||
watch: true
|
watch: true
|
||||||
|
|
||||||
certificatesResolvers:
|
certificatesResolvers:
|
||||||
|
|||||||
196
docker-compose.blue-green.yml
Normal file
196
docker-compose.blue-green.yml
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
# Blue-Green Deployment Overlay for MotoVaultPro
|
||||||
|
# Usage: docker compose -f docker-compose.yml -f docker-compose.blue-green.yml up -d
|
||||||
|
#
|
||||||
|
# This overlay defines blue and green stacks that share the same database layer.
|
||||||
|
# Traffic routing is handled by Traefik's weighted load balancer.
|
||||||
|
#
|
||||||
|
# Stack naming:
|
||||||
|
# BLUE: mvp-frontend-blue, mvp-backend-blue
|
||||||
|
# GREEN: mvp-frontend-green, mvp-backend-green
|
||||||
|
#
|
||||||
|
# Shared services (from base compose):
|
||||||
|
# mvp-traefik, mvp-postgres, mvp-redis
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ========================================
|
||||||
|
# BLUE Stack - Frontend
|
||||||
|
# ========================================
|
||||||
|
mvp-frontend-blue:
|
||||||
|
image: ${FRONTEND_IMAGE:-registry.motovaultpro.com/motovaultpro/frontend:latest}
|
||||||
|
container_name: mvp-frontend-blue
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
VITE_API_BASE_URL: /api
|
||||||
|
VITE_AUTH0_DOMAIN: ${VITE_AUTH0_DOMAIN:-motovaultpro.us.auth0.com}
|
||||||
|
VITE_AUTH0_CLIENT_ID: ${VITE_AUTH0_CLIENT_ID:-yspR8zdnSxmV8wFIghHynQ08iXAPoQJ3}
|
||||||
|
VITE_AUTH0_AUDIENCE: ${VITE_AUTH0_AUDIENCE:-https://api.motovaultpro.com}
|
||||||
|
SECRETS_DIR: /run/secrets
|
||||||
|
volumes:
|
||||||
|
- ./secrets/app/google-maps-api-key.txt:/run/secrets/google-maps-api-key:ro
|
||||||
|
- ./secrets/app/google-maps-map-id.txt:/run/secrets/google-maps-map-id:ro
|
||||||
|
networks:
|
||||||
|
- frontend
|
||||||
|
depends_on:
|
||||||
|
- mvp-backend-blue
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -sf http://localhost:3000 || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=motovaultpro_frontend"
|
||||||
|
- "com.motovaultpro.stack=blue"
|
||||||
|
- "com.motovaultpro.service=frontend"
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# BLUE Stack - Backend
|
||||||
|
# ========================================
|
||||||
|
mvp-backend-blue:
|
||||||
|
image: ${BACKEND_IMAGE:-registry.motovaultpro.com/motovaultpro/backend:latest}
|
||||||
|
container_name: mvp-backend-blue
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
CONFIG_PATH: /app/config/production.yml
|
||||||
|
SECRETS_DIR: /run/secrets
|
||||||
|
DATABASE_HOST: mvp-postgres
|
||||||
|
REDIS_HOST: mvp-redis
|
||||||
|
volumes:
|
||||||
|
- ./config/app/production.yml:/app/config/production.yml:ro
|
||||||
|
- ./config/shared/production.yml:/app/config/shared.yml:ro
|
||||||
|
- ./secrets/app/postgres-password.txt:/run/secrets/postgres-password:ro
|
||||||
|
- ./secrets/app/auth0-client-secret.txt:/run/secrets/auth0-client-secret:ro
|
||||||
|
- ./secrets/app/google-maps-api-key.txt:/run/secrets/google-maps-api-key:ro
|
||||||
|
- ./secrets/app/google-maps-map-id.txt:/run/secrets/google-maps-map-id:ro
|
||||||
|
- ./secrets/app/resend-api-key.txt:/run/secrets/resend-api-key:ro
|
||||||
|
- ./secrets/app/auth0-management-client-id.txt:/run/secrets/auth0-management-client-id:ro
|
||||||
|
- ./secrets/app/auth0-management-client-secret.txt:/run/secrets/auth0-management-client-secret:ro
|
||||||
|
- ./data/documents:/app/data/documents
|
||||||
|
- ./data/backups:/app/data/backups
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
- database
|
||||||
|
depends_on:
|
||||||
|
- mvp-postgres
|
||||||
|
- mvp-redis
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD-SHELL
|
||||||
|
- node -e "require('http').get('http://localhost:3001/health', r => process.exit(r.statusCode===200?0:1)).on('error', () => process.exit(1))"
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 1G
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=motovaultpro_backend"
|
||||||
|
- "com.motovaultpro.stack=blue"
|
||||||
|
- "com.motovaultpro.service=backend"
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# GREEN Stack - Frontend
|
||||||
|
# ========================================
|
||||||
|
mvp-frontend-green:
|
||||||
|
image: ${FRONTEND_IMAGE:-registry.motovaultpro.com/motovaultpro/frontend:latest}
|
||||||
|
container_name: mvp-frontend-green
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
VITE_API_BASE_URL: /api
|
||||||
|
VITE_AUTH0_DOMAIN: ${VITE_AUTH0_DOMAIN:-motovaultpro.us.auth0.com}
|
||||||
|
VITE_AUTH0_CLIENT_ID: ${VITE_AUTH0_CLIENT_ID:-yspR8zdnSxmV8wFIghHynQ08iXAPoQJ3}
|
||||||
|
VITE_AUTH0_AUDIENCE: ${VITE_AUTH0_AUDIENCE:-https://api.motovaultpro.com}
|
||||||
|
SECRETS_DIR: /run/secrets
|
||||||
|
volumes:
|
||||||
|
- ./secrets/app/google-maps-api-key.txt:/run/secrets/google-maps-api-key:ro
|
||||||
|
- ./secrets/app/google-maps-map-id.txt:/run/secrets/google-maps-map-id:ro
|
||||||
|
networks:
|
||||||
|
- frontend
|
||||||
|
depends_on:
|
||||||
|
- mvp-backend-green
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -sf http://localhost:3000 || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=motovaultpro_frontend"
|
||||||
|
- "com.motovaultpro.stack=green"
|
||||||
|
- "com.motovaultpro.service=frontend"
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# GREEN Stack - Backend
|
||||||
|
# ========================================
|
||||||
|
mvp-backend-green:
|
||||||
|
image: ${BACKEND_IMAGE:-registry.motovaultpro.com/motovaultpro/backend:latest}
|
||||||
|
container_name: mvp-backend-green
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
CONFIG_PATH: /app/config/production.yml
|
||||||
|
SECRETS_DIR: /run/secrets
|
||||||
|
DATABASE_HOST: mvp-postgres
|
||||||
|
REDIS_HOST: mvp-redis
|
||||||
|
volumes:
|
||||||
|
- ./config/app/production.yml:/app/config/production.yml:ro
|
||||||
|
- ./config/shared/production.yml:/app/config/shared.yml:ro
|
||||||
|
- ./secrets/app/postgres-password.txt:/run/secrets/postgres-password:ro
|
||||||
|
- ./secrets/app/auth0-client-secret.txt:/run/secrets/auth0-client-secret:ro
|
||||||
|
- ./secrets/app/google-maps-api-key.txt:/run/secrets/google-maps-api-key:ro
|
||||||
|
- ./secrets/app/google-maps-map-id.txt:/run/secrets/google-maps-map-id:ro
|
||||||
|
- ./secrets/app/resend-api-key.txt:/run/secrets/resend-api-key:ro
|
||||||
|
- ./secrets/app/auth0-management-client-id.txt:/run/secrets/auth0-management-client-id:ro
|
||||||
|
- ./secrets/app/auth0-management-client-secret.txt:/run/secrets/auth0-management-client-secret:ro
|
||||||
|
- ./data/documents:/app/data/documents
|
||||||
|
- ./data/backups:/app/data/backups
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
- database
|
||||||
|
depends_on:
|
||||||
|
- mvp-postgres
|
||||||
|
- mvp-redis
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD-SHELL
|
||||||
|
- node -e "require('http').get('http://localhost:3001/health', r => process.exit(r.statusCode===200?0:1)).on('error', () => process.exit(1))"
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 1G
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=motovaultpro_backend"
|
||||||
|
- "com.motovaultpro.stack=green"
|
||||||
|
- "com.motovaultpro.service=backend"
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# Override Traefik to add dynamic config
|
||||||
|
# ========================================
|
||||||
|
mvp-traefik:
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- ./config/traefik/traefik.yml:/etc/traefik/traefik.yml:ro
|
||||||
|
- ./config/traefik/middleware.yml:/etc/traefik/middleware.yml:ro
|
||||||
|
- ./config/traefik/dynamic:/etc/traefik/dynamic:ro
|
||||||
|
- ./certs:/certs:ro
|
||||||
|
- traefik_data:/data
|
||||||
|
- ./secrets/app/cloudflare-dns-token.txt:/run/secrets/cloudflare-dns-token:ro
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
|
# Base registry for mirrored images (override with environment variable)
|
||||||
|
x-registry: ®istry
|
||||||
|
REGISTRY_MIRRORS: ${REGISTRY_MIRRORS:-registry.motovaultpro.com/mirrors}
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# Traefik - Service Discovery and Load Balancing
|
# Traefik - Service Discovery and Load Balancing
|
||||||
mvp-traefik:
|
mvp-traefik:
|
||||||
image: traefik:v3.6
|
image: ${REGISTRY_MIRRORS:-registry.motovaultpro.com/mirrors}/traefik:v3.6
|
||||||
container_name: mvp-traefik
|
container_name: mvp-traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
@@ -15,7 +19,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
- ./config/traefik/traefik.yml:/etc/traefik/traefik.yml:ro
|
- ./config/traefik/traefik.yml:/etc/traefik/traefik.yml:ro
|
||||||
- ./config/traefik/middleware.yml:/etc/traefik/middleware.yml:ro
|
- ./config/traefik/dynamic:/etc/traefik/dynamic:ro
|
||||||
- ./certs:/certs:ro
|
- ./certs:/certs:ro
|
||||||
- traefik_data:/data
|
- traefik_data:/data
|
||||||
- ./secrets/app/cloudflare-dns-token.txt:/run/secrets/cloudflare-dns-token:ro
|
- ./secrets/app/cloudflare-dns-token.txt:/run/secrets/cloudflare-dns-token:ro
|
||||||
@@ -154,7 +158,7 @@ services:
|
|||||||
|
|
||||||
# Database Services - Application PostgreSQL
|
# Database Services - Application PostgreSQL
|
||||||
mvp-postgres:
|
mvp-postgres:
|
||||||
image: postgres:18-alpine
|
image: ${REGISTRY_MIRRORS:-registry.motovaultpro.com/mirrors}/postgres:18-alpine
|
||||||
container_name: mvp-postgres
|
container_name: mvp-postgres
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@@ -179,7 +183,7 @@ services:
|
|||||||
|
|
||||||
# Database Services - Application Redis
|
# Database Services - Application Redis
|
||||||
mvp-redis:
|
mvp-redis:
|
||||||
image: redis:8.4-alpine
|
image: ${REGISTRY_MIRRORS:-registry.motovaultpro.com/mirrors}/redis:8.4-alpine
|
||||||
container_name: mvp-redis
|
container_name: mvp-redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: redis-server --appendonly yes
|
command: redis-server --appendonly yes
|
||||||
|
|||||||
315
docs/BUILD-SERVER-SETUP.md
Normal file
315
docs/BUILD-SERVER-SETUP.md
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
# Build Server Setup Guide
|
||||||
|
|
||||||
|
Complete guide for setting up a dedicated build VPS for MotoVaultPro CI/CD pipeline.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The build server isolates resource-intensive Docker builds from the production server, ensuring deployments don't impact application performance.
|
||||||
|
|
||||||
|
```
|
||||||
|
+-------------------+ +--------------------+
|
||||||
|
| GitLab Server | | Production Server |
|
||||||
|
| (CI/CD + Registry)| | (Shell Runner) |
|
||||||
|
+--------+----------+ +----------+---------+
|
||||||
|
| |
|
||||||
|
v v
|
||||||
|
+--------+----------+ +----------+---------+
|
||||||
|
| Build VPS | | Blue-Green Stacks |
|
||||||
|
| (Docker Runner) |---->| + Shared Data |
|
||||||
|
+-------------------+ +--------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server Requirements
|
||||||
|
|
||||||
|
### Minimum Specifications
|
||||||
|
|
||||||
|
| Resource | Requirement |
|
||||||
|
|----------|-------------|
|
||||||
|
| CPU | 2 cores |
|
||||||
|
| RAM | 4GB |
|
||||||
|
| Storage | 50GB SSD |
|
||||||
|
| Network | 100Mbps+ |
|
||||||
|
| OS | Ubuntu 22.04 LTS / Debian 12 |
|
||||||
|
|
||||||
|
### Network Requirements
|
||||||
|
|
||||||
|
- Outbound HTTPS to GitLab instance
|
||||||
|
- Outbound HTTPS to Docker registries (for fallback)
|
||||||
|
- SSH access for administration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation Steps
|
||||||
|
|
||||||
|
### 1. Update System
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update && sudo apt upgrade -y
|
||||||
|
sudo apt install -y curl git ca-certificates gnupg
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install Docker Engine
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add Docker's official GPG key
|
||||||
|
sudo install -m 0755 -d /etc/apt/keyrings
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||||
|
sudo chmod a+r /etc/apt/keyrings/docker.gpg
|
||||||
|
|
||||||
|
# Add the repository to Apt sources
|
||||||
|
echo \
|
||||||
|
"deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
|
||||||
|
"$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
|
||||||
|
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
|
||||||
|
# Install Docker
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
docker --version
|
||||||
|
docker compose version
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Install GitLab Runner
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add GitLab Runner repository
|
||||||
|
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
|
||||||
|
|
||||||
|
# Install GitLab Runner
|
||||||
|
sudo apt install gitlab-runner
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
gitlab-runner --version
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Register Runner with Shell Executor
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo gitlab-runner register \
|
||||||
|
--non-interactive \
|
||||||
|
--url "https://git.motovaultpro.com" \
|
||||||
|
--registration-token "YOUR_REGISTRATION_TOKEN" \
|
||||||
|
--executor "shell" \
|
||||||
|
--description "Build Server - Shell Executor" \
|
||||||
|
--tag-list "build" \
|
||||||
|
--run-untagged="false" \
|
||||||
|
--locked="true"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- Replace `YOUR_REGISTRATION_TOKEN` with the token from GitLab Admin > CI/CD > Runners
|
||||||
|
- Shell executor runs jobs directly on the host with access to Docker
|
||||||
|
- Tag `build` is used in `.gitlab-ci.yml` to route build jobs to this server
|
||||||
|
|
||||||
|
### 5. Add gitlab-runner to Docker Group
|
||||||
|
|
||||||
|
The gitlab-runner user needs access to Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo usermod -aG docker gitlab-runner
|
||||||
|
|
||||||
|
# Verify access
|
||||||
|
sudo -u gitlab-runner docker info
|
||||||
|
sudo -u gitlab-runner docker compose version
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Configure Docker Registry Authentication
|
||||||
|
|
||||||
|
Create credentials file for GitLab Container Registry:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Login to GitLab Container Registry (creates ~/.docker/config.json)
|
||||||
|
docker login registry.motovaultpro.com -u <deploy-token-username> -p <deploy-token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Creating Deploy Token:**
|
||||||
|
1. Go to GitLab Project > Settings > Repository > Deploy Tokens
|
||||||
|
2. Create token with `read_registry` and `write_registry` scopes
|
||||||
|
3. Use the token username/password for Docker login
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### Test Runner Registration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo gitlab-runner verify
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
```
|
||||||
|
Verifying runner... is alive runner=XXXXXX
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Docker Access
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo gitlab-runner exec docker --docker-privileged test-job
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Registry Push
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and push a test image
|
||||||
|
docker build -t registry.motovaultpro.com/motovaultpro/test:latest -f- . <<EOF
|
||||||
|
FROM alpine:latest
|
||||||
|
RUN echo "test"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
docker push registry.motovaultpro.com/motovaultpro/test:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### Disk Cleanup
|
||||||
|
|
||||||
|
Docker builds accumulate disk space. Set up automated cleanup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create cleanup script
|
||||||
|
sudo tee /usr/local/bin/docker-cleanup.sh > /dev/null <<'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
# Remove unused Docker resources older than 7 days
|
||||||
|
docker system prune -af --filter "until=168h"
|
||||||
|
docker volume prune -f
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sudo chmod +x /usr/local/bin/docker-cleanup.sh
|
||||||
|
|
||||||
|
# Add to crontab (run daily at 3 AM)
|
||||||
|
echo "0 3 * * * /usr/local/bin/docker-cleanup.sh >> /var/log/docker-cleanup.log 2>&1" | sudo crontab -
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Rotation
|
||||||
|
|
||||||
|
Configure log rotation for GitLab Runner:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo tee /etc/logrotate.d/gitlab-runner > /dev/null <<EOF
|
||||||
|
/var/log/gitlab-runner/*.log {
|
||||||
|
daily
|
||||||
|
rotate 7
|
||||||
|
compress
|
||||||
|
missingok
|
||||||
|
notifempty
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Runner
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update GitLab Runner
|
||||||
|
sudo apt update
|
||||||
|
sudo apt upgrade gitlab-runner
|
||||||
|
|
||||||
|
# Restart runner
|
||||||
|
sudo gitlab-runner restart
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Firewall Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Allow only necessary outbound traffic
|
||||||
|
sudo ufw default deny incoming
|
||||||
|
sudo ufw default allow outgoing
|
||||||
|
sudo ufw allow ssh
|
||||||
|
sudo ufw enable
|
||||||
|
```
|
||||||
|
|
||||||
|
### Runner Security
|
||||||
|
|
||||||
|
- **Locked runner**: Only accepts jobs from the specific project
|
||||||
|
- **Protected tags**: Only runs on protected branches (main)
|
||||||
|
- **Docker socket**: Mounted read-only where possible
|
||||||
|
|
||||||
|
### Secrets Management
|
||||||
|
|
||||||
|
The build server does NOT store application secrets. All secrets are:
|
||||||
|
- Stored in GitLab CI/CD Variables
|
||||||
|
- Injected at runtime on the production server
|
||||||
|
- Never cached in Docker layers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Runner Not Picking Up Jobs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check runner status
|
||||||
|
sudo gitlab-runner status
|
||||||
|
|
||||||
|
# View runner logs
|
||||||
|
sudo journalctl -u gitlab-runner -f
|
||||||
|
|
||||||
|
# Re-register runner if needed
|
||||||
|
sudo gitlab-runner unregister --all-runners
|
||||||
|
sudo gitlab-runner register
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Build Failures
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Docker daemon
|
||||||
|
sudo systemctl status docker
|
||||||
|
|
||||||
|
# Check available disk space
|
||||||
|
df -h
|
||||||
|
|
||||||
|
# Clear Docker cache
|
||||||
|
docker system prune -af
|
||||||
|
```
|
||||||
|
|
||||||
|
### Registry Push Failures
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify registry login
|
||||||
|
docker login registry.motovaultpro.com
|
||||||
|
|
||||||
|
# Check network connectivity
|
||||||
|
curl -v https://registry.motovaultpro.com/v2/
|
||||||
|
|
||||||
|
# Verify image exists
|
||||||
|
docker images | grep motovaultpro
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Important Paths
|
||||||
|
|
||||||
|
| Path | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `/etc/gitlab-runner/config.toml` | Runner configuration |
|
||||||
|
| `/var/log/gitlab-runner/` | Runner logs |
|
||||||
|
| `~/.docker/config.json` | Docker registry credentials |
|
||||||
|
| `/var/lib/docker/` | Docker data |
|
||||||
|
|
||||||
|
### Common Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Runner management
|
||||||
|
sudo gitlab-runner status
|
||||||
|
sudo gitlab-runner restart
|
||||||
|
sudo gitlab-runner verify
|
||||||
|
|
||||||
|
# Docker management
|
||||||
|
docker system df # Check disk usage
|
||||||
|
docker system prune -af # Clean all unused resources
|
||||||
|
docker images # List images
|
||||||
|
docker ps -a # List containers
|
||||||
|
|
||||||
|
# View build logs
|
||||||
|
sudo journalctl -u gitlab-runner --since "1 hour ago"
|
||||||
|
```
|
||||||
@@ -1,17 +1,72 @@
|
|||||||
# MotoVaultPro GitLab CI/CD Deployment Guide
|
# MotoVaultPro GitLab CI/CD Deployment Guide
|
||||||
|
|
||||||
Complete guide for deploying MotoVaultPro using GitLab CI/CD with shell executor runners.
|
Complete guide for deploying MotoVaultPro using GitLab CI/CD with blue-green deployment and auto-rollback.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
1. [Prerequisites](#prerequisites)
|
1. [Architecture Overview](#architecture-overview)
|
||||||
2. [GitLab Runner Setup](#gitlab-runner-setup)
|
2. [Prerequisites](#prerequisites)
|
||||||
3. [CI/CD Variables Configuration](#cicd-variables-configuration)
|
3. [Pipeline Stages](#pipeline-stages)
|
||||||
4. [Secrets Architecture](#secrets-architecture)
|
4. [Blue-Green Deployment](#blue-green-deployment)
|
||||||
5. [Pipeline Overview](#pipeline-overview)
|
5. [CI/CD Variables Configuration](#cicd-variables-configuration)
|
||||||
6. [Deployment Process](#deployment-process)
|
6. [Container Registry](#container-registry)
|
||||||
7. [Rollback Procedure](#rollback-procedure)
|
7. [Deployment Process](#deployment-process)
|
||||||
8. [Troubleshooting](#troubleshooting)
|
8. [Rollback Procedures](#rollback-procedures)
|
||||||
|
9. [Maintenance Migrations](#maintenance-migrations)
|
||||||
|
10. [Notifications](#notifications)
|
||||||
|
11. [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
MotoVaultPro uses a blue-green deployment strategy with automatic rollback:
|
||||||
|
|
||||||
|
```
|
||||||
|
+---------------------------------------------------+
|
||||||
|
| GitLab (CI/CD + Registry) |
|
||||||
|
+---------------------------------------------------+
|
||||||
|
| |
|
||||||
|
v v
|
||||||
|
+------------------+ +-----------------------+
|
||||||
|
| Build VPS | | Production Server |
|
||||||
|
| (Docker Runner) | | (Shell Runner) |
|
||||||
|
| Tags: build | | Tags: production |
|
||||||
|
+------------------+ +-----------+-----------+
|
||||||
|
| |
|
||||||
|
| Push images | Pull + Deploy
|
||||||
|
v v
|
||||||
|
+---------------------------------------------------+
|
||||||
|
| GitLab Container Registry |
|
||||||
|
| registry.motovaultpro.com/motovaultpro/ |
|
||||||
|
+---------------------------------------------------+
|
||||||
|
|
|
||||||
|
+---------------+---------------+
|
||||||
|
| |
|
||||||
|
+--------v--------+ +--------v--------+
|
||||||
|
| BLUE Stack | | GREEN Stack |
|
||||||
|
| mvp-frontend | | mvp-frontend |
|
||||||
|
| mvp-backend | | mvp-backend |
|
||||||
|
+-----------------+ +-----------------+
|
||||||
|
| |
|
||||||
|
+----------- Traefik -----------+
|
||||||
|
(weighted LB)
|
||||||
|
|
|
||||||
|
+---------------+---------------+
|
||||||
|
| |
|
||||||
|
+--------v--------+ +--------v--------+
|
||||||
|
| PostgreSQL | | Redis |
|
||||||
|
| (shared) | | (shared) |
|
||||||
|
+-----------------+ +-----------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
- **Zero-downtime deployments**: Traffic switches in under 5 seconds
|
||||||
|
- **Instant rollback**: Previous version remains running
|
||||||
|
- **Automatic rollback**: On health check failure
|
||||||
|
- **Email notifications**: Via Resend API
|
||||||
|
- **Container registry**: Self-hosted on GitLab (no Docker Hub)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -19,55 +74,115 @@ Complete guide for deploying MotoVaultPro using GitLab CI/CD with shell executor
|
|||||||
|
|
||||||
### Server Requirements
|
### Server Requirements
|
||||||
|
|
||||||
- Linux server with Docker Engine installed
|
| Server | Purpose | Specs | Runner Tags |
|
||||||
- Docker Compose v2 (plugin version)
|
|--------|---------|-------|-------------|
|
||||||
- GitLab Runner installed and registered
|
| Build VPS | Docker image builds | 2 CPU, 4GB RAM | `build` |
|
||||||
- Git installed
|
| Prod Server | Application hosting | 8GB+ RAM | `production` |
|
||||||
- curl installed (for health checks)
|
|
||||||
|
|
||||||
### GitLab Requirements
|
See [BUILD-SERVER-SETUP.md](BUILD-SERVER-SETUP.md) for build server setup.
|
||||||
|
|
||||||
- GitLab 18.6+ (tested with 18.6.2)
|
### Software Requirements
|
||||||
- Project with CI/CD enabled
|
|
||||||
- Protected `main` branch
|
- GitLab 18.6+
|
||||||
- Maintainer access for CI/CD variable configuration
|
- Docker Engine 24.0+
|
||||||
|
- Docker Compose v2
|
||||||
|
- GitLab Runner (shell executor on both servers)
|
||||||
|
- `jq` for JSON processing
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## GitLab Runner Setup
|
## Pipeline Stages
|
||||||
|
|
||||||
### 1. Verify Runner Registration
|
The CI/CD pipeline consists of 7 stages:
|
||||||
|
|
||||||
```bash
|
```
|
||||||
sudo gitlab-runner verify
|
validate -> build -> deploy-prepare -> deploy-switch -> verify -> [rollback] -> notify
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected output should show your runner as active with shell executor.
|
| Stage | Runner | Purpose |
|
||||||
|
|-------|--------|---------|
|
||||||
|
| `validate` | prod | Check prerequisites, determine target stack |
|
||||||
|
| `build` | build | Build and push images to GitLab registry |
|
||||||
|
| `deploy-prepare` | prod | Pull images, start inactive stack, health check |
|
||||||
|
| `deploy-switch` | prod | Switch Traefik traffic weights |
|
||||||
|
| `verify` | prod | Production health verification |
|
||||||
|
| `rollback` | prod | Auto-triggered on verify failure |
|
||||||
|
| `notify` | prod | Email success/failure notifications |
|
||||||
|
|
||||||
### 2. Verify Docker Permissions
|
### Pipeline Flow
|
||||||
|
|
||||||
The `gitlab-runner` user must have Docker access:
|
```
|
||||||
|
[Push to main]
|
||||||
```bash
|
|
|
||||||
# Add gitlab-runner to docker group (if not already done)
|
v
|
||||||
sudo usermod -aG docker gitlab-runner
|
[validate] - Checks Docker, paths, registry
|
||||||
|
|
|
||||||
# Verify access
|
v
|
||||||
sudo -u gitlab-runner docker info
|
[build] - Builds backend + frontend images
|
||||||
sudo -u gitlab-runner docker compose version
|
| Pushes to registry.motovaultpro.com
|
||||||
|
v
|
||||||
|
[deploy-prepare] - Pulls new images
|
||||||
|
| Starts inactive stack (blue or green)
|
||||||
|
| Runs health checks
|
||||||
|
v
|
||||||
|
[deploy-switch] - Updates Traefik weights
|
||||||
|
| Switches traffic instantly
|
||||||
|
v
|
||||||
|
[verify] - External health check
|
||||||
|
| Container status verification
|
||||||
|
|
|
||||||
|
+--[SUCCESS]--> [notify-success] - Sends success email
|
||||||
|
|
|
||||||
|
+--[FAILURE]--> [rollback] - Switches back to previous stack
|
||||||
|
|
|
||||||
|
v
|
||||||
|
[notify-failure] - Sends failure email
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Verify Deployment Directory
|
---
|
||||||
|
|
||||||
Ensure the deployment directory exists and is accessible:
|
## Blue-Green Deployment
|
||||||
|
|
||||||
```bash
|
### Stack Configuration
|
||||||
# Create deployment directory
|
|
||||||
sudo mkdir -p /opt/motovaultpro
|
|
||||||
sudo chown gitlab-runner:gitlab-runner /opt/motovaultpro
|
|
||||||
|
|
||||||
# Clone repository (first time only)
|
Both stacks share the same database layer:
|
||||||
sudo -u gitlab-runner git clone <repository-url> /opt/motovaultpro
|
|
||||||
|
| Component | Blue Stack | Green Stack | Shared |
|
||||||
|
|-----------|------------|-------------|--------|
|
||||||
|
| Frontend | `mvp-frontend-blue` | `mvp-frontend-green` | - |
|
||||||
|
| Backend | `mvp-backend-blue` | `mvp-backend-green` | - |
|
||||||
|
| PostgreSQL | - | - | `mvp-postgres` |
|
||||||
|
| Redis | - | - | `mvp-redis` |
|
||||||
|
| Traefik | - | - | `mvp-traefik` |
|
||||||
|
|
||||||
|
### Traffic Routing
|
||||||
|
|
||||||
|
Traefik uses weighted services for traffic distribution:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# config/traefik/dynamic/blue-green.yml
|
||||||
|
services:
|
||||||
|
mvp-frontend-weighted:
|
||||||
|
weighted:
|
||||||
|
services:
|
||||||
|
- name: mvp-frontend-blue-svc
|
||||||
|
weight: 100 # Active
|
||||||
|
- name: mvp-frontend-green-svc
|
||||||
|
weight: 0 # Standby
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment State
|
||||||
|
|
||||||
|
State is tracked in `config/deployment/state.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"active_stack": "blue",
|
||||||
|
"inactive_stack": "green",
|
||||||
|
"last_deployment": "2024-01-15T10:30:00Z",
|
||||||
|
"last_deployment_commit": "abc123",
|
||||||
|
"rollback_available": true
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -76,410 +191,316 @@ sudo -u gitlab-runner git clone <repository-url> /opt/motovaultpro
|
|||||||
|
|
||||||
Navigate to **Settings > CI/CD > Variables** in your GitLab project.
|
Navigate to **Settings > CI/CD > Variables** in your GitLab project.
|
||||||
|
|
||||||
### Secrets (File Type Variables)
|
### Required Variables
|
||||||
|
|
||||||
These variables use GitLab's **File** type, which writes the value to a temporary file and provides the path as the environment variable. This replicates the Kubernetes secrets pattern used by the application.
|
| Variable | Type | Protected | Purpose |
|
||||||
|
|----------|------|-----------|---------|
|
||||||
| Variable Name | Type | Protected | Masked | Description |
|
| `DEPLOY_NOTIFY_EMAIL` | Variable | Yes | Notification recipient |
|
||||||
|--------------|------|-----------|--------|-------------|
|
| `VITE_AUTH0_DOMAIN` | Variable | No | Auth0 domain |
|
||||||
| `POSTGRES_PASSWORD` | File | Yes | Yes | PostgreSQL database password |
|
| `VITE_AUTH0_CLIENT_ID` | Variable | No | Auth0 client ID |
|
||||||
| `AUTH0_CLIENT_SECRET` | File | Yes | Yes | Auth0 client secret for backend |
|
| `VITE_AUTH0_AUDIENCE` | Variable | No | Auth0 audience |
|
||||||
| `GOOGLE_MAPS_API_KEY` | File | Yes | Yes | Google Maps API key |
|
|
||||||
| `GOOGLE_MAPS_MAP_ID` | File | Yes | No | Google Maps Map ID |
|
|
||||||
| `CF_DNS_API_TOKEN` | File | Yes | Yes | Cloudflare API token for Let's Encrypt DNS challenge |
|
|
||||||
| `RESEND_API_KEY` | File | Yes | Yes | Resend API key for email notifications |
|
|
||||||
|
|
||||||
### Configuration Variables
|
|
||||||
|
|
||||||
| Variable Name | Type | Protected | Masked | Value |
|
|
||||||
|--------------|------|-----------|--------|-------|
|
|
||||||
| `VITE_AUTH0_DOMAIN` | Variable | No | No | `motovaultpro.us.auth0.com` |
|
|
||||||
| `VITE_AUTH0_CLIENT_ID` | Variable | No | No | Your Auth0 client ID |
|
|
||||||
| `VITE_AUTH0_AUDIENCE` | Variable | No | No | `https://api.motovaultpro.com` |
|
|
||||||
|
|
||||||
Note: `DEPLOY_PATH` is automatically set in `.gitlab-ci.yml` using `GIT_CLONE_PATH` for a stable path.
|
|
||||||
|
|
||||||
### Creating Cloudflare API Token
|
|
||||||
|
|
||||||
The `CF_DNS_API_TOKEN` is required for automatic SSL certificate generation via Let's Encrypt DNS-01 challenge.
|
|
||||||
|
|
||||||
1. Go to [Cloudflare Dashboard](https://dash.cloudflare.com/profile/api-tokens)
|
|
||||||
2. Click **Create Token**
|
|
||||||
3. Use template: **Edit zone DNS**
|
|
||||||
4. Configure permissions:
|
|
||||||
- **Permissions**: Zone > DNS > Edit
|
|
||||||
- **Zone Resources**: Include > Specific zone > `motovaultpro.com`
|
|
||||||
5. Click **Continue to summary** then **Create Token**
|
|
||||||
6. Copy the token value immediately (it won't be shown again)
|
|
||||||
7. Add as `CF_DNS_API_TOKEN` File variable in GitLab
|
|
||||||
|
|
||||||
### Setting Up a File Type Variable
|
|
||||||
|
|
||||||
1. Go to **Settings > CI/CD > Variables**
|
|
||||||
2. Click **Add variable**
|
|
||||||
3. Enter the variable key (e.g., `POSTGRES_PASSWORD`)
|
|
||||||
4. Enter the secret value in the **Value** field
|
|
||||||
5. Set **Type** to **File**
|
|
||||||
6. Enable **Protect variable** (recommended)
|
|
||||||
7. Enable **Mask variable** (for sensitive data)
|
|
||||||
8. Click **Add variable**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Secrets Architecture
|
|
||||||
|
|
||||||
MotoVaultPro uses a Kubernetes-style secrets pattern where secrets are mounted as files at `/run/secrets/` inside containers.
|
|
||||||
|
|
||||||
### How It Works
|
|
||||||
|
|
||||||
1. **GitLab stores secrets** as File type CI/CD variables
|
|
||||||
2. **During pipeline execution**, GitLab writes each secret to a temporary file
|
|
||||||
3. **The `inject-secrets.sh` script** copies these files to `secrets/app/` directory
|
|
||||||
4. **Docker Compose** mounts these files to `/run/secrets/` in containers
|
|
||||||
5. **Application code** reads secrets from the filesystem (not environment variables)
|
|
||||||
|
|
||||||
### Secret Files
|
### Secret Files
|
||||||
|
|
||||||
```
|
These use GitLab's **File** type and are injected via `scripts/inject-secrets.sh`:
|
||||||
secrets/app/
|
|
||||||
postgres-password.txt -> /run/secrets/postgres-password
|
|
||||||
auth0-client-secret.txt -> /run/secrets/auth0-client-secret
|
|
||||||
google-maps-api-key.txt -> /run/secrets/google-maps-api-key
|
|
||||||
google-maps-map-id.txt -> /run/secrets/google-maps-map-id
|
|
||||||
cloudflare-dns-token.txt -> /run/secrets/cloudflare-dns-token
|
|
||||||
resend-api-key.txt -> /run/secrets/resend-api-key
|
|
||||||
```
|
|
||||||
|
|
||||||
### Security Benefits
|
| Variable | Type | Protected | Masked |
|
||||||
|
|----------|------|-----------|--------|
|
||||||
|
| `POSTGRES_PASSWORD` | File | Yes | Yes |
|
||||||
|
| `AUTH0_CLIENT_SECRET` | File | Yes | Yes |
|
||||||
|
| `GOOGLE_MAPS_API_KEY` | File | Yes | Yes |
|
||||||
|
| `GOOGLE_MAPS_MAP_ID` | File | Yes | No |
|
||||||
|
| `CF_DNS_API_TOKEN` | File | Yes | Yes |
|
||||||
|
| `RESEND_API_KEY` | File | Yes | Yes |
|
||||||
|
|
||||||
- Secrets never appear as environment variables (not visible in `env` or `printenv`)
|
### Registry Authentication
|
||||||
- File permissions (600) restrict access
|
|
||||||
- Masked variables prevent accidental log exposure
|
GitLab provides these automatically:
|
||||||
- Protected variables only available on protected branches
|
- `CI_REGISTRY_USER` - Registry username
|
||||||
|
- `CI_REGISTRY_PASSWORD` - Registry token
|
||||||
|
- `CI_REGISTRY` - Registry URL
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Pipeline Overview
|
## Container Registry
|
||||||
|
|
||||||
The CI/CD pipeline consists of four stages:
|
All images are hosted on the GitLab Container Registry to avoid Docker Hub rate limits.
|
||||||
|
|
||||||
### Stage 1: Validate
|
### Registry URL
|
||||||
|
|
||||||
Verifies deployment prerequisites:
|
|
||||||
- Docker is accessible
|
|
||||||
- Docker Compose is available
|
|
||||||
- Deployment directory exists
|
|
||||||
|
|
||||||
### Stage 2: Build
|
|
||||||
|
|
||||||
Builds Docker images:
|
|
||||||
- Pulls latest code from repository
|
|
||||||
- Builds all service images with `--no-cache`
|
|
||||||
|
|
||||||
### Stage 3: Deploy
|
|
||||||
|
|
||||||
Deploys the application:
|
|
||||||
1. Injects secrets from GitLab variables
|
|
||||||
2. Stops existing services gracefully
|
|
||||||
3. Pulls base images
|
|
||||||
4. Starts database services (PostgreSQL, Redis)
|
|
||||||
5. Runs database migrations
|
|
||||||
6. Starts all services
|
|
||||||
|
|
||||||
### Stage 4: Verify
|
|
||||||
|
|
||||||
Validates deployment health:
|
|
||||||
- Checks all containers are running
|
|
||||||
- Tests backend health endpoint
|
|
||||||
- Reports deployment status
|
|
||||||
|
|
||||||
### Pipeline Diagram
|
|
||||||
|
|
||||||
```
|
```
|
||||||
[Validate] -> [Build] -> [Deploy] -> [Verify]
|
registry.motovaultpro.com
|
||||||
| | | |
|
|
||||||
Check Build Inject Health
|
|
||||||
prereqs images secrets checks
|
|
||||||
|
|
|
||||||
Migrate
|
|
||||||
|
|
|
||||||
Start
|
|
||||||
services
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Image Paths
|
||||||
|
|
||||||
|
| Image | Path |
|
||||||
|
|-------|------|
|
||||||
|
| Backend | `registry.motovaultpro.com/motovaultpro/backend:$TAG` |
|
||||||
|
| Frontend | `registry.motovaultpro.com/motovaultpro/frontend:$TAG` |
|
||||||
|
| Mirrors | `registry.motovaultpro.com/mirrors/` |
|
||||||
|
|
||||||
|
### Base Image Mirrors
|
||||||
|
|
||||||
|
Mirror upstream images to avoid rate limits:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run manually or via scheduled pipeline
|
||||||
|
./scripts/ci/mirror-base-images.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Mirrored images:
|
||||||
|
- `node:20-alpine`
|
||||||
|
- `nginx:alpine`
|
||||||
|
- `postgres:18-alpine`
|
||||||
|
- `redis:8.4-alpine`
|
||||||
|
- `traefik:v3.6`
|
||||||
|
- `docker:24.0`
|
||||||
|
- `docker:24.0-dind`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Deployment Process
|
## Deployment Process
|
||||||
|
|
||||||
### Automatic Deployment
|
### Automatic Deployment
|
||||||
|
|
||||||
Deployments are triggered automatically when:
|
Deployments trigger automatically on push to `main`:
|
||||||
- Code is pushed to the `main` branch
|
|
||||||
- A merge request is merged into `main`
|
1. **Validate**: Check prerequisites, determine target stack
|
||||||
|
2. **Build**: Build images on dedicated build server
|
||||||
|
3. **Prepare**: Start inactive stack, run health checks
|
||||||
|
4. **Switch**: Update Traefik weights (instant)
|
||||||
|
5. **Verify**: External health check
|
||||||
|
6. **Notify**: Send email notification
|
||||||
|
|
||||||
### Manual Deployment
|
### Manual Deployment
|
||||||
|
|
||||||
To trigger a manual deployment:
|
|
||||||
|
|
||||||
1. Go to **CI/CD > Pipelines**
|
1. Go to **CI/CD > Pipelines**
|
||||||
2. Click **Run pipeline**
|
2. Click **Run pipeline**
|
||||||
3. Select the `main` branch
|
3. Select `main` branch
|
||||||
4. Click **Run pipeline**
|
4. Click **Run pipeline**
|
||||||
|
|
||||||
### Deployment Steps (What Happens)
|
### Deployment Timeline
|
||||||
|
|
||||||
1. **Secrets Injection**
|
| Phase | Duration |
|
||||||
- `inject-secrets.sh` copies GitLab File variables to `secrets/app/`
|
|-------|----------|
|
||||||
- Permissions are set to 600 for security
|
| Validate | ~5s |
|
||||||
|
| Build | ~2 min |
|
||||||
2. **Service Shutdown**
|
| Deploy-prepare | ~30s |
|
||||||
- Existing containers are stopped gracefully (30s timeout)
|
| Deploy-switch | ~3s |
|
||||||
- Volumes are preserved
|
| Verify | ~30s |
|
||||||
|
| **Total** | ~3 min |
|
||||||
3. **Database Startup**
|
|
||||||
- PostgreSQL and Redis start first
|
|
||||||
- 15-second wait for database readiness
|
|
||||||
|
|
||||||
4. **Migrations**
|
|
||||||
- Backend container runs database migrations
|
|
||||||
- Ensures schema is up-to-date
|
|
||||||
|
|
||||||
5. **Full Service Startup**
|
|
||||||
- All services start via `docker compose up -d`
|
|
||||||
- Traefik routes traffic automatically
|
|
||||||
|
|
||||||
6. **Health Verification**
|
|
||||||
- Container status checks
|
|
||||||
- Backend health endpoint validation
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Rollback Procedure
|
## Rollback Procedures
|
||||||
|
|
||||||
### Automatic Rollback
|
### Automatic Rollback
|
||||||
|
|
||||||
If the verify stage fails, the pipeline will report failure but services remain running. Manual intervention is required.
|
Triggers automatically when:
|
||||||
|
- Health check fails in `deploy-prepare`
|
||||||
|
- `verify` stage fails after switch
|
||||||
|
- Container becomes unhealthy within verification period
|
||||||
|
|
||||||
|
The pipeline runs `scripts/ci/auto-rollback.sh` which:
|
||||||
|
1. Verifies previous stack is healthy
|
||||||
|
2. Switches traffic back
|
||||||
|
3. Sends notification
|
||||||
|
|
||||||
### Manual Rollback
|
### Manual Rollback
|
||||||
|
|
||||||
Use the rollback script:
|
SSH to production server:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# SSH to server
|
|
||||||
ssh user@server
|
|
||||||
|
|
||||||
# Run rollback to previous commit
|
|
||||||
cd /opt/motovaultpro
|
cd /opt/motovaultpro
|
||||||
./scripts/rollback.sh HEAD~1
|
|
||||||
|
|
||||||
# Or rollback to specific tag/commit
|
# Check current state
|
||||||
./scripts/rollback.sh v1.0.0
|
cat config/deployment/state.json | jq .
|
||||||
|
|
||||||
|
# Switch to other stack
|
||||||
|
./scripts/ci/switch-traffic.sh blue # or green
|
||||||
```
|
```
|
||||||
|
|
||||||
### Rollback Script Details
|
|
||||||
|
|
||||||
The script performs:
|
|
||||||
1. Stops all current services
|
|
||||||
2. Checks out the specified version
|
|
||||||
3. Rebuilds images
|
|
||||||
4. Starts services
|
|
||||||
|
|
||||||
### Emergency Recovery
|
### Emergency Recovery
|
||||||
|
|
||||||
If rollback fails:
|
If both stacks are unhealthy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop everything
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.blue-green.yml down
|
||||||
|
|
||||||
|
# Restart shared services
|
||||||
|
docker compose up -d mvp-postgres mvp-redis mvp-traefik
|
||||||
|
|
||||||
|
# Wait for database
|
||||||
|
sleep 15
|
||||||
|
|
||||||
|
# Start one stack
|
||||||
|
export BACKEND_IMAGE=registry.motovaultpro.com/motovaultpro/backend:latest
|
||||||
|
export FRONTEND_IMAGE=registry.motovaultpro.com/motovaultpro/frontend:latest
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.blue-green.yml up -d \
|
||||||
|
mvp-frontend-blue mvp-backend-blue
|
||||||
|
|
||||||
|
# Switch traffic
|
||||||
|
./scripts/ci/switch-traffic.sh blue
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Maintenance Migrations
|
||||||
|
|
||||||
|
For breaking database changes requiring downtime:
|
||||||
|
|
||||||
|
### Via Pipeline (Recommended)
|
||||||
|
|
||||||
|
1. Go to **CI/CD > Pipelines**
|
||||||
|
2. Find the `maintenance-migration` job
|
||||||
|
3. Click **Play** to trigger manually
|
||||||
|
|
||||||
|
### Via Script
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/motovaultpro
|
cd /opt/motovaultpro
|
||||||
|
|
||||||
# Stop everything
|
# With backup
|
||||||
docker compose down
|
./scripts/ci/maintenance-migrate.sh backup
|
||||||
|
|
||||||
# Check git history
|
# Without backup
|
||||||
git log --oneline -10
|
./scripts/ci/maintenance-migrate.sh
|
||||||
|
```
|
||||||
|
|
||||||
# Checkout known working version
|
### What Happens
|
||||||
git checkout <commit-hash>
|
|
||||||
|
|
||||||
# Rebuild and start
|
1. Sends maintenance notification
|
||||||
docker compose build
|
2. Enables maintenance mode (stops traffic)
|
||||||
docker compose up -d
|
3. Creates database backup (if requested)
|
||||||
|
4. Runs migrations
|
||||||
|
5. Restarts backends
|
||||||
|
6. Restores traffic
|
||||||
|
7. Sends completion notification
|
||||||
|
|
||||||
# Verify
|
---
|
||||||
docker compose ps
|
|
||||||
|
## Notifications
|
||||||
|
|
||||||
|
Email notifications via Resend API for:
|
||||||
|
|
||||||
|
| Event | Subject |
|
||||||
|
|-------|---------|
|
||||||
|
| `success` | Deployment Successful |
|
||||||
|
| `failure` | Deployment Failed |
|
||||||
|
| `rollback` | Auto-Rollback Executed |
|
||||||
|
| `rollback_failed` | CRITICAL: Rollback Failed |
|
||||||
|
| `maintenance_start` | Maintenance Mode Started |
|
||||||
|
| `maintenance_end` | Maintenance Complete |
|
||||||
|
|
||||||
|
Configure recipient in GitLab CI/CD variables:
|
||||||
|
```
|
||||||
|
DEPLOY_NOTIFY_EMAIL = admin@example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Pipeline Fails at Validate Stage
|
|
||||||
|
|
||||||
**Symptom**: `DEPLOY_PATH not found`
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
```bash
|
|
||||||
# Create directory on runner server
|
|
||||||
sudo mkdir -p /opt/motovaultpro
|
|
||||||
sudo chown gitlab-runner:gitlab-runner /opt/motovaultpro
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pipeline Fails at Build Stage
|
### Pipeline Fails at Build Stage
|
||||||
|
|
||||||
**Symptom**: Docker build errors
|
**Check build server connectivity:**
|
||||||
|
|
||||||
**Solutions**:
|
|
||||||
1. Check Dockerfile syntax
|
|
||||||
2. Verify network connectivity for npm/package downloads
|
|
||||||
3. Check disk space: `df -h`
|
|
||||||
4. Clear Docker cache: `docker system prune -a`
|
|
||||||
|
|
||||||
### Pipeline Fails at Deploy Stage
|
|
||||||
|
|
||||||
**Symptom**: Secrets injection fails
|
|
||||||
|
|
||||||
**Solutions**:
|
|
||||||
1. Verify CI/CD variables are configured correctly
|
|
||||||
2. Check variable types are set to **File** for secrets
|
|
||||||
3. Ensure variables are not restricted to specific environments
|
|
||||||
|
|
||||||
**Symptom**: Migration fails
|
|
||||||
|
|
||||||
**Solutions**:
|
|
||||||
1. Check database connectivity
|
|
||||||
2. Verify PostgreSQL is healthy: `docker logs mvp-postgres`
|
|
||||||
3. Run migrations manually:
|
|
||||||
```bash
|
|
||||||
docker compose exec mvp-backend npm run migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pipeline Fails at Verify Stage
|
|
||||||
|
|
||||||
**Symptom**: Container not running
|
|
||||||
|
|
||||||
**Solutions**:
|
|
||||||
1. Check container logs: `docker logs <container-name>`
|
|
||||||
2. Verify secrets are correctly mounted
|
|
||||||
3. Check for port conflicts
|
|
||||||
|
|
||||||
**Symptom**: Health check fails
|
|
||||||
|
|
||||||
**Solutions**:
|
|
||||||
1. Wait longer (service might be starting)
|
|
||||||
2. Check backend logs: `docker logs mvp-backend`
|
|
||||||
3. Verify database connection
|
|
||||||
|
|
||||||
### Services Start But Application Doesn't Work
|
|
||||||
|
|
||||||
**Check secrets are mounted**:
|
|
||||||
```bash
|
```bash
|
||||||
docker compose exec mvp-backend ls -la /run/secrets/
|
# On build server
|
||||||
|
sudo gitlab-runner verify
|
||||||
|
docker login registry.motovaultpro.com
|
||||||
```
|
```
|
||||||
|
|
||||||
**Check configuration**:
|
**Check disk space:**
|
||||||
```bash
|
```bash
|
||||||
docker compose exec mvp-backend cat /app/config/production.yml
|
df -h
|
||||||
|
docker system prune -af
|
||||||
```
|
```
|
||||||
|
|
||||||
**Check network connectivity**:
|
### Pipeline Fails at Deploy-Prepare
|
||||||
|
|
||||||
|
**Container won't start:**
|
||||||
```bash
|
```bash
|
||||||
docker network ls
|
docker logs mvp-backend-blue --tail 100
|
||||||
docker network inspect motovaultpro_backend
|
docker logs mvp-frontend-blue --tail 100
|
||||||
```
|
```
|
||||||
|
|
||||||
### Viewing Logs
|
**Health check timeout:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# All services
|
# Increase timeout in .gitlab-ci.yml
|
||||||
docker compose logs -f
|
HEALTH_CHECK_TIMEOUT: "90"
|
||||||
|
|
||||||
# Specific service
|
|
||||||
docker compose logs -f mvp-backend
|
|
||||||
|
|
||||||
# Last 100 lines
|
|
||||||
docker compose logs --tail 100 mvp-backend
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### Traffic Not Switching
|
||||||
|
|
||||||
## Maintenance
|
|
||||||
|
|
||||||
### Updating Secrets
|
|
||||||
|
|
||||||
1. Update the CI/CD variable in GitLab
|
|
||||||
2. Trigger a new pipeline (push or manual)
|
|
||||||
3. The new secrets will be injected during deployment
|
|
||||||
|
|
||||||
### Database Backups
|
|
||||||
|
|
||||||
Backups should be configured separately. Recommended approach:
|
|
||||||
|
|
||||||
|
**Check Traefik config:**
|
||||||
```bash
|
```bash
|
||||||
# Manual backup
|
cat config/traefik/dynamic/blue-green.yml
|
||||||
docker compose exec mvp-postgres pg_dump -U postgres motovaultpro > backup.sql
|
docker exec mvp-traefik traefik healthcheck
|
||||||
|
|
||||||
# Automated backup (add to cron)
|
|
||||||
0 2 * * * cd /opt/motovaultpro && docker compose exec -T mvp-postgres pg_dump -U postgres motovaultpro > /backups/mvp-$(date +\%Y\%m\%d).sql
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Monitoring
|
**Check routing:**
|
||||||
|
```bash
|
||||||
|
curl -I https://motovaultpro.com/api/health
|
||||||
|
```
|
||||||
|
|
||||||
Consider adding:
|
### Verify Stage Fails
|
||||||
- Prometheus metrics (Traefik already configured)
|
|
||||||
- Health check alerts
|
**Check external connectivity:**
|
||||||
- Log aggregation
|
```bash
|
||||||
|
curl -sf https://motovaultpro.com/api/health
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check container health:**
|
||||||
|
```bash
|
||||||
|
docker inspect --format='{{.State.Health.Status}}' mvp-backend-blue
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick Reference
|
## Quick Reference
|
||||||
|
|
||||||
### Common Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# View pipeline status
|
|
||||||
# GitLab UI: CI/CD > Pipelines
|
|
||||||
|
|
||||||
# SSH to server
|
|
||||||
ssh user@your-server
|
|
||||||
|
|
||||||
# Navigate to project
|
|
||||||
cd /opt/motovaultpro
|
|
||||||
|
|
||||||
# View running containers
|
|
||||||
docker compose ps
|
|
||||||
|
|
||||||
# View logs
|
|
||||||
docker compose logs -f
|
|
||||||
|
|
||||||
# Restart a service
|
|
||||||
docker compose restart mvp-backend
|
|
||||||
|
|
||||||
# Run migrations manually
|
|
||||||
docker compose exec mvp-backend npm run migrate
|
|
||||||
|
|
||||||
# Access database
|
|
||||||
docker compose exec mvp-postgres psql -U postgres motovaultpro
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
curl http://localhost:3001/health
|
|
||||||
```
|
|
||||||
|
|
||||||
### Important Paths
|
### Important Paths
|
||||||
|
|
||||||
| Path | Description |
|
| Path | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `$CI_BUILDS_DIR/motovaultpro` | Application root (stable clone path) |
|
| `config/deployment/state.json` | Deployment state |
|
||||||
| `$CI_BUILDS_DIR/motovaultpro/secrets/app/` | Secrets directory |
|
| `config/traefik/dynamic/blue-green.yml` | Traffic routing |
|
||||||
| `$CI_BUILDS_DIR/motovaultpro/data/documents/` | Document storage |
|
| `scripts/ci/` | Deployment scripts |
|
||||||
| `$CI_BUILDS_DIR/motovaultpro/config/` | Configuration files |
|
|
||||||
|
|
||||||
Note: `CI_BUILDS_DIR` is typically `/opt/gitlab-runner/builds` for shell executors.
|
### Common Commands
|
||||||
|
|
||||||
### Container Names
|
```bash
|
||||||
|
# View current state
|
||||||
|
cat config/deployment/state.json | jq .
|
||||||
|
|
||||||
| Container | Purpose |
|
# Check container status
|
||||||
|-----------|---------|
|
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Health}}"
|
||||||
| `mvp-traefik` | Reverse proxy, TLS termination |
|
|
||||||
| `mvp-frontend` | React SPA |
|
# View logs
|
||||||
| `mvp-backend` | Node.js API |
|
docker logs mvp-backend-blue -f
|
||||||
| `mvp-postgres` | PostgreSQL database |
|
|
||||||
| `mvp-redis` | Redis cache |
|
# Manual traffic switch
|
||||||
|
./scripts/ci/switch-traffic.sh green
|
||||||
|
|
||||||
|
# Run health check
|
||||||
|
./scripts/ci/health-check.sh blue
|
||||||
|
|
||||||
|
# Send test notification
|
||||||
|
./scripts/ci/notify.sh success "Test message"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Memory Budget (8GB Server)
|
||||||
|
|
||||||
|
| Component | RAM |
|
||||||
|
|-----------|-----|
|
||||||
|
| Blue frontend | 512MB |
|
||||||
|
| Blue backend | 1GB |
|
||||||
|
| Green frontend | 512MB |
|
||||||
|
| Green backend | 1GB |
|
||||||
|
| PostgreSQL | 2GB |
|
||||||
|
| Redis | 512MB |
|
||||||
|
| Traefik | 128MB |
|
||||||
|
| System | 1.3GB |
|
||||||
|
| **Total** | ~7GB |
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
# Production Dockerfile for MotoVaultPro Frontend
|
# Production Dockerfile for MotoVaultPro Frontend
|
||||||
|
# Uses mirrored base images from GitLab Container Registry
|
||||||
|
|
||||||
|
# Build argument for registry (defaults to GitLab mirrors, falls back to Docker Hub)
|
||||||
|
ARG REGISTRY_MIRRORS=registry.motovaultpro.com/mirrors
|
||||||
|
|
||||||
# Stage 1: Base with dependencies
|
# Stage 1: Base with dependencies
|
||||||
FROM node:lts-alpine AS base
|
FROM ${REGISTRY_MIRRORS}/node:20-alpine AS base
|
||||||
RUN apk add --no-cache dumb-init curl
|
RUN apk add --no-cache dumb-init curl
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
@@ -29,7 +33,7 @@ COPY . .
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Stage 4: Production stage with nginx
|
# Stage 4: Production stage with nginx
|
||||||
FROM nginx:alpine AS production
|
FROM ${REGISTRY_MIRRORS}/nginx:alpine AS production
|
||||||
|
|
||||||
# Add curl for healthchecks
|
# Add curl for healthchecks
|
||||||
RUN apk add --no-cache curl
|
RUN apk add --no-cache curl
|
||||||
|
|||||||
152
scripts/ci/auto-rollback.sh
Executable file
152
scripts/ci/auto-rollback.sh
Executable file
@@ -0,0 +1,152 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Auto-rollback script for blue-green deployment
|
||||||
|
# Reverts traffic to the previous healthy stack
|
||||||
|
#
|
||||||
|
# Usage: ./auto-rollback.sh [reason]
|
||||||
|
# reason: Optional description of why rollback is happening
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 - Rollback successful
|
||||||
|
# 1 - Rollback failed
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
|
||||||
|
REASON="${1:-Automatic rollback triggered}"
|
||||||
|
|
||||||
|
STATE_FILE="$PROJECT_ROOT/config/deployment/state.json"
|
||||||
|
SWITCH_SCRIPT="$SCRIPT_DIR/switch-traffic.sh"
|
||||||
|
HEALTH_SCRIPT="$SCRIPT_DIR/health-check.sh"
|
||||||
|
NOTIFY_SCRIPT="$SCRIPT_DIR/notify.sh"
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo "AUTO-ROLLBACK INITIATED"
|
||||||
|
echo "Reason: $REASON"
|
||||||
|
echo "Time: $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Determine current and rollback stacks
|
||||||
|
if [[ -f "$STATE_FILE" ]] && command -v jq &> /dev/null; then
|
||||||
|
CURRENT_STACK=$(jq -r '.active_stack // "unknown"' "$STATE_FILE")
|
||||||
|
ROLLBACK_STACK=$(jq -r '.inactive_stack // "unknown"' "$STATE_FILE")
|
||||||
|
else
|
||||||
|
echo "ERROR: Cannot determine current stack state"
|
||||||
|
echo "State file: $STATE_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$CURRENT_STACK" == "unknown" ]] || [[ "$ROLLBACK_STACK" == "unknown" ]]; then
|
||||||
|
echo "ERROR: Invalid stack state"
|
||||||
|
echo " Current: $CURRENT_STACK"
|
||||||
|
echo " Rollback target: $ROLLBACK_STACK"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Stack Status:"
|
||||||
|
echo " Currently active: $CURRENT_STACK"
|
||||||
|
echo " Rollback target: $ROLLBACK_STACK"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Verify rollback stack is healthy before switching
|
||||||
|
echo "Step 1/3: Verifying rollback stack health..."
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
if [[ -x "$HEALTH_SCRIPT" ]]; then
|
||||||
|
if ! "$HEALTH_SCRIPT" "$ROLLBACK_STACK" 30; then
|
||||||
|
echo ""
|
||||||
|
echo "CRITICAL: Rollback stack ($ROLLBACK_STACK) is NOT healthy!"
|
||||||
|
echo "Manual intervention required."
|
||||||
|
echo ""
|
||||||
|
echo "Troubleshooting steps:"
|
||||||
|
echo " 1. Check container logs: docker logs mvp-backend-$ROLLBACK_STACK"
|
||||||
|
echo " 2. Check container status: docker ps -a"
|
||||||
|
echo " 3. Consider restarting rollback stack"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Send critical notification
|
||||||
|
if [[ -x "$NOTIFY_SCRIPT" ]]; then
|
||||||
|
"$NOTIFY_SCRIPT" "rollback_failed" \
|
||||||
|
"Rollback to $ROLLBACK_STACK failed - stack unhealthy. Manual intervention required. Reason: $REASON" \
|
||||||
|
|| true
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " OK: Rollback stack is healthy"
|
||||||
|
else
|
||||||
|
echo " WARNING: Health check script not found, proceeding anyway"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Switch traffic to rollback stack
|
||||||
|
echo ""
|
||||||
|
echo "Step 2/3: Switching traffic to $ROLLBACK_STACK..."
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
if [[ -x "$SWITCH_SCRIPT" ]]; then
|
||||||
|
if ! "$SWITCH_SCRIPT" "$ROLLBACK_STACK" instant; then
|
||||||
|
echo "ERROR: Traffic switch failed"
|
||||||
|
|
||||||
|
if [[ -x "$NOTIFY_SCRIPT" ]]; then
|
||||||
|
"$NOTIFY_SCRIPT" "rollback_failed" \
|
||||||
|
"Rollback traffic switch failed. Manual intervention required. Reason: $REASON" \
|
||||||
|
|| true
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "ERROR: Traffic switch script not found: $SWITCH_SCRIPT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update state file with rollback info
|
||||||
|
echo ""
|
||||||
|
echo "Step 3/3: Updating deployment state..."
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
if [[ -f "$STATE_FILE" ]] && command -v jq &> /dev/null; then
|
||||||
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
jq --arg stack "$ROLLBACK_STACK" \
|
||||||
|
--arg reason "$REASON" \
|
||||||
|
--arg ts "$TIMESTAMP" \
|
||||||
|
--arg failed "$CURRENT_STACK" \
|
||||||
|
'.active_stack = $stack |
|
||||||
|
.inactive_stack = $failed |
|
||||||
|
.last_rollback = $ts |
|
||||||
|
.last_rollback_reason = $reason |
|
||||||
|
.rollback_available = false' \
|
||||||
|
"$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
|
||||||
|
|
||||||
|
echo " State updated"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Send notification
|
||||||
|
if [[ -x "$NOTIFY_SCRIPT" ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "Sending rollback notification..."
|
||||||
|
"$NOTIFY_SCRIPT" "rollback" \
|
||||||
|
"Rollback executed. Traffic switched from $CURRENT_STACK to $ROLLBACK_STACK. Reason: $REASON" \
|
||||||
|
|| echo " WARNING: Notification failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "ROLLBACK COMPLETE"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
echo "Summary:"
|
||||||
|
echo " Previous stack: $CURRENT_STACK (now inactive)"
|
||||||
|
echo " Current stack: $ROLLBACK_STACK (now active)"
|
||||||
|
echo " Reason: $REASON"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Investigate why $CURRENT_STACK failed"
|
||||||
|
echo " 2. Check logs: docker logs mvp-backend-$CURRENT_STACK"
|
||||||
|
echo " 3. Fix issues before next deployment"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
exit 0
|
||||||
185
scripts/ci/health-check.sh
Executable file
185
scripts/ci/health-check.sh
Executable file
@@ -0,0 +1,185 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Health check script for blue-green deployment
|
||||||
|
# Verifies container health and HTTP endpoints
|
||||||
|
#
|
||||||
|
# Usage: ./health-check.sh <stack> [timeout_seconds]
|
||||||
|
# stack: blue or green
|
||||||
|
# timeout_seconds: max wait time (default: 60)
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 - All health checks passed
|
||||||
|
# 1 - Health check failed
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
|
||||||
|
STACK="${1:-}"
|
||||||
|
TIMEOUT="${2:-60}"
|
||||||
|
|
||||||
|
if [[ -z "$STACK" ]] || [[ ! "$STACK" =~ ^(blue|green)$ ]]; then
|
||||||
|
echo "Usage: $0 <blue|green> [timeout_seconds]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
FRONTEND_CONTAINER="mvp-frontend-$STACK"
|
||||||
|
BACKEND_CONTAINER="mvp-backend-$STACK"
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo "Health Check - $STACK Stack"
|
||||||
|
echo "Timeout: ${TIMEOUT}s"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Function to check Docker container health
|
||||||
|
check_container_health() {
|
||||||
|
local container="$1"
|
||||||
|
local status
|
||||||
|
|
||||||
|
status=$(docker inspect --format='{{.State.Health.Status}}' "$container" 2>/dev/null || echo "not found")
|
||||||
|
|
||||||
|
case "$status" in
|
||||||
|
"healthy")
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
"starting")
|
||||||
|
return 2 # Still starting
|
||||||
|
;;
|
||||||
|
"unhealthy"|"not found"|"")
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check HTTP endpoint
|
||||||
|
check_http_endpoint() {
|
||||||
|
local container="$1"
|
||||||
|
local port="$2"
|
||||||
|
local path="$3"
|
||||||
|
|
||||||
|
if docker exec "$container" curl -sf "http://localhost:${port}${path}" > /dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check database connectivity via backend
|
||||||
|
check_database_connectivity() {
|
||||||
|
local container="$1"
|
||||||
|
|
||||||
|
# The /health endpoint should verify database connectivity
|
||||||
|
if docker exec "$container" curl -sf "http://localhost:3001/health" 2>/dev/null | grep -q '"database"'; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait for containers to be healthy
|
||||||
|
wait_for_health() {
|
||||||
|
local container="$1"
|
||||||
|
local elapsed=0
|
||||||
|
|
||||||
|
while [[ $elapsed -lt $TIMEOUT ]]; do
|
||||||
|
check_container_health "$container"
|
||||||
|
local status=$?
|
||||||
|
|
||||||
|
if [[ $status -eq 0 ]]; then
|
||||||
|
return 0
|
||||||
|
elif [[ $status -eq 1 ]]; then
|
||||||
|
echo " ERROR: Container $container is unhealthy"
|
||||||
|
docker logs "$container" --tail 20 2>/dev/null || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Still starting, wait
|
||||||
|
sleep 2
|
||||||
|
elapsed=$((elapsed + 2))
|
||||||
|
echo " Waiting for $container... (${elapsed}s/${TIMEOUT}s)"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo " ERROR: Timeout waiting for $container"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main health check sequence
|
||||||
|
echo ""
|
||||||
|
echo "Step 1/4: Checking container status..."
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
for container in "$FRONTEND_CONTAINER" "$BACKEND_CONTAINER"; do
|
||||||
|
running=$(docker inspect --format='{{.State.Running}}' "$container" 2>/dev/null || echo "false")
|
||||||
|
if [[ "$running" != "true" ]]; then
|
||||||
|
echo " ERROR: Container $container is not running"
|
||||||
|
docker ps -a --filter "name=$container" --format "table {{.Names}}\t{{.Status}}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " OK: $container is running"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Step 2/4: Waiting for Docker health checks..."
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
for container in "$FRONTEND_CONTAINER" "$BACKEND_CONTAINER"; do
|
||||||
|
echo " Checking $container..."
|
||||||
|
if ! wait_for_health "$container"; then
|
||||||
|
echo " FAILED: $container health check"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " OK: $container is healthy"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Step 3/4: Verifying HTTP endpoints..."
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
# Check frontend
|
||||||
|
echo " Checking frontend HTTP..."
|
||||||
|
if ! check_http_endpoint "$FRONTEND_CONTAINER" 3000 "/"; then
|
||||||
|
echo " FAILED: Frontend HTTP check"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " OK: Frontend responds on port 3000"
|
||||||
|
|
||||||
|
# Check backend health endpoint
|
||||||
|
echo " Checking backend HTTP..."
|
||||||
|
if ! check_http_endpoint "$BACKEND_CONTAINER" 3001 "/health"; then
|
||||||
|
echo " FAILED: Backend HTTP check"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " OK: Backend responds on port 3001"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Step 4/4: Verifying database connectivity..."
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
if ! check_database_connectivity "$BACKEND_CONTAINER"; then
|
||||||
|
echo " WARNING: Could not verify database connectivity"
|
||||||
|
echo " (Backend may not expose database status in /health)"
|
||||||
|
else
|
||||||
|
echo " OK: Database connectivity verified"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "Health Check PASSED - $STACK Stack"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Update state file
|
||||||
|
STATE_FILE="$PROJECT_ROOT/config/deployment/state.json"
|
||||||
|
if [[ -f "$STATE_FILE" ]]; then
|
||||||
|
# Update stack health status using jq if available
|
||||||
|
if command -v jq &> /dev/null; then
|
||||||
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
jq --arg stack "$STACK" --arg ts "$TIMESTAMP" \
|
||||||
|
'.[$stack].healthy = true | .[$stack].last_health_check = $ts' \
|
||||||
|
"$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
220
scripts/ci/maintenance-migrate.sh
Executable file
220
scripts/ci/maintenance-migrate.sh
Executable file
@@ -0,0 +1,220 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Maintenance mode migration script
|
||||||
|
# Enables maintenance mode, runs migrations, then restores service
|
||||||
|
#
|
||||||
|
# Usage: ./maintenance-migrate.sh [backup]
|
||||||
|
# backup: If set, creates a database backup before migration
|
||||||
|
#
|
||||||
|
# This script is for BREAKING migrations that require downtime.
|
||||||
|
# Non-breaking migrations run automatically on container start.
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 - Migration successful
|
||||||
|
# 1 - Migration failed (maintenance mode will remain active)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
|
||||||
|
CREATE_BACKUP="${1:-}"
|
||||||
|
|
||||||
|
COMPOSE_FILE="$PROJECT_ROOT/docker-compose.yml"
|
||||||
|
COMPOSE_BLUE_GREEN="$PROJECT_ROOT/docker-compose.blue-green.yml"
|
||||||
|
STATE_FILE="$PROJECT_ROOT/config/deployment/state.json"
|
||||||
|
NOTIFY_SCRIPT="$SCRIPT_DIR/notify.sh"
|
||||||
|
TRAEFIK_CONFIG="$PROJECT_ROOT/config/traefik/dynamic/blue-green.yml"
|
||||||
|
BACKUP_DIR="$PROJECT_ROOT/data/backups"
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo "MAINTENANCE MODE MIGRATION"
|
||||||
|
echo "Time: $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Determine active stack
|
||||||
|
if [[ -f "$STATE_FILE" ]] && command -v jq &> /dev/null; then
|
||||||
|
ACTIVE_STACK=$(jq -r '.active_stack // "blue"' "$STATE_FILE")
|
||||||
|
else
|
||||||
|
ACTIVE_STACK="blue"
|
||||||
|
fi
|
||||||
|
|
||||||
|
BACKEND_CONTAINER="mvp-backend-$ACTIVE_STACK"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Configuration:"
|
||||||
|
echo " Active stack: $ACTIVE_STACK"
|
||||||
|
echo " Backend container: $BACKEND_CONTAINER"
|
||||||
|
echo " Create backup: ${CREATE_BACKUP:-no}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 1: Send maintenance notification
|
||||||
|
echo "Step 1/6: Sending maintenance notification..."
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
if [[ -x "$NOTIFY_SCRIPT" ]]; then
|
||||||
|
"$NOTIFY_SCRIPT" "maintenance_start" "Starting maintenance window for database migration" || true
|
||||||
|
fi
|
||||||
|
echo " OK"
|
||||||
|
|
||||||
|
# Step 2: Enable maintenance mode
|
||||||
|
echo ""
|
||||||
|
echo "Step 2/6: Enabling maintenance mode..."
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
# Update state file
|
||||||
|
if [[ -f "$STATE_FILE" ]] && command -v jq &> /dev/null; then
|
||||||
|
jq '.maintenance_mode = true' "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stop traffic to both stacks (weight 0)
|
||||||
|
echo " Stopping traffic to application stacks..."
|
||||||
|
|
||||||
|
# Set both stacks to weight 0 - Traefik will return 503
|
||||||
|
sed -i.bak -E "s/(name: mvp-frontend-blue-svc[[:space:]]+weight:)[[:space:]]+[0-9]+/\1 0/" "$TRAEFIK_CONFIG"
|
||||||
|
sed -i.bak -E "s/(name: mvp-frontend-green-svc[[:space:]]+weight:)[[:space:]]+[0-9]+/\1 0/" "$TRAEFIK_CONFIG"
|
||||||
|
sed -i.bak -E "s/(name: mvp-backend-blue-svc[[:space:]]+weight:)[[:space:]]+[0-9]+/\1 0/" "$TRAEFIK_CONFIG"
|
||||||
|
sed -i.bak -E "s/(name: mvp-backend-green-svc[[:space:]]+weight:)[[:space:]]+[0-9]+/\1 0/" "$TRAEFIK_CONFIG"
|
||||||
|
rm -f "${TRAEFIK_CONFIG}.bak"
|
||||||
|
|
||||||
|
# Wait for Traefik to pick up changes
|
||||||
|
sleep 3
|
||||||
|
echo " OK: Maintenance mode active"
|
||||||
|
|
||||||
|
# Step 3: Create database backup (if requested)
|
||||||
|
if [[ -n "$CREATE_BACKUP" ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "Step 3/6: Creating database backup..."
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
BACKUP_FILE="$BACKUP_DIR/pre-migration-$(date +%Y%m%d-%H%M%S).sql"
|
||||||
|
|
||||||
|
if docker exec mvp-postgres pg_dump -U postgres motovaultpro > "$BACKUP_FILE"; then
|
||||||
|
echo " OK: Backup created: $BACKUP_FILE"
|
||||||
|
ls -lh "$BACKUP_FILE"
|
||||||
|
else
|
||||||
|
echo " ERROR: Backup failed"
|
||||||
|
echo ""
|
||||||
|
echo "Aborting migration. Restoring traffic..."
|
||||||
|
|
||||||
|
# Restore traffic
|
||||||
|
if [[ "$ACTIVE_STACK" == "blue" ]]; then
|
||||||
|
sed -i -E "s/(name: mvp-frontend-blue-svc[[:space:]]+weight:)[[:space:]]+[0-9]+/\1 100/" "$TRAEFIK_CONFIG"
|
||||||
|
sed -i -E "s/(name: mvp-backend-blue-svc[[:space:]]+weight:)[[:space:]]+[0-9]+/\1 100/" "$TRAEFIK_CONFIG"
|
||||||
|
else
|
||||||
|
sed -i -E "s/(name: mvp-frontend-green-svc[[:space:]]+weight:)[[:space:]]+[0-9]+/\1 100/" "$TRAEFIK_CONFIG"
|
||||||
|
sed -i -E "s/(name: mvp-backend-green-svc[[:space:]]+weight:)[[:space:]]+[0-9]+/\1 100/" "$TRAEFIK_CONFIG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$STATE_FILE" ]] && command -v jq &> /dev/null; then
|
||||||
|
jq '.maintenance_mode = false' "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "Step 3/6: Skipping backup (not requested)..."
|
||||||
|
echo "----------------------------------------"
|
||||||
|
echo " OK"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 4: Run migrations
|
||||||
|
echo ""
|
||||||
|
echo "Step 4/6: Running database migrations..."
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
# Flush Redis cache before migration
|
||||||
|
echo " Flushing Redis cache..."
|
||||||
|
docker exec mvp-redis redis-cli FLUSHALL > /dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
echo " Running migrations..."
|
||||||
|
if docker exec "$BACKEND_CONTAINER" npm run migrate; then
|
||||||
|
echo " OK: Migrations completed"
|
||||||
|
else
|
||||||
|
echo " ERROR: Migration failed"
|
||||||
|
echo ""
|
||||||
|
echo "Migration failed. Maintenance mode remains active."
|
||||||
|
echo "To restore:"
|
||||||
|
echo " 1. Fix migration issues"
|
||||||
|
echo " 2. Re-run migrations: docker exec $BACKEND_CONTAINER npm run migrate"
|
||||||
|
echo " 3. Run: $0 to retry, or manually restore traffic"
|
||||||
|
|
||||||
|
if [[ -x "$NOTIFY_SCRIPT" ]]; then
|
||||||
|
"$NOTIFY_SCRIPT" "failure" "Database migration failed during maintenance window. Manual intervention required." || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 5: Restart backend containers
|
||||||
|
echo ""
|
||||||
|
echo "Step 5/6: Restarting backend containers..."
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
# Restart both backends to pick up any schema changes
|
||||||
|
docker restart mvp-backend-blue mvp-backend-green 2>/dev/null || true
|
||||||
|
|
||||||
|
# Wait for backends to be healthy
|
||||||
|
echo " Waiting for backends to be healthy..."
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
for container in mvp-backend-blue mvp-backend-green; do
|
||||||
|
for i in {1..12}; do
|
||||||
|
health=$(docker inspect --format='{{.State.Health.Status}}' "$container" 2>/dev/null || echo "unknown")
|
||||||
|
if [[ "$health" == "healthy" ]]; then
|
||||||
|
echo " OK: $container is healthy"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [[ $i -eq 12 ]]; then
|
||||||
|
echo " WARNING: $container health check timeout"
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# Step 6: Disable maintenance mode
|
||||||
|
echo ""
|
||||||
|
echo "Step 6/6: Restoring traffic..."
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
# Restore traffic to active stack
|
||||||
|
if [[ "$ACTIVE_STACK" == "blue" ]]; then
|
||||||
|
sed -i -E "s/(name: mvp-frontend-blue-svc[[:space:]]+weight:)[[:space:]]+[0-9]+/\1 100/" "$TRAEFIK_CONFIG"
|
||||||
|
sed -i -E "s/(name: mvp-backend-blue-svc[[:space:]]+weight:)[[:space:]]+[0-9]+/\1 100/" "$TRAEFIK_CONFIG"
|
||||||
|
else
|
||||||
|
sed -i -E "s/(name: mvp-frontend-green-svc[[:space:]]+weight:)[[:space:]]+[0-9]+/\1 100/" "$TRAEFIK_CONFIG"
|
||||||
|
sed -i -E "s/(name: mvp-backend-green-svc[[:space:]]+weight:)[[:space:]]+[0-9]+/\1 100/" "$TRAEFIK_CONFIG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update state file
|
||||||
|
if [[ -f "$STATE_FILE" ]] && command -v jq &> /dev/null; then
|
||||||
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
jq --arg ts "$TIMESTAMP" '.maintenance_mode = false | .last_migration = $ts' "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wait for Traefik to pick up changes
|
||||||
|
sleep 3
|
||||||
|
echo " OK: Traffic restored"
|
||||||
|
|
||||||
|
# Send completion notification
|
||||||
|
if [[ -x "$NOTIFY_SCRIPT" ]]; then
|
||||||
|
"$NOTIFY_SCRIPT" "maintenance_end" "Maintenance window complete. Database migration successful." || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "MAINTENANCE MIGRATION COMPLETE"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
echo "Summary:"
|
||||||
|
echo " - Migrations: Successful"
|
||||||
|
echo " - Active stack: $ACTIVE_STACK"
|
||||||
|
echo " - Maintenance mode: Disabled"
|
||||||
|
if [[ -n "$CREATE_BACKUP" ]]; then
|
||||||
|
echo " - Backup: $BACKUP_FILE"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
exit 0
|
||||||
84
scripts/ci/mirror-base-images.sh
Executable file
84
scripts/ci/mirror-base-images.sh
Executable file
@@ -0,0 +1,84 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Mirror upstream Docker images to GitLab Container Registry
|
||||||
|
# Run manually or via scheduled GitLab pipeline
|
||||||
|
# This avoids Docker Hub rate limits and ensures build reliability
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REGISTRY="${REGISTRY:-registry.motovaultpro.com/mirrors}"
|
||||||
|
|
||||||
|
# Base images required by MotoVaultPro
|
||||||
|
IMAGES=(
|
||||||
|
"node:20-alpine"
|
||||||
|
"nginx:alpine"
|
||||||
|
"postgres:18-alpine"
|
||||||
|
"redis:8.4-alpine"
|
||||||
|
"traefik:v3.6"
|
||||||
|
"docker:24.0"
|
||||||
|
"docker:24.0-dind"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo "Base Image Mirroring Script"
|
||||||
|
echo "Registry: $REGISTRY"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Check if logged into registry
|
||||||
|
if ! docker info 2>/dev/null | grep -q "Username"; then
|
||||||
|
echo "WARNING: Not logged into Docker registry"
|
||||||
|
echo "Run: docker login registry.motovaultpro.com"
|
||||||
|
fi
|
||||||
|
|
||||||
|
FAILED=()
|
||||||
|
SUCCESS=()
|
||||||
|
|
||||||
|
for img in "${IMAGES[@]}"; do
|
||||||
|
echo ""
|
||||||
|
echo "Processing: $img"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
# Pull from upstream
|
||||||
|
echo " Pulling from upstream..."
|
||||||
|
if ! docker pull "$img"; then
|
||||||
|
echo " ERROR: Failed to pull $img"
|
||||||
|
FAILED+=("$img")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Tag for local registry
|
||||||
|
local_tag="$REGISTRY/$img"
|
||||||
|
echo " Tagging as: $local_tag"
|
||||||
|
docker tag "$img" "$local_tag"
|
||||||
|
|
||||||
|
# Push to local registry
|
||||||
|
echo " Pushing to registry..."
|
||||||
|
if ! docker push "$local_tag"; then
|
||||||
|
echo " ERROR: Failed to push $local_tag"
|
||||||
|
FAILED+=("$img")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
SUCCESS+=("$img")
|
||||||
|
echo " OK: $img mirrored successfully"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "Summary"
|
||||||
|
echo "========================================"
|
||||||
|
echo "Successful: ${#SUCCESS[@]}"
|
||||||
|
for img in "${SUCCESS[@]}"; do
|
||||||
|
echo " - $img"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#FAILED[@]} -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Failed: ${#FAILED[@]}"
|
||||||
|
for img in "${FAILED[@]}"; do
|
||||||
|
echo " - $img"
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "All images mirrored successfully"
|
||||||
195
scripts/ci/notify.sh
Executable file
195
scripts/ci/notify.sh
Executable file
@@ -0,0 +1,195 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Deployment notification script using Resend API
|
||||||
|
# Sends email notifications for deployment events
|
||||||
|
#
|
||||||
|
# Usage: ./notify.sh <event_type> [message] [commit_sha]
|
||||||
|
# event_type: success, failure, rollback, rollback_failed, maintenance_start, maintenance_end
|
||||||
|
# message: Optional custom message
|
||||||
|
# commit_sha: Optional commit SHA for context
|
||||||
|
#
|
||||||
|
# Required environment variables:
|
||||||
|
# DEPLOY_NOTIFY_EMAIL - Recipient email address
|
||||||
|
#
|
||||||
|
# Reads Resend API key from:
|
||||||
|
# /run/secrets/resend-api-key (container)
|
||||||
|
# ./secrets/app/resend-api-key.txt (local)
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 - Notification sent (or skipped if not configured)
|
||||||
|
# 1 - Notification failed
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
|
||||||
|
EVENT_TYPE="${1:-}"
|
||||||
|
MESSAGE="${2:-}"
|
||||||
|
COMMIT_SHA="${3:-${CI_COMMIT_SHORT_SHA:-unknown}}"
|
||||||
|
|
||||||
|
if [[ -z "$EVENT_TYPE" ]]; then
|
||||||
|
echo "Usage: $0 <success|failure|rollback|rollback_failed|maintenance_start|maintenance_end> [message] [commit_sha]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get recipient email
|
||||||
|
NOTIFY_EMAIL="${DEPLOY_NOTIFY_EMAIL:-}"
|
||||||
|
if [[ -z "$NOTIFY_EMAIL" ]]; then
|
||||||
|
echo "DEPLOY_NOTIFY_EMAIL not set, skipping notification"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get Resend API key
|
||||||
|
RESEND_API_KEY=""
|
||||||
|
if [[ -f "/run/secrets/resend-api-key" ]]; then
|
||||||
|
RESEND_API_KEY=$(cat /run/secrets/resend-api-key)
|
||||||
|
elif [[ -f "$PROJECT_ROOT/secrets/app/resend-api-key.txt" ]]; then
|
||||||
|
RESEND_API_KEY=$(cat "$PROJECT_ROOT/secrets/app/resend-api-key.txt")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$RESEND_API_KEY" ]]; then
|
||||||
|
echo "WARNING: Resend API key not found, skipping notification"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine subject and styling based on event type
|
||||||
|
case "$EVENT_TYPE" in
|
||||||
|
"success")
|
||||||
|
SUBJECT="Deployment Successful - MotoVaultPro"
|
||||||
|
STATUS_COLOR="#22c55e"
|
||||||
|
STATUS_EMOJI="[OK]"
|
||||||
|
STATUS_TEXT="Deployment Successful"
|
||||||
|
DEFAULT_MESSAGE="Version $COMMIT_SHA is now live."
|
||||||
|
;;
|
||||||
|
"failure")
|
||||||
|
SUBJECT="Deployment Failed - MotoVaultPro"
|
||||||
|
STATUS_COLOR="#ef4444"
|
||||||
|
STATUS_EMOJI="[FAILED]"
|
||||||
|
STATUS_TEXT="Deployment Failed"
|
||||||
|
DEFAULT_MESSAGE="Deployment of $COMMIT_SHA failed. Check pipeline logs."
|
||||||
|
;;
|
||||||
|
"rollback")
|
||||||
|
SUBJECT="Auto-Rollback Executed - MotoVaultPro"
|
||||||
|
STATUS_COLOR="#f59e0b"
|
||||||
|
STATUS_EMOJI="[ROLLBACK]"
|
||||||
|
STATUS_TEXT="Rollback Executed"
|
||||||
|
DEFAULT_MESSAGE="Automatic rollback was triggered. Previous version restored."
|
||||||
|
;;
|
||||||
|
"rollback_failed")
|
||||||
|
SUBJECT="CRITICAL: Rollback Failed - MotoVaultPro"
|
||||||
|
STATUS_COLOR="#dc2626"
|
||||||
|
STATUS_EMOJI="[CRITICAL]"
|
||||||
|
STATUS_TEXT="Rollback Failed"
|
||||||
|
DEFAULT_MESSAGE="Rollback attempt failed. Manual intervention required immediately."
|
||||||
|
;;
|
||||||
|
"maintenance_start")
|
||||||
|
SUBJECT="Maintenance Mode Started - MotoVaultPro"
|
||||||
|
STATUS_COLOR="#6366f1"
|
||||||
|
STATUS_EMOJI="[MAINTENANCE]"
|
||||||
|
STATUS_TEXT="Maintenance Mode Active"
|
||||||
|
DEFAULT_MESSAGE="Application is in maintenance mode for database migration."
|
||||||
|
;;
|
||||||
|
"maintenance_end")
|
||||||
|
SUBJECT="Maintenance Mode Ended - MotoVaultPro"
|
||||||
|
STATUS_COLOR="#22c55e"
|
||||||
|
STATUS_EMOJI="[ONLINE]"
|
||||||
|
STATUS_TEXT="Maintenance Complete"
|
||||||
|
DEFAULT_MESSAGE="Maintenance window complete. Application is online."
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown event type: $EVENT_TYPE"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Use custom message or default
|
||||||
|
FINAL_MESSAGE="${MESSAGE:-$DEFAULT_MESSAGE}"
|
||||||
|
TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
||||||
|
|
||||||
|
# Build HTML email
|
||||||
|
HTML_BODY=$(cat <<EOF
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f3f4f6; padding: 20px; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.header { background: ${STATUS_COLOR}; color: white; padding: 20px; text-align: center; }
|
||||||
|
.header h1 { margin: 0; font-size: 24px; }
|
||||||
|
.content { padding: 30px; }
|
||||||
|
.status { font-size: 18px; font-weight: 600; margin-bottom: 15px; }
|
||||||
|
.message { color: #374151; line-height: 1.6; margin-bottom: 20px; }
|
||||||
|
.details { background: #f9fafb; border-radius: 6px; padding: 15px; font-size: 14px; }
|
||||||
|
.details-row { display: flex; justify-content: space-between; margin-bottom: 8px; }
|
||||||
|
.details-label { color: #6b7280; }
|
||||||
|
.details-value { color: #111827; font-weight: 500; }
|
||||||
|
.footer { padding: 20px; text-align: center; color: #6b7280; font-size: 12px; border-top: 1px solid #e5e7eb; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>MotoVaultPro</h1>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="status">${STATUS_EMOJI} ${STATUS_TEXT}</div>
|
||||||
|
<div class="message">${FINAL_MESSAGE}</div>
|
||||||
|
<div class="details">
|
||||||
|
<div class="details-row">
|
||||||
|
<span class="details-label">Environment:</span>
|
||||||
|
<span class="details-value">Production</span>
|
||||||
|
</div>
|
||||||
|
<div class="details-row">
|
||||||
|
<span class="details-label">Commit:</span>
|
||||||
|
<span class="details-value">${COMMIT_SHA}</span>
|
||||||
|
</div>
|
||||||
|
<div class="details-row">
|
||||||
|
<span class="details-label">Time:</span>
|
||||||
|
<span class="details-value">${TIMESTAMP}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
MotoVaultPro CI/CD Notification System
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send email via Resend API
|
||||||
|
echo "Sending notification: $EVENT_TYPE"
|
||||||
|
echo " To: $NOTIFY_EMAIL"
|
||||||
|
echo " Subject: $SUBJECT"
|
||||||
|
|
||||||
|
# Build JSON payload
|
||||||
|
JSON_PAYLOAD=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"from": "MotoVaultPro <deploy@motovaultpro.com>",
|
||||||
|
"to": ["$NOTIFY_EMAIL"],
|
||||||
|
"subject": "$SUBJECT",
|
||||||
|
"html": $(echo "$HTML_BODY" | jq -Rs .)
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send via Resend API
|
||||||
|
RESPONSE=$(curl -s -w "\n%{http_code}" \
|
||||||
|
-X POST \
|
||||||
|
-H "Authorization: Bearer $RESEND_API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$JSON_PAYLOAD" \
|
||||||
|
"https://api.resend.com/emails")
|
||||||
|
|
||||||
|
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
||||||
|
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$HTTP_CODE" == "200" ]] || [[ "$HTTP_CODE" == "201" ]]; then
|
||||||
|
echo " OK: Notification sent successfully"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo " ERROR: Failed to send notification (HTTP $HTTP_CODE)"
|
||||||
|
echo " Response: $BODY"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
157
scripts/ci/switch-traffic.sh
Executable file
157
scripts/ci/switch-traffic.sh
Executable file
@@ -0,0 +1,157 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Traffic switching script for blue-green deployment
|
||||||
|
# Updates Traefik weighted routing configuration
|
||||||
|
#
|
||||||
|
# Usage: ./switch-traffic.sh <target_stack> [mode]
|
||||||
|
# target_stack: blue or green (the stack to switch TO)
|
||||||
|
# mode: instant (default) or gradual
|
||||||
|
#
|
||||||
|
# Gradual mode: 25% -> 50% -> 75% -> 100% with 3s intervals
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 - Traffic switch successful
|
||||||
|
# 1 - Switch failed
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
|
||||||
|
TARGET_STACK="${1:-}"
|
||||||
|
MODE="${2:-instant}"
|
||||||
|
|
||||||
|
if [[ -z "$TARGET_STACK" ]] || [[ ! "$TARGET_STACK" =~ ^(blue|green)$ ]]; then
|
||||||
|
echo "Usage: $0 <blue|green> [instant|gradual]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TRAEFIK_CONFIG="$PROJECT_ROOT/config/traefik/dynamic/blue-green.yml"
|
||||||
|
STATE_FILE="$PROJECT_ROOT/config/deployment/state.json"
|
||||||
|
|
||||||
|
if [[ ! -f "$TRAEFIK_CONFIG" ]]; then
|
||||||
|
echo "ERROR: Traefik config not found: $TRAEFIK_CONFIG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo "Traffic Switch"
|
||||||
|
echo "Target Stack: $TARGET_STACK"
|
||||||
|
echo "Mode: $MODE"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Determine source and target weights
|
||||||
|
if [[ "$TARGET_STACK" == "blue" ]]; then
|
||||||
|
SOURCE_STACK="green"
|
||||||
|
else
|
||||||
|
SOURCE_STACK="blue"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Function to update weights in Traefik config
|
||||||
|
update_weights() {
|
||||||
|
local blue_weight="$1"
|
||||||
|
local green_weight="$2"
|
||||||
|
|
||||||
|
echo " Setting weights: blue=$blue_weight, green=$green_weight"
|
||||||
|
|
||||||
|
# Use sed to update weights in the YAML file
|
||||||
|
# Frontend blue weight
|
||||||
|
sed -i.bak -E "s/(name: mvp-frontend-blue-svc[[:space:]]+weight:)[[:space:]]+[0-9]+/\1 $blue_weight/" "$TRAEFIK_CONFIG"
|
||||||
|
# Frontend green weight
|
||||||
|
sed -i.bak -E "s/(name: mvp-frontend-green-svc[[:space:]]+weight:)[[:space:]]+[0-9]+/\1 $green_weight/" "$TRAEFIK_CONFIG"
|
||||||
|
# Backend blue weight
|
||||||
|
sed -i.bak -E "s/(name: mvp-backend-blue-svc[[:space:]]+weight:)[[:space:]]+[0-9]+/\1 $blue_weight/" "$TRAEFIK_CONFIG"
|
||||||
|
# Backend green weight
|
||||||
|
sed -i.bak -E "s/(name: mvp-backend-green-svc[[:space:]]+weight:)[[:space:]]+[0-9]+/\1 $green_weight/" "$TRAEFIK_CONFIG"
|
||||||
|
|
||||||
|
# Clean up backup files
|
||||||
|
rm -f "${TRAEFIK_CONFIG}.bak"
|
||||||
|
|
||||||
|
# Traefik watches the file and reloads automatically
|
||||||
|
# Give it a moment to pick up changes
|
||||||
|
sleep 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify Traefik has picked up the changes
|
||||||
|
verify_traefik_reload() {
|
||||||
|
# Give Traefik time to reload config
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Check if Traefik is still healthy
|
||||||
|
if docker exec mvp-traefik traefik healthcheck > /dev/null 2>&1; then
|
||||||
|
echo " OK: Traefik config reloaded"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo " WARNING: Could not verify Traefik health"
|
||||||
|
return 0 # Don't fail on this, file watcher is reliable
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$MODE" == "gradual" ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "Gradual traffic switch starting..."
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
if [[ "$TARGET_STACK" == "blue" ]]; then
|
||||||
|
STEPS=("25 75" "50 50" "75 25" "100 0")
|
||||||
|
else
|
||||||
|
STEPS=("75 25" "50 50" "25 75" "0 100")
|
||||||
|
fi
|
||||||
|
|
||||||
|
step=1
|
||||||
|
for weights in "${STEPS[@]}"; do
|
||||||
|
blue_w=$(echo "$weights" | cut -d' ' -f1)
|
||||||
|
green_w=$(echo "$weights" | cut -d' ' -f2)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Step $step/4: blue=$blue_w%, green=$green_w%"
|
||||||
|
update_weights "$blue_w" "$green_w"
|
||||||
|
verify_traefik_reload
|
||||||
|
|
||||||
|
if [[ $step -lt 4 ]]; then
|
||||||
|
echo " Waiting 3 seconds before next step..."
|
||||||
|
sleep 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
step=$((step + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
else
|
||||||
|
# Instant switch
|
||||||
|
echo ""
|
||||||
|
echo "Instant traffic switch..."
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
if [[ "$TARGET_STACK" == "blue" ]]; then
|
||||||
|
update_weights 100 0
|
||||||
|
else
|
||||||
|
update_weights 0 100
|
||||||
|
fi
|
||||||
|
|
||||||
|
verify_traefik_reload
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update state file
|
||||||
|
echo ""
|
||||||
|
echo "Updating deployment state..."
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
if [[ -f "$STATE_FILE" ]] && command -v jq &> /dev/null; then
|
||||||
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
jq --arg active "$TARGET_STACK" \
|
||||||
|
--arg inactive "$SOURCE_STACK" \
|
||||||
|
--arg ts "$TIMESTAMP" \
|
||||||
|
'.active_stack = $active | .inactive_stack = $inactive | .last_switch = $ts' \
|
||||||
|
"$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
|
||||||
|
|
||||||
|
echo " Active stack: $TARGET_STACK"
|
||||||
|
echo " Inactive stack: $SOURCE_STACK"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "Traffic Switch COMPLETE"
|
||||||
|
echo "All traffic now routed to: $TARGET_STACK"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
exit 0
|
||||||
Reference in New Issue
Block a user