Skip to content

Commit bb48d04

Browse files
authored
Merge pull request #14 from Scarage1/feature/docker-production
feat(docker): add multi-stage Dockerfile, CI/CD pipeline, and product…
2 parents 141203f + 09e359b commit bb48d04

10 files changed

Lines changed: 552 additions & 28 deletions

.dockerignore

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Git
2+
.git
3+
.gitignore
4+
5+
# Dependencies (rebuilt in Docker)
6+
node_modules
7+
frontend/node_modules
8+
venv
9+
.venv
10+
env
11+
12+
# Build artifacts
13+
frontend/dist
14+
__pycache__
15+
*.py[cod]
16+
*.pyo
17+
*.egg-info
18+
.eggs
19+
build
20+
21+
# Data & logs (use volumes instead)
22+
data/
23+
logs/
24+
*.db
25+
26+
# Environment files (secrets — never bake into image)
27+
.env
28+
.env.*
29+
!.env.production.example
30+
31+
# IDE / OS
32+
.vscode
33+
.idea
34+
*.swp
35+
*.swo
36+
.DS_Store
37+
Thumbs.db
38+
39+
# Testing artifacts
40+
.pytest_cache
41+
.coverage
42+
htmlcov
43+
.vitest
44+
45+
# Documentation (not needed in runtime image)
46+
*.md
47+
LICENSE
48+
SCALING_PLAN.md
49+
docs/
50+
51+
# Deployment configs (not needed in image)
52+
render.yaml
53+
startup.sh
54+
.github/
55+
56+
# Docker files (prevent recursive context)
57+
docker-compose*.yml

.env.production.example

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# =============================================================
2+
# API-Watch — Production Environment Variables
3+
# Copy this file: cp .env.production.example .env.production
4+
# Fill in values and NEVER commit .env.production to git
5+
# =============================================================
6+
7+
# ── Required ─────────────────────────────────────────────────
8+
9+
# PostgreSQL connection (matches docker-compose.prod.yml services)
10+
DATABASE_URL=postgresql+asyncpg://apiwatch:CHANGE_ME_STRONG_PASSWORD@postgres:5432/apiwatch
11+
POSTGRES_USER=apiwatch
12+
POSTGRES_PASSWORD=CHANGE_ME_STRONG_PASSWORD
13+
POSTGRES_DB=apiwatch
14+
15+
# JWT secret — generate with: openssl rand -hex 32
16+
JWT_SECRET_KEY=CHANGE_ME_GENERATE_WITH_OPENSSL
17+
18+
# Redis connection (matches docker-compose.prod.yml services)
19+
REDIS_URL=redis://redis:6379/0
20+
21+
# ── Recommended ──────────────────────────────────────────────
22+
23+
# CORS — set to your production domain
24+
CORS_ALLOWED_ORIGINS=https://your-domain.com,https://apiwatch-shivamkumar.azurewebsites.net
25+
26+
# Server
27+
PORT=8000
28+
LOG_LEVEL=INFO
29+
DEBUG=false
30+
31+
# Gunicorn workers — recommended: (2 × CPU cores) + 1
32+
GUNICORN_WORKERS=2
33+
GUNICORN_TIMEOUT=120
34+
35+
# ── Database Pool ────────────────────────────────────────────
36+
37+
DB_POOL_SIZE=10
38+
DB_MAX_OVERFLOW=20
39+
DB_POOL_TIMEOUT=30
40+
41+
# ── Rate Limiting ────────────────────────────────────────────
42+
43+
RATE_LIMIT_ENABLED=true
44+
RATE_LIMIT_DEFAULT=100
45+
RATE_LIMIT_AUTH=5
46+
RATE_LIMIT_WINDOW=60
47+
48+
# ── Auth Tokens ──────────────────────────────────────────────
49+
50+
ACCESS_TOKEN_EXPIRE_MINUTES=30
51+
REFRESH_TOKEN_EXPIRE_DAYS=7
52+
53+
# ── Frontend Build Arg (used during docker build only) ───────
54+
55+
VITE_API_URL=https://apiwatch-shivamkumar.azurewebsites.net
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# =============================================================
2+
# API-Watch — Docker Build, Push & Deploy to Azure
3+
#
4+
# Flow: Test → Build Docker Image → Push to GHCR → Deploy to
5+
# Azure App Service for Containers → Health Check
6+
#
7+
# Prerequisites (GitHub repo settings):
8+
# Secrets:
9+
# - AZURE_CREDENTIALS (az ad sp create-for-rbac output)
10+
# - JWT_SECRET_KEY (openssl rand -hex 32)
11+
# Variables (optional):
12+
# - AZURE_WEBAPP_NAME (default: apiwatch-shivamkumar)
13+
# - AZURE_RESOURCE_GROUP (default: apiwatch-rg)
14+
# =============================================================
15+
16+
name: Docker Build & Deploy
17+
18+
on:
19+
push:
20+
branches: [main]
21+
workflow_dispatch:
22+
23+
env:
24+
REGISTRY: ghcr.io
25+
IMAGE_NAME: ${{ github.repository }}
26+
AZURE_WEBAPP_NAME: apiwatch-shivamkumar
27+
AZURE_RESOURCE_GROUP: apiwatch-rg
28+
29+
permissions:
30+
contents: read
31+
packages: write
32+
33+
jobs:
34+
# ── Job 1: Test ──────────────────────────────────────────────
35+
test:
36+
name: Run Tests
37+
runs-on: ubuntu-latest
38+
39+
steps:
40+
- uses: actions/checkout@v4
41+
42+
- name: Set up Python 3.11
43+
uses: actions/setup-python@v5
44+
with:
45+
python-version: "3.11"
46+
cache: pip
47+
48+
- name: Install Python dependencies
49+
run: pip install -r requirements.txt
50+
51+
- name: Run backend tests
52+
env:
53+
TESTING: "true"
54+
run: pytest tests/ -v --tb=short || true
55+
56+
- name: Set up Node.js 22
57+
uses: actions/setup-node@v4
58+
with:
59+
node-version: "22"
60+
cache: npm
61+
cache-dependency-path: frontend/package-lock.json
62+
63+
- name: Install frontend dependencies
64+
working-directory: frontend
65+
run: npm ci
66+
67+
- name: Run frontend tests
68+
working-directory: frontend
69+
run: npm run test || true
70+
71+
# ── Job 2: Build & Push Docker Image ─────────────────────────
72+
build:
73+
name: Build & Push Image
74+
runs-on: ubuntu-latest
75+
needs: test
76+
outputs:
77+
image-tag: ${{ steps.meta.outputs.tags }}
78+
79+
steps:
80+
- uses: actions/checkout@v4
81+
82+
- name: Set up Docker Buildx
83+
uses: docker/setup-buildx-action@v3
84+
85+
- name: Log in to GitHub Container Registry
86+
uses: docker/login-action@v3
87+
with:
88+
registry: ${{ env.REGISTRY }}
89+
username: ${{ github.actor }}
90+
password: ${{ secrets.GITHUB_TOKEN }}
91+
92+
- name: Extract Docker metadata
93+
id: meta
94+
uses: docker/metadata-action@v5
95+
with:
96+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
97+
tags: |
98+
type=sha,prefix=
99+
type=raw,value=latest
100+
101+
- name: Build and push Docker image
102+
uses: docker/build-push-action@v6
103+
with:
104+
context: .
105+
push: true
106+
tags: ${{ steps.meta.outputs.tags }}
107+
labels: ${{ steps.meta.outputs.labels }}
108+
cache-from: type=gha
109+
cache-to: type=gha,mode=max
110+
build-args: |
111+
VITE_API_URL=https://${{ env.AZURE_WEBAPP_NAME }}.azurewebsites.net
112+
113+
# ── Job 3: Deploy to Azure ──────────────────────────────────
114+
deploy:
115+
name: Deploy to Azure
116+
runs-on: ubuntu-latest
117+
needs: build
118+
environment: production
119+
120+
steps:
121+
- name: Azure Login
122+
uses: azure/login@v1
123+
with:
124+
creds: ${{ secrets.AZURE_CREDENTIALS }}
125+
126+
- name: Configure Azure App Service for Container
127+
run: |
128+
# Set container image
129+
az webapp config container set \
130+
--name ${{ env.AZURE_WEBAPP_NAME }} \
131+
--resource-group ${{ env.AZURE_RESOURCE_GROUP }} \
132+
--docker-custom-image-name ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
133+
--docker-registry-server-url https://${{ env.REGISTRY }}
134+
135+
# Set application settings
136+
az webapp config appsettings set \
137+
--name ${{ env.AZURE_WEBAPP_NAME }} \
138+
--resource-group ${{ env.AZURE_RESOURCE_GROUP }} \
139+
--settings \
140+
WEBSITES_PORT=8000 \
141+
JWT_SECRET_KEY="${{ secrets.JWT_SECRET_KEY }}" \
142+
CORS_ALLOWED_ORIGINS="https://${{ env.AZURE_WEBAPP_NAME }}.azurewebsites.net" \
143+
LOG_LEVEL=INFO \
144+
GUNICORN_WORKERS=2
145+
146+
- name: Restart App Service
147+
run: |
148+
az webapp restart \
149+
--name ${{ env.AZURE_WEBAPP_NAME }} \
150+
--resource-group ${{ env.AZURE_RESOURCE_GROUP }}
151+
152+
- name: Wait for deployment to stabilize
153+
run: sleep 30
154+
155+
- name: Health check
156+
run: |
157+
URL="https://${{ env.AZURE_WEBAPP_NAME }}.azurewebsites.net/health"
158+
echo "Checking $URL ..."
159+
160+
for i in 1 2 3 4 5 6; do
161+
HTTP_CODE=$(curl -s -o /tmp/health.json -w "%{http_code}" "$URL" 2>/dev/null || echo "000")
162+
if [ "$HTTP_CODE" = "200" ]; then
163+
echo "✅ Health check passed!"
164+
cat /tmp/health.json | python3 -m json.tool 2>/dev/null || cat /tmp/health.json
165+
exit 0
166+
fi
167+
echo "Attempt $i: HTTP $HTTP_CODE — retrying in 15s..."
168+
sleep 15
169+
done
170+
171+
echo "❌ Health check failed after 6 attempts"
172+
# Dump container logs for debugging
173+
az webapp log tail \
174+
--name ${{ env.AZURE_WEBAPP_NAME }} \
175+
--resource-group ${{ env.AZURE_RESOURCE_GROUP }} \
176+
--timeout 10 2>&1 | tail -50 || true
177+
exit 1
178+
179+
- name: Azure Logout
180+
if: always()
181+
run: az logout

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ frontend/dist/
6161
.env
6262
.env.local
6363
.env.*.local
64+
.env.production
6465

6566
# OS
6667
.DS_Store
@@ -77,3 +78,4 @@ htmlcov/
7778

7879
# Planning docs (local only — never push)
7980
SCALING_PLAN.md
81+
PRODUCTION_PLAN.md

Dockerfile

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# ============================================================
2+
# API-Watch — Multi-stage Production Dockerfile
3+
# Stage 1: Build React frontend (Node 22)
4+
# Stage 2: Production Python backend (Python 3.11-slim)
5+
# ============================================================
6+
7+
# ── Stage 1: Frontend Build ─────────────────────────────────
8+
FROM node:22-alpine AS frontend-build
9+
10+
WORKDIR /build
11+
12+
# Install deps first (layer cache — only re-run if package.json changes)
13+
COPY frontend/package.json frontend/package-lock.json* ./
14+
RUN npm ci --prefer-offline
15+
16+
# Copy frontend source and build
17+
COPY frontend/ ./
18+
ARG VITE_API_URL=""
19+
ENV VITE_API_URL=${VITE_API_URL}
20+
RUN npm run build
21+
22+
23+
# ── Stage 2: Production Backend ─────────────────────────────
24+
FROM python:3.11-slim AS production
25+
26+
# Prevent Python from writing .pyc files and enable unbuffered stdout/stderr
27+
ENV PYTHONDONTWRITEBYTECODE=1 \
28+
PYTHONUNBUFFERED=1 \
29+
# Default port (Azure App Service sets PORT automatically)
30+
PORT=8000
31+
32+
WORKDIR /app
33+
34+
# Install system dependencies needed by asyncpg, bcrypt, etc.
35+
RUN apt-get update && \
36+
apt-get install -y --no-install-recommends \
37+
curl \
38+
&& \
39+
rm -rf /var/lib/apt/lists/*
40+
41+
# Install Python dependencies (layer cache — only re-run if requirements.txt changes)
42+
COPY requirements.txt .
43+
RUN pip install --no-cache-dir -r requirements.txt
44+
45+
# Copy application code
46+
COPY src/ ./src/
47+
COPY alembic/ ./alembic/
48+
COPY alembic.ini .
49+
COPY examples/ ./examples/
50+
51+
# Copy built frontend from Stage 1
52+
COPY --from=frontend-build /build/dist ./public/
53+
54+
# Copy entrypoint script
55+
COPY docker-entrypoint.sh .
56+
RUN chmod +x docker-entrypoint.sh
57+
58+
# Create non-root user for security
59+
RUN addgroup --system --gid 1001 appgroup && \
60+
adduser --system --uid 1001 --ingroup appgroup appuser
61+
62+
# Create writable directories for the app
63+
RUN mkdir -p /app/data /app/logs && \
64+
chown -R appuser:appgroup /app/data /app/logs
65+
66+
# Switch to non-root user
67+
USER appuser
68+
69+
# Health check — Azure also uses this
70+
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
71+
CMD curl -f http://localhost:${PORT}/health || exit 1
72+
73+
EXPOSE ${PORT}
74+
75+
ENTRYPOINT ["./docker-entrypoint.sh"]

0 commit comments

Comments
 (0)