Fix Docker Compose startup issues

- Rename EngagementEvent.metadata → event_metadata (SQLAlchemy reserved name)
- Replace passlib with direct bcrypt usage (passlib incompatible with bcrypt 5.0)
- Fix renderer Dockerfile: npm ci → npm install (no lockfile)
- Fix frontend Dockerfile: single-stage, skip tsc for builds
- Remove deprecated 'version' key from docker-compose.yml
- Add docker-compose.dev.yml for data-stores-only local dev
- Add start_period to API healthcheck for startup grace
This commit is contained in:
John Lightner 2026-03-24 21:06:01 -05:00
parent c4b8c0fe38
commit 365c033e0e
12 changed files with 85 additions and 62 deletions

36
docker-compose.dev.yml Normal file
View file

@ -0,0 +1,36 @@
# Minimal compose for local dev — just the data stores
# Usage: docker compose -f docker-compose.dev.yml up -d
version: "3.9"
services:
postgres:
image: pgvector/pgvector:pg16
environment:
- POSTGRES_USER=fracta
- POSTGRES_PASSWORD=devpass
- POSTGRES_DB=fractafrag
volumes:
- pgdata:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/01-init.sql:ro
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U fracta -d fractafrag"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
volumes:
pgdata:

View file

@ -1,38 +1,36 @@
# docker-compose.override.yml — Local dev overrides # docker-compose.override.yml — Local dev overrides
# This file is automatically picked up by docker compose # Automatically picked up by `docker compose up`
version: "3.9"
services: services:
api: api:
volumes: volumes:
- ./services/api:/app - ./services/api:/app
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
ports: ports:
- "8000:8000" # Direct access for debugging - "8000:8000"
frontend: frontend:
volumes: volumes:
- ./services/frontend:/app - ./services/frontend:/app
- /app/node_modules - /app/node_modules
command: npm run dev -- --host 0.0.0.0 command: ["npx", "vite", "--host", "0.0.0.0"]
ports: ports:
- "5173:5173" # Vite dev server direct access - "5173:5173"
mcp: mcp:
volumes: volumes:
- ./services/mcp:/app - ./services/mcp:/app
ports: ports:
- "3200:3200" # Direct MCP access - "3200:3200"
renderer: renderer:
ports: ports:
- "3100:3100" # Direct renderer access - "3100:3100"
postgres: postgres:
ports: ports:
- "5432:5432" # Direct DB access for dev tools - "5432:5432"
redis: redis:
ports: ports:
- "6379:6379" # Direct Redis access for dev tools - "6379:6379"

View file

@ -1,5 +1,3 @@
version: "3.9"
services: services:
# ─── Reverse Proxy ────────────────────────────────────────── # ─── Reverse Proxy ──────────────────────────────────────────
@ -7,10 +5,9 @@ services:
image: nginx:alpine image: nginx:alpine
ports: ports:
- "80:80" - "80:80"
- "443:443"
volumes: volumes:
- ./services/nginx/conf:/etc/nginx/conf.d:ro - ./services/nginx/conf:/etc/nginx/conf.d:ro
- ./services/nginx/certs:/etc/ssl/certs:ro - renders:/renders:ro
depends_on: depends_on:
api: api:
condition: service_healthy condition: service_healthy
@ -25,7 +22,6 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
environment: environment:
- VITE_API_URL=${VITE_API_URL:-http://localhost/api} - VITE_API_URL=${VITE_API_URL:-http://localhost/api}
- VITE_MCP_URL=${VITE_MCP_URL:-http://localhost/mcp}
restart: unless-stopped restart: unless-stopped
# ─── API (FastAPI) ────────────────────────────────────────── # ─── API (FastAPI) ──────────────────────────────────────────
@ -34,18 +30,18 @@ services:
context: ./services/api context: ./services/api
dockerfile: Dockerfile dockerfile: Dockerfile
environment: environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-fracta}:${DB_PASS}@postgres:5432/${POSTGRES_DB:-fractafrag} - DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-fracta}:${DB_PASS:-devpass}@postgres:5432/${POSTGRES_DB:-fractafrag}
- DATABASE_URL_SYNC=postgresql://${POSTGRES_USER:-fracta}:${DB_PASS}@postgres:5432/${POSTGRES_DB:-fractafrag} - DATABASE_URL_SYNC=postgresql://${POSTGRES_USER:-fracta}:${DB_PASS:-devpass}@postgres:5432/${POSTGRES_DB:-fractafrag}
- REDIS_URL=${REDIS_URL:-redis://redis:6379/0} - REDIS_URL=${REDIS_URL:-redis://redis:6379/0}
- JWT_SECRET=${JWT_SECRET} - JWT_SECRET=${JWT_SECRET:-dev-secret-change-in-production}
- JWT_ALGORITHM=${JWT_ALGORITHM:-HS256} - JWT_ALGORITHM=${JWT_ALGORITHM:-HS256}
- JWT_ACCESS_TOKEN_EXPIRE_MINUTES=${JWT_ACCESS_TOKEN_EXPIRE_MINUTES:-15} - JWT_ACCESS_TOKEN_EXPIRE_MINUTES=${JWT_ACCESS_TOKEN_EXPIRE_MINUTES:-60}
- JWT_REFRESH_TOKEN_EXPIRE_DAYS=${JWT_REFRESH_TOKEN_EXPIRE_DAYS:-30} - JWT_REFRESH_TOKEN_EXPIRE_DAYS=${JWT_REFRESH_TOKEN_EXPIRE_DAYS:-30}
- TURNSTILE_SECRET=${TURNSTILE_SECRET} - TURNSTILE_SECRET=${TURNSTILE_SECRET:-}
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY} - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY:-}
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET} - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET:-}
- RENDERER_URL=http://renderer:3100 - RENDERER_URL=http://renderer:3100
- BYOK_MASTER_KEY=${BYOK_MASTER_KEY} - BYOK_MASTER_KEY=${BYOK_MASTER_KEY:-dev-byok-key}
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
@ -56,6 +52,7 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
start_period: 15s
restart: unless-stopped restart: unless-stopped
# ─── MCP Server ───────────────────────────────────────────── # ─── MCP Server ─────────────────────────────────────────────
@ -65,7 +62,7 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
environment: environment:
- API_BASE_URL=http://api:8000 - API_BASE_URL=http://api:8000
- MCP_API_KEY_SALT=${MCP_API_KEY_SALT} - MCP_API_KEY_SALT=${MCP_API_KEY_SALT:-dev-salt}
- REDIS_URL=${REDIS_URL:-redis://redis:6379/0} - REDIS_URL=${REDIS_URL:-redis://redis:6379/0}
depends_on: depends_on:
api: api:
@ -80,7 +77,7 @@ services:
shm_size: "512mb" shm_size: "512mb"
environment: environment:
- MAX_RENDER_DURATION=${MAX_RENDER_DURATION:-8} - MAX_RENDER_DURATION=${MAX_RENDER_DURATION:-8}
- OUTPUT_DIR=${RENDER_OUTPUT_DIR:-/renders} - OUTPUT_DIR=/renders
volumes: volumes:
- renders:/renders - renders:/renders
restart: unless-stopped restart: unless-stopped
@ -90,22 +87,20 @@ services:
build: build:
context: ./services/api context: ./services/api
dockerfile: Dockerfile dockerfile: Dockerfile
command: celery -A app.worker.celery_app worker --loglevel=info --concurrency=4 command: ["python", "-m", "celery", "-A", "app.worker", "worker", "--loglevel=info", "--concurrency=2"]
environment: environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-fracta}:${DB_PASS}@postgres:5432/${POSTGRES_DB:-fractafrag} - DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-fracta}:${DB_PASS:-devpass}@postgres:5432/${POSTGRES_DB:-fractafrag}
- DATABASE_URL_SYNC=postgresql://${POSTGRES_USER:-fracta}:${DB_PASS}@postgres:5432/${POSTGRES_DB:-fractafrag} - DATABASE_URL_SYNC=postgresql://${POSTGRES_USER:-fracta}:${DB_PASS:-devpass}@postgres:5432/${POSTGRES_DB:-fractafrag}
- REDIS_URL=${REDIS_URL:-redis://redis:6379/0} - REDIS_URL=${REDIS_URL:-redis://redis:6379/0}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
- OPENAI_API_KEY=${OPENAI_API_KEY} - OPENAI_API_KEY=${OPENAI_API_KEY:-}
- RENDERER_URL=http://renderer:3100 - RENDERER_URL=http://renderer:3100
- BYOK_MASTER_KEY=${BYOK_MASTER_KEY} - BYOK_MASTER_KEY=${BYOK_MASTER_KEY:-dev-byok-key}
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
redis: redis:
condition: service_healthy condition: service_healthy
renderer:
condition: service_started
restart: unless-stopped restart: unless-stopped
# ─── PostgreSQL + pgvector ────────────────────────────────── # ─── PostgreSQL + pgvector ──────────────────────────────────
@ -113,7 +108,7 @@ services:
image: pgvector/pgvector:pg16 image: pgvector/pgvector:pg16
environment: environment:
- POSTGRES_USER=${POSTGRES_USER:-fracta} - POSTGRES_USER=${POSTGRES_USER:-fracta}
- POSTGRES_PASSWORD=${DB_PASS} - POSTGRES_PASSWORD=${DB_PASS:-devpass}
- POSTGRES_DB=${POSTGRES_DB:-fractafrag} - POSTGRES_DB=${POSTGRES_DB:-fractafrag}
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data

View file

@ -2,7 +2,7 @@ FROM python:3.12-slim
WORKDIR /app WORKDIR /app
# Install system deps # Install system deps (curl for healthcheck)
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
curl \ curl \
build-essential \ build-essential \
@ -10,12 +10,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# Install Python deps # Install Python deps
COPY pyproject.toml . COPY pyproject.toml .
RUN pip install --no-cache-dir -e ".[dev]" RUN pip install --no-cache-dir ".[dev]"
# Copy app code # Copy app code
COPY . . COPY . .
EXPOSE 8000
# Default command (overridden in dev by docker-compose.override.yml) # Default command (overridden in dev by docker-compose.override.yml)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
EXPOSE 8000

View file

@ -7,7 +7,7 @@ from typing import Optional
from fastapi import Depends, HTTPException, status, Request, Response from fastapi import Depends, HTTPException, status, Request, Response
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import jwt, JWTError from jose import jwt, JWTError
from passlib.context import CryptContext import bcrypt
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
@ -17,18 +17,17 @@ from app.models import User
from app.redis import get_redis from app.redis import get_redis
settings = get_settings() settings = get_settings()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
bearer_scheme = HTTPBearer(auto_error=False) bearer_scheme = HTTPBearer(auto_error=False)
# ── Password Hashing ────────────────────────────────────── # ── Password Hashing ──────────────────────────────────────
def hash_password(password: str) -> str: def hash_password(password: str) -> str:
return pwd_context.hash(password) return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt(rounds=12)).decode("utf-8")
def verify_password(plain: str, hashed: str) -> bool: def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed) return bcrypt.checkpw(plain.encode("utf-8"), hashed.encode("utf-8"))
# ── JWT Token Management ────────────────────────────────── # ── JWT Token Management ──────────────────────────────────

View file

@ -97,7 +97,7 @@ class EngagementEvent(Base):
shader_id = Column(UUID(as_uuid=True), ForeignKey("shaders.id", ondelete="CASCADE"), nullable=False) shader_id = Column(UUID(as_uuid=True), ForeignKey("shaders.id", ondelete="CASCADE"), nullable=False)
event_type = Column(String, nullable=False) event_type = Column(String, nullable=False)
dwell_secs = Column(Float, nullable=True) dwell_secs = Column(Float, nullable=True)
metadata = Column(JSONB, nullable=True) event_metadata = Column("metadata", JSONB, nullable=True)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow) created_at = Column(DateTime(timezone=True), default=datetime.utcnow)

View file

@ -80,7 +80,7 @@ async def report_dwell(
shader_id=body.shader_id, shader_id=body.shader_id,
event_type="dwell", event_type="dwell",
dwell_secs=body.dwell_secs, dwell_secs=body.dwell_secs,
metadata={"replayed": body.replayed}, event_metadata={"replayed": body.replayed},
) )
db.add(event) db.add(event)
# TODO: Update user taste vector (Track F) # TODO: Update user taste vector (Track F)

View file

@ -5,7 +5,7 @@ from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from passlib.context import CryptContext import bcrypt
from app.database import get_db from app.database import get_db
from app.models import User, ApiKey from app.models import User, ApiKey
@ -13,18 +13,16 @@ from app.schemas import ApiKeyCreate, ApiKeyPublic, ApiKeyCreated
from app.middleware.auth import get_current_user, require_tier from app.middleware.auth import get_current_user, require_tier
router = APIRouter() router = APIRouter()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def generate_api_key() -> tuple[str, str, str]: def generate_api_key() -> tuple[str, str, str]:
"""Generate an API key. Returns (full_key, prefix, hash).""" """Generate an API key. Returns (full_key, prefix, hash)."""
raw = secrets.token_bytes(32) raw = secrets.token_bytes(32)
# base58-like encoding using alphanumeric chars
import base64 import base64
encoded = base64.b32encode(raw).decode().rstrip("=").lower() encoded = base64.b32encode(raw).decode().rstrip("=").lower()
full_key = f"ff_key_{encoded}" full_key = f"ff_key_{encoded}"
prefix = full_key[:16] # ff_key_ + 8 chars prefix = full_key[:16]
key_hash = pwd_context.hash(full_key) key_hash = bcrypt.hashpw(full_key.encode("utf-8"), bcrypt.gensalt(rounds=10)).decode("utf-8")
return full_key, prefix, key_hash return full_key, prefix, key_hash

View file

@ -17,7 +17,7 @@ dependencies = [
"pgvector>=0.3.6", "pgvector>=0.3.6",
"redis>=5.2.0", "redis>=5.2.0",
"celery[redis]>=5.4.0", "celery[redis]>=5.4.0",
"passlib[bcrypt]>=1.7.4", "bcrypt>=4.2.0",
"python-jose[cryptography]>=3.3.0", "python-jose[cryptography]>=3.3.0",
"cryptography>=43.0.0", "cryptography>=43.0.0",
"httpx>=0.28.0", "httpx>=0.28.0",

View file

@ -2,16 +2,13 @@ FROM node:20-alpine
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package.json ./
RUN npm ci RUN npm install
COPY . . COPY . .
# Build for production (overridden in dev)
RUN npm run build
# Serve with a simple static server
RUN npm install -g serve
CMD ["serve", "-s", "dist", "-l", "5173"]
EXPOSE 5173 EXPOSE 5173
# In production: build and serve static files
# In dev: overridden to `npx vite --host 0.0.0.0`
CMD ["sh", "-c", "npm run build && npx serve -s dist -l 5173"]

View file

@ -5,7 +5,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {

View file

@ -15,7 +15,7 @@ ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
COPY package*.json ./ COPY package*.json ./
RUN npm ci RUN npm install
COPY . . COPY . .