From 74f0b968b8327505beccf29c39840a418fe94d0f Mon Sep 17 00:00:00 2001 From: Jenish-1235 Date: Sat, 20 Dec 2025 03:50:44 +0530 Subject: [PATCH 1/3] chore : add docker compose for prod and update config loader to use url encoding --- config.py | 15 +++++--- docker-compose.prod.yml | 79 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 docker-compose.prod.yml diff --git a/config.py b/config.py index 8aaf76d..b58be7e 100644 --- a/config.py +++ b/config.py @@ -7,6 +7,8 @@ 3. .env file (fallback for development, bootstrap credentials) """ +from urllib.parse import quote_plus + from pydantic import field_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -103,14 +105,17 @@ def is_development(self) -> bool: @property def postgres_url(self) -> str: - """Build PostgreSQL connection URL.""" + """Build PostgreSQL connection URL with URL-encoded password.""" if not self.postgres_password: return f"postgresql://{self.postgres_user}@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}" - return f"postgresql://{self.postgres_user}:{self.postgres_password}@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}" + # URL-encode password to handle special characters (@, :, /, etc.) + encoded_password = quote_plus(self.postgres_password) + encoded_user = quote_plus(self.postgres_user) + return f"postgresql://{encoded_user}:{encoded_password}@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}" @property def redis_url(self) -> str: - """Build Redis connection URL.""" + """Build Redis connection URL with URL-encoded password.""" # Check for explicit REDIS_URL environment variable first (useful for Docker Compose) import os @@ -120,7 +125,9 @@ def redis_url(self) -> str: # Otherwise, build from components if self.redis_password: - return f"redis://:{self.redis_password}@{self.redis_host}:{self.redis_port}/{self.redis_db}" + # URL-encode password to handle special characters (@, :, /, %, etc.) + encoded_password = quote_plus(self.redis_password) + return f"redis://:{encoded_password}@{self.redis_host}:{self.redis_port}/{self.redis_db}" return f"redis://{self.redis_host}:{self.redis_port}/{self.redis_db}" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..3b4c33c --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,79 @@ +version: '3.8' + +# Production Docker Compose for NeroSpatial Backend +# This file should be deployed to Azure VM via Jenkins +# Only bootstrap credentials needed - rest loaded from Azure App Config & Key Vault + +services: + postgres: + image: postgres:16-alpine + container_name: nerospatial-postgres + environment: + POSTGRES_DB: ${POSTGRES_DB:-nerospatial} + POSTGRES_USER: ${POSTGRES_USER:-nerospatial} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - postgres-data:/var/lib/postgresql/data + restart: unless-stopped + networks: + - nerospatial-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-nerospatial} -d ${POSTGRES_DB:-nerospatial}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + + redis: + image: redis:7-alpine + container_name: nerospatial-redis + command: redis-server --requirepass ${REDIS_PASSWORD} + volumes: + - redis-data:/data + restart: unless-stopped + networks: + - nerospatial-network + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + + backend: + image: harinypatel/nerospatial-backend:latest + container_name: nerospatial-backend + ports: + - "8000:8000" + environment: + # Bootstrap credentials only - app will load rest from Azure App Config & Key Vault + - ENVIRONMENT=production + - AZURE_KEY_VAULT_URL=${AZURE_KEY_VAULT_URL} + - AZURE_APP_CONFIG_URL=${AZURE_APP_CONFIG_URL} + - AZURE_TENANT_ID=${AZURE_TENANT_ID} + - AZURE_CLIENT_ID=${AZURE_CLIENT_ID} + - AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET} + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + restart: unless-stopped + networks: + - nerospatial-network + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s # Give app time to load config from Azure + +volumes: + postgres-data: + driver: local + redis-data: + driver: local + +networks: + nerospatial-network: + driver: bridge From 8330c6ae2c39451c85ebffe2638ba9fcba12bafc Mon Sep 17 00:00:00 2001 From: Jenish-1235 Date: Sat, 20 Dec 2025 04:19:04 +0530 Subject: [PATCH 2/3] ci : update ci image verification to use prod vars --- .github/workflows/ci.yml | 134 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 124 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64d1be4..271f68d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,17 +134,131 @@ jobs: cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache cache-to: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache,mode=max - - name: Verify pushed image + - name: Verify pushed image with docker-compose + env: + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} + REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }} + AZURE_KEY_VAULT_URL: ${{ secrets.AZURE_KEY_VAULT_URL }} + AZURE_APP_CONFIG_URL: ${{ secrets.AZURE_APP_CONFIG_URL }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} run: | - # Pull the image we just pushed using branch name tag (guaranteed to exist) - # The metadata action creates tags: branch-name, sha-, and latest (for main) + # Pull the image we just pushed IMAGE_TAG="${{ env.IMAGE_NAME }}:${{ github.ref_name }}" docker pull ${IMAGE_TAG} - docker run -d --name test-container -p 8000:8000 ${IMAGE_TAG} - sleep 5 - curl --fail http://localhost:8000/health || exit 1 - docker stop test-container - docker rm test-container + + # Tag it as latest for docker-compose + docker tag ${IMAGE_TAG} harinypatel/nerospatial-backend:latest + + # Create docker-compose file for verification + cat > docker-compose.verify.yml << 'EOF' + version: '3.8' + + services: + postgres: + image: postgres:16-alpine + container_name: verify-postgres + environment: + POSTGRES_DB: nerospatial + POSTGRES_USER: nerospatial + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U nerospatial -d nerospatial"] + interval: 5s + timeout: 5s + retries: 5 + start_period: 5s + networks: + - verify-network + + redis: + image: redis:7-alpine + container_name: verify-redis + command: redis-server --requirepass ${REDIS_PASSWORD} + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] + interval: 5s + timeout: 5s + retries: 5 + start_period: 5s + networks: + - verify-network + + backend: + image: harinypatel/nerospatial-backend:latest + container_name: verify-backend + ports: + - "8000:8000" + environment: + - ENVIRONMENT=production + - AZURE_KEY_VAULT_URL=${AZURE_KEY_VAULT_URL} + - AZURE_APP_CONFIG_URL=${AZURE_APP_CONFIG_URL} + - AZURE_TENANT_ID=${AZURE_TENANT_ID} + - AZURE_CLIENT_ID=${AZURE_CLIENT_ID} + - AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET} + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - verify-network + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] + interval: 10s + timeout: 10s + retries: 3 + start_period: 30s + + networks: + verify-network: + driver: bridge + EOF + + # Start services + docker-compose -f docker-compose.verify.yml up -d + + # Wait for backend to be healthy (up to 60 seconds) + echo "Waiting for backend to start and connect to Azure services..." + timeout=60 + elapsed=0 + while [ $elapsed -lt $timeout ]; do + if docker ps | grep -q verify-backend; then + # Check if container is still running (not crashed) + if ! docker ps | grep verify-backend | grep -q Up; then + echo "Backend container stopped unexpectedly" + docker logs verify-backend + docker-compose -f docker-compose.verify.yml down + exit 1 + fi + + # Try health check + if curl -f -s http://localhost:8000/health > /dev/null 2>&1; then + echo "Backend health check passed!" + break + fi + fi + sleep 2 + elapsed=$((elapsed + 2)) + done + + # Final health check + if ! curl -f http://localhost:8000/health; then + echo "Health check failed after $timeout seconds" + echo "Backend logs:" + docker logs verify-backend + echo "Postgres logs:" + docker logs verify-postgres + echo "Redis logs:" + docker logs verify-redis + docker-compose -f docker-compose.verify.yml down + exit 1 + fi + + # Cleanup + docker-compose -f docker-compose.verify.yml down + echo "Image verification successful" - name: Workflow Summary if: always() @@ -153,9 +267,9 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "### Build Status" >> $GITHUB_STEP_SUMMARY if [ "${{ job.status }}" == "success" ]; then - echo "✅ **Build Successful**" >> $GITHUB_STEP_SUMMARY + echo "**Build Successful**" >> $GITHUB_STEP_SUMMARY else - echo "❌ **Build Failed**" >> $GITHUB_STEP_SUMMARY + echo "**Build Failed**" >> $GITHUB_STEP_SUMMARY fi echo "" >> $GITHUB_STEP_SUMMARY echo "### Image Tags" >> $GITHUB_STEP_SUMMARY From b73d0f0784ff08989fd2d68e3c90dc6a52e14e4a Mon Sep 17 00:00:00 2001 From: Jenish-1235 Date: Sat, 20 Dec 2025 04:26:56 +0530 Subject: [PATCH 3/3] ci : update docker-compose commands to use newer 'docker compose' commands --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 271f68d..1129a4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,7 +148,7 @@ jobs: IMAGE_TAG="${{ env.IMAGE_NAME }}:${{ github.ref_name }}" docker pull ${IMAGE_TAG} - # Tag it as latest for docker-compose + # Tag it as latest for docker compose docker tag ${IMAGE_TAG} harinypatel/nerospatial-backend:latest # Create docker-compose file for verification @@ -217,7 +217,7 @@ jobs: EOF # Start services - docker-compose -f docker-compose.verify.yml up -d + docker compose -f docker-compose.verify.yml up -d # Wait for backend to be healthy (up to 60 seconds) echo "Waiting for backend to start and connect to Azure services..." @@ -229,7 +229,7 @@ jobs: if ! docker ps | grep verify-backend | grep -q Up; then echo "Backend container stopped unexpectedly" docker logs verify-backend - docker-compose -f docker-compose.verify.yml down + docker compose -f docker-compose.verify.yml down exit 1 fi @@ -252,12 +252,12 @@ jobs: docker logs verify-postgres echo "Redis logs:" docker logs verify-redis - docker-compose -f docker-compose.verify.yml down + docker compose -f docker-compose.verify.yml down exit 1 fi # Cleanup - docker-compose -f docker-compose.verify.yml down + docker compose -f docker-compose.verify.yml down echo "Image verification successful" - name: Workflow Summary