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 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-01-31 23:03:26 -05:00
parent 2421f10dbd
commit f6bc4b0e40
9 changed files with 154 additions and 3 deletions

20
backend/.dockerignore Normal file
View File

@@ -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

22
backend/Dockerfile Normal file
View File

@@ -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}

View File

@@ -1,4 +1,5 @@
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from pydantic import field_validator
from typing import Optional from typing import Optional
@@ -8,10 +9,26 @@ class Settings(BaseSettings):
DEBUG: bool = False DEBUG: bool = False
API_V1_PREFIX: str = "/api/v1" 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: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/patherly"
DATABASE_URL_SYNC: str = "postgresql://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 # JWT Settings
SECRET_KEY: str = "your-secret-key-change-in-production-use-openssl-rand-hex-32" SECRET_KEY: str = "your-secret-key-change-in-production-use-openssl-rand-hex-32"
ALGORITHM: str = "HS256" ALGORITHM: str = "HS256"
@@ -21,8 +38,17 @@ class Settings(BaseSettings):
# Security # Security
BCRYPT_ROUNDS: int = 12 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"] 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: class Config:
env_file = ".env" env_file = ".env"

View File

@@ -44,7 +44,7 @@ app.add_middleware(RequestLoggingMiddleware)
# Configure CORS # Configure CORS
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=settings.CORS_ORIGINS, allow_origins=settings.allowed_origins,
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],

9
backend/railway.toml Normal file
View File

@@ -0,0 +1,9 @@
[build]
builder = "dockerfile"
dockerfilePath = "Dockerfile"
[deploy]
healthcheckPath = "/health"
healthcheckTimeout = 100
restartPolicyType = "on_failure"
restartPolicyMaxRetries = 3

10
frontend/.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
node_modules
dist
.git
.gitignore
*.log
.env
.env.*
.vscode
coverage
.eslintcache

34
frontend/Dockerfile Normal file
View File

@@ -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;"]

21
frontend/nginx.conf Normal file
View File

@@ -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";
}
}

9
frontend/railway.toml Normal file
View File

@@ -0,0 +1,9 @@
[build]
builder = "dockerfile"
dockerfilePath = "Dockerfile"
[deploy]
healthcheckPath = "/"
healthcheckTimeout = 100
restartPolicyType = "on_failure"
restartPolicyMaxRetries = 3