From f6bc4b0e407a59347726f9a6c12a8fc0a00f6215 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 31 Jan 2026 23:03:26 -0500 Subject: [PATCH] Add Railway deployment configuration - Add Dockerfiles for backend (FastAPI) and frontend (nginx) - Add railway.toml configs with health checks - Add .dockerignore files for optimized builds - Update config.py to auto-convert Railway DATABASE_URL format - Add FRONTEND_URL env var for production CORS Co-Authored-By: Claude Opus 4.5 --- backend/.dockerignore | 20 ++++++++++++++++++++ backend/Dockerfile | 22 ++++++++++++++++++++++ backend/app/core/config.py | 30 ++++++++++++++++++++++++++++-- backend/app/main.py | 2 +- backend/railway.toml | 9 +++++++++ frontend/.dockerignore | 10 ++++++++++ frontend/Dockerfile | 34 ++++++++++++++++++++++++++++++++++ frontend/nginx.conf | 21 +++++++++++++++++++++ frontend/railway.toml | 9 +++++++++ 9 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 backend/.dockerignore create mode 100644 backend/Dockerfile create mode 100644 backend/railway.toml create mode 100644 frontend/.dockerignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/nginx.conf create mode 100644 frontend/railway.toml diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 00000000..d5b331e7 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,20 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +.env +.env.* +.venv +venv/ +.git +.gitignore +.pytest_cache +.mypy_cache +*.egg-info +dist/ +build/ +.coverage +htmlcov/ +.tox +*.log diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 00000000..708d6c3f --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.12-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Expose port (Railway uses PORT env variable) +EXPOSE 8000 + +# Run the application - use shell form to expand $PORT +CMD uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000} diff --git a/backend/app/core/config.py b/backend/app/core/config.py index f5f49075..cd858f87 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,4 +1,5 @@ from pydantic_settings import BaseSettings +from pydantic import field_validator from typing import Optional @@ -8,10 +9,26 @@ class Settings(BaseSettings): DEBUG: bool = False API_V1_PREFIX: str = "/api/v1" - # Database + # Database - Railway provides DATABASE_URL, we convert it for asyncpg DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/patherly" DATABASE_URL_SYNC: str = "postgresql://postgres:postgres@localhost:5432/patherly" + @field_validator("DATABASE_URL", mode="before") + @classmethod + def convert_database_url(cls, v: str) -> str: + """Convert standard postgres URL to asyncpg format.""" + if v.startswith("postgresql://"): + return v.replace("postgresql://", "postgresql+asyncpg://", 1) + return v + + @field_validator("DATABASE_URL_SYNC", mode="before") + @classmethod + def ensure_sync_url(cls, v: str) -> str: + """Ensure sync URL uses standard postgresql prefix.""" + if v.startswith("postgresql+asyncpg://"): + return v.replace("postgresql+asyncpg://", "postgresql://", 1) + return v + # JWT Settings SECRET_KEY: str = "your-secret-key-change-in-production-use-openssl-rand-hex-32" ALGORITHM: str = "HS256" @@ -21,8 +38,17 @@ class Settings(BaseSettings): # Security BCRYPT_ROUNDS: int = 12 - # CORS + # CORS - set FRONTEND_URL in production (e.g., https://patherly.up.railway.app) CORS_ORIGINS: list[str] = ["http://localhost:3000", "http://localhost:5173", "http://localhost:5174"] + FRONTEND_URL: Optional[str] = None + + @property + def allowed_origins(self) -> list[str]: + """Get all allowed CORS origins including FRONTEND_URL if set.""" + origins = self.CORS_ORIGINS.copy() + if self.FRONTEND_URL and self.FRONTEND_URL not in origins: + origins.append(self.FRONTEND_URL) + return origins class Config: env_file = ".env" diff --git a/backend/app/main.py b/backend/app/main.py index 6670d45b..5d963379 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -44,7 +44,7 @@ app.add_middleware(RequestLoggingMiddleware) # Configure CORS app.add_middleware( CORSMiddleware, - allow_origins=settings.CORS_ORIGINS, + allow_origins=settings.allowed_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/backend/railway.toml b/backend/railway.toml new file mode 100644 index 00000000..f77ddc05 --- /dev/null +++ b/backend/railway.toml @@ -0,0 +1,9 @@ +[build] +builder = "dockerfile" +dockerfilePath = "Dockerfile" + +[deploy] +healthcheckPath = "/health" +healthcheckTimeout = 100 +restartPolicyType = "on_failure" +restartPolicyMaxRetries = 3 diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 00000000..2498722b --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,10 @@ +node_modules +dist +.git +.gitignore +*.log +.env +.env.* +.vscode +coverage +.eslintcache diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 00000000..89c7f77a --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,34 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Build argument for API URL (set at build time) +ARG VITE_API_URL +ENV VITE_API_URL=$VITE_API_URL + +# Build the application +RUN npm run build + +# Production stage +FROM nginx:alpine + +# Copy custom nginx config +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy built files from builder stage +COPY --from=builder /app/dist /usr/share/nginx/html + +# Expose port +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 00000000..bdfe966e --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,21 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + # Handle SPA routing - serve index.html for all routes + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/frontend/railway.toml b/frontend/railway.toml new file mode 100644 index 00000000..9b27d9c1 --- /dev/null +++ b/frontend/railway.toml @@ -0,0 +1,9 @@ +[build] +builder = "dockerfile" +dockerfilePath = "Dockerfile" + +[deploy] +healthcheckPath = "/" +healthcheckTimeout = 100 +restartPolicyType = "on_failure" +restartPolicyMaxRetries = 3