-
Notifications
You must be signed in to change notification settings - Fork 4
Render deploy #599
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
kbighorse
wants to merge
11
commits into
staging
Choose a base branch
from
render-deploy
base: staging
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Render deploy #599
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
252699e
feat: add health check endpoints for Render deployment
kblake 02a6c31
feat: add DATABASE_URL environment variable support
kblake af13c04
feat: optimize Docker setup for Render deployment
kblake a792970
feat: add Render deployment configuration with staging and production
kblake bb03c21
feat: add database schema dump and sample data for new deployments
kblake aec528d
chore: configure render.yaml for free tier testing
kbighorse b421d65
fix: skip local db wait when DATABASE_URL is set (Render)
kbighorse eab8024
refactor: switch to native Python runtime instead of Docker
kbighorse 9c93db0
Remove Docker files to force native Python runtime on Render
kbighorse 70feb5c
Remove Procfile to use render.yaml startCommand
kbighorse c11c5ef
Fix alembic to use DATABASE_URL for Render deployment
kbighorse File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,248 @@ | ||
| # Render Deployment Guide | ||
|
|
||
| This guide explains how to deploy OcotilloAPI to Render using the `render.yaml` blueprint. | ||
|
|
||
| ## Overview | ||
|
|
||
| The `render.yaml` file defines two complete environments: | ||
| - **Staging**: Auto-deploys from `staging` branch | ||
| - **Production**: Manual deploys from `main` branch (requires approval) | ||
|
|
||
| ## Architecture | ||
|
|
||
| ### Services Created | ||
|
|
||
| 1. **Web Services** (2) | ||
| - `ocotillo-api-staging` - Staging environment | ||
| - `ocotillo-api-production` - Production environment | ||
|
|
||
| 2. **Databases** (2) | ||
| - `ocotillo-db-staging` - PostgreSQL 17 with PostGIS | ||
| - `ocotillo-db-production` - PostgreSQL 17 with PostGIS | ||
|
|
||
| 3. **Environment Variable Groups** (3) | ||
| - `ocotillo-shared` - Common configuration for all environments | ||
| - `ocotillo-staging` - Staging-specific settings | ||
| - `ocotillo-production` - Production-specific settings | ||
|
|
||
| ## Initial Deployment | ||
|
|
||
| ### Step 1: Connect Repository | ||
|
|
||
| 1. Log in to [Render Dashboard](https://dashboard.render.com) | ||
| 2. Click **"New Blueprint Instance"** | ||
| 3. Connect your GitHub repository | ||
| 4. Select the branch containing `render.yaml` (e.g., `render-deploy`) | ||
|
|
||
| ### Step 2: Configure Environment Variables | ||
|
|
||
| Render will automatically create the environment variable groups, but you need to set values for `sync: false` variables: | ||
|
|
||
| #### Staging Environment (`ocotillo-staging` group) | ||
|
|
||
| Set these in the Render dashboard: | ||
|
|
||
| ```bash | ||
| # Authentik OAuth Configuration | ||
| AUTHENTIK_URL=https://your-staging-authentik-instance.com | ||
| AUTHENTIK_CLIENT_ID=your_staging_client_id | ||
| AUTHENTIK_AUTHORIZE_URL=https://your-staging-authentik-instance.com/application/o/authorize/ | ||
| AUTHENTIK_TOKEN_URL=https://your-staging-authentik-instance.com/application/o/token/ | ||
|
|
||
| # Google Cloud Storage (if using assets) | ||
| GCS_BUCKET_NAME=your-staging-bucket | ||
| GOOGLE_APPLICATION_CREDENTIALS=/path/to/staging/credentials.json | ||
| ``` | ||
|
|
||
| #### Production Environment (`ocotillo-production` group) | ||
|
|
||
| ```bash | ||
| # Authentik OAuth Configuration | ||
| AUTHENTIK_URL=https://your-production-authentik-instance.com | ||
| AUTHENTIK_CLIENT_ID=your_production_client_id | ||
| AUTHENTIK_AUTHORIZE_URL=https://your-production-authentik-instance.com/application/o/authorize/ | ||
| AUTHENTIK_TOKEN_URL=https://your-production-authentik-instance.com/application/o/token/ | ||
|
|
||
| # Google Cloud Storage (if using assets) | ||
| GCS_BUCKET_NAME=your-production-bucket | ||
| GOOGLE_APPLICATION_CREDENTIALS=/path/to/production/credentials.json | ||
| ``` | ||
|
|
||
| **Note**: `SESSION_SECRET_KEY` is auto-generated by Render (`generateValue: true`) | ||
|
|
||
| ### Step 3: Review and Deploy | ||
|
|
||
| 1. Review the services that will be created | ||
| 2. Click **"Apply"** to create all resources | ||
| 3. Render will: | ||
| - Create PostgreSQL databases with PostGIS extension | ||
| - Build Docker images | ||
| - Run database migrations | ||
| - Deploy the applications | ||
|
|
||
| ## Database Configuration | ||
|
|
||
| ### PostGIS Extension | ||
|
|
||
| The `preDeployCommand` automatically installs PostGIS: | ||
| ```bash | ||
| psql $DATABASE_URL -c "CREATE EXTENSION IF NOT EXISTS postgis;" | ||
| ``` | ||
|
|
||
| ### Connection Details | ||
|
|
||
| - **DATABASE_URL** is automatically provided by Render | ||
| - Connections are internal-only by default (`ipAllowList: []`) | ||
| - To allow external connections, add IP addresses to `ipAllowList` in `render.yaml` | ||
|
|
||
| ## Health Checks | ||
|
|
||
| Both environments use `/health/ready` endpoint: | ||
| - Checks application responsiveness | ||
| - Verifies database connectivity | ||
| - Returns 200 if healthy, 503 if not ready | ||
|
|
||
| ## Deployment Workflow | ||
|
|
||
| ### Staging Deployments | ||
|
|
||
| ```bash | ||
| git checkout staging | ||
| git merge render-deploy # or your feature branch | ||
| git push origin staging | ||
| ``` | ||
|
|
||
| **Result**: Automatic deployment to staging environment | ||
|
|
||
| ### Production Deployments | ||
|
|
||
| ```bash | ||
| git checkout main | ||
| git merge staging | ||
| git push origin main | ||
| ``` | ||
|
|
||
| **Result**: Build triggered, but requires manual approval in Render dashboard | ||
|
|
||
| ## Environment-Specific Configuration | ||
|
|
||
| ### Staging | ||
| - `MODE=development` | ||
| - Auto-deploy enabled | ||
| - Authentication enabled (Authentik) | ||
| - Smaller instance size (starter plan) | ||
|
|
||
| ### Production | ||
| - `MODE=production` | ||
| - Manual deploy (requires approval) | ||
| - Authentication enabled (Authentik) | ||
| - Larger instance size (standard plan) | ||
|
|
||
| ## Service Plans | ||
|
|
||
| Current configuration in `render.yaml`: | ||
|
|
||
| | Service | Plan | Notes | | ||
| |---------|------|-------| | ||
| | Staging Web | Starter | Suitable for testing | | ||
| | Production Web | Standard | Recommended for production traffic | | ||
| | Staging DB | Standard | Can downgrade to Starter if needed | | ||
| | Production DB | Standard | Recommended for production data | | ||
|
|
||
| To change plans, edit the `plan` field in `render.yaml`. | ||
|
|
||
| ## Scaling Configuration | ||
|
|
||
| The application uses **Gunicorn with 4 workers** by default (configured in `entrypoint.sh`). | ||
|
|
||
| To adjust workers: | ||
| 1. Edit `entrypoint.sh` | ||
| 2. Change `--workers 4` to desired number | ||
| 3. Commit and push changes | ||
|
|
||
| **Recommended**: 2-4 workers per container, scale horizontally by adding more instances. | ||
|
|
||
| ## Monitoring | ||
|
|
||
| ### Health Check Endpoints | ||
|
|
||
| - `/health/live` - Basic liveness check | ||
| - `/health/ready` - Readiness check with DB connectivity | ||
| - `/health/status` - Detailed status with pool metrics | ||
|
|
||
| ### Logs | ||
|
|
||
| Access logs in Render dashboard: | ||
| 1. Navigate to your service | ||
| 2. Click **"Logs"** tab | ||
| 3. Filter by severity or search terms | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| ### Database Connection Issues | ||
|
|
||
| Check if PostGIS extension is installed: | ||
| ```bash | ||
| # Via Render Shell | ||
| psql $DATABASE_URL -c "SELECT PostGIS_Version();" | ||
| ``` | ||
|
|
||
| ### Migration Failures | ||
|
|
||
| Run migrations manually via Render Shell: | ||
| ```bash | ||
| alembic upgrade head | ||
| ``` | ||
|
|
||
| ### Port Binding Issues | ||
|
|
||
| Ensure `$PORT` environment variable is being used (already configured in `entrypoint.sh`). | ||
|
|
||
| ## Updating the Blueprint | ||
|
|
||
| After modifying `render.yaml`: | ||
|
|
||
| 1. Commit and push changes | ||
| 2. Go to Render dashboard | ||
| 3. Navigate to Blueprint instance | ||
| 4. Click **"Sync"** to apply changes | ||
|
|
||
| **Warning**: Some changes may cause service restarts. | ||
|
|
||
| ## Cost Optimization | ||
|
|
||
| ### Staging Environment | ||
|
|
||
| Consider downgrading if budget-constrained: | ||
| - Web service: `starter` → `free` (with limitations) | ||
| - Database: `standard` → `starter` (1GB storage, no point-in-time recovery) | ||
|
|
||
| ### Production Environment | ||
|
|
||
| Recommended minimum: | ||
| - Web service: `standard` (for multiple workers) | ||
| - Database: `standard` (for backups and PITR) | ||
|
|
||
| ## Security Considerations | ||
|
|
||
| 1. **Authentication**: Authentik OAuth is enabled by default | ||
| 2. **Database**: Internal-only access (no public IP by default) | ||
| 3. **Secrets**: All sensitive values use `sync: false` (not in git) | ||
| 4. **HTTPS**: Automatically provided by Render | ||
|
|
||
| ## Next Steps | ||
|
|
||
| After deployment: | ||
|
|
||
| 1. ✅ Verify health checks are passing | ||
| 2. ✅ Test API endpoints | ||
| 3. ✅ Configure custom domains (optional) | ||
| 4. ✅ Set up monitoring/alerting | ||
| 5. ✅ Configure backups schedule | ||
| 6. ✅ Review and optimize database performance | ||
|
|
||
| ## Support | ||
|
|
||
| - [Render Documentation](https://render.com/docs) | ||
| - [Render Status Page](https://status.render.com) | ||
| - [Community Forum](https://community.render.com) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| """Health check endpoints for deployment monitoring and container orchestration.""" | ||
|
|
||
| from datetime import datetime | ||
| from typing import Any | ||
|
|
||
| from fastapi import APIRouter, HTTPException, status | ||
| from sqlalchemy import text | ||
|
|
||
| from core.dependencies import session_dependency | ||
|
|
||
| router = APIRouter(prefix="/health", tags=["Health"]) | ||
|
|
||
|
|
||
| @router.get("/live", summary="Liveness probe") | ||
| async def liveness_check() -> dict[str, str]: | ||
| """ | ||
| Basic liveness probe - checks if the application is running. | ||
|
|
||
| This endpoint should always return 200 if the app is alive. | ||
| No dependencies checked - just confirms the web server is responding. | ||
|
|
||
| Use this for Kubernetes/Docker liveness probes. | ||
| """ | ||
| return {"status": "ok"} | ||
|
|
||
|
|
||
| @router.get("/ready", summary="Readiness probe") | ||
| async def readiness_check(session: session_dependency) -> dict[str, Any]: | ||
| """ | ||
| Readiness probe - checks if the application is ready to serve traffic. | ||
|
|
||
| Verifies: | ||
| - Application is running | ||
| - Database connection is available | ||
|
|
||
| Returns 200 if ready, 503 if not ready. | ||
| Use this for Kubernetes/Docker readiness probes and load balancer health checks. | ||
| """ | ||
| try: | ||
| # Test database connectivity with a simple query | ||
| result = await session.execute(text("SELECT 1")) | ||
| result.scalar() | ||
|
|
||
| return { | ||
| "status": "ready", | ||
| "database": "connected", | ||
| "checks": {"db_connection": True}, | ||
| } | ||
| except Exception as e: | ||
| raise HTTPException( | ||
| status_code=status.HTTP_503_SERVICE_UNAVAILABLE, | ||
| detail={ | ||
| "status": "not_ready", | ||
| "database": "disconnected", | ||
| "error": str(e), | ||
| "checks": {"db_connection": False}, | ||
| }, | ||
| ) | ||
|
|
||
|
|
||
| @router.get("/status", summary="Detailed status information") | ||
| async def status_check(session: session_dependency) -> dict[str, Any]: | ||
| """ | ||
| Detailed status endpoint - provides comprehensive health information. | ||
|
|
||
| Returns: | ||
| - Application status | ||
| - Database connectivity and pool information | ||
| - Timestamp | ||
| - Additional service status | ||
|
|
||
| Use this for monitoring dashboards and detailed health reports. | ||
| """ | ||
| timestamp = datetime.utcnow().isoformat() | ||
|
|
||
| # Check database connectivity | ||
| db_status = "disconnected" | ||
| db_connected = False | ||
| pool_info = {} | ||
|
|
||
| try: | ||
| result = await session.execute(text("SELECT 1")) | ||
| result.scalar() | ||
| db_status = "connected" | ||
| db_connected = True | ||
|
|
||
| # Get connection pool information | ||
| pool = session.get_bind().pool | ||
| pool_info = { | ||
| "size": pool.size(), | ||
| "overflow": pool.overflow() if hasattr(pool, "overflow") else None, | ||
| "checked_in": pool.checkedin() if hasattr(pool, "checkedin") else None, | ||
| "checked_out": pool.checkedout() if hasattr(pool, "checkedout") else None, | ||
| } | ||
| # Remove None values | ||
| pool_info = {k: v for k, v in pool_info.items() if v is not None} | ||
|
|
||
| except Exception as e: | ||
| db_status = f"error: {str(e)}" | ||
|
|
||
| response = { | ||
| "status": "healthy" if db_connected else "degraded", | ||
| "timestamp": timestamp, | ||
| "database": { | ||
| "status": db_status, | ||
| "connected": db_connected, | ||
| }, | ||
| "services": {"admin": "available", "cors": "enabled"}, | ||
| } | ||
|
|
||
| # Add pool info if available | ||
| if pool_info: | ||
| response["database"]["pool_info"] = pool_info | ||
|
|
||
| # Return 503 if database is not connected | ||
| if not db_connected: | ||
| raise HTTPException( | ||
| status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=response | ||
| ) | ||
|
|
||
| return response | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check warning
Code scanning / CodeQL
Information exposure through an exception Medium
Copilot Autofix
AI 6 days ago
In general, to fix this class of problem you should avoid including raw exception messages or stack traces in API responses. Instead, log the detailed error server-side (using your logging framework) and return a generic, non-sensitive status or error description to the client.
For this specific code:
readiness_check, thedetailfield currently includes"error": str(e). This should be replaced with a generic message such as"error": "database connectivity check failed"to avoid leakingstr(e)to the user. Optionally, log the real exception with a logger.status_check, theexceptblock setsdb_status = f"error: {str(e)}", which is then returned to the client inresponse["database"]["status"]. Replace this with a generic description like"error: database connectivity check failed"and, again, optionally log the exception.We can introduce a
loggingimport at the top ofapi/health.pyand a module logger vialogger = logging.getLogger(__name__). In bothexceptblocks, calllogger.exception(...)so developers still get full tracebacks in the logs, while the API only returns sanitized messages. No changes to function signatures or overall behavior (HTTP status codes, JSON structure, keys like"status"/"database"/"checks") are needed—only the content of the error strings changes.