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
# This file is automatically picked up by docker compose
version: "3.9"
# Automatically picked up by `docker compose up`
services:
api:
volumes:
- ./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:
- "8000:8000" # Direct access for debugging
- "8000:8000"
frontend:
volumes:
- ./services/frontend:/app
- /app/node_modules
command: npm run dev -- --host 0.0.0.0
command: ["npx", "vite", "--host", "0.0.0.0"]
ports:
- "5173:5173" # Vite dev server direct access
- "5173:5173"
mcp:
volumes:
- ./services/mcp:/app
ports:
- "3200:3200" # Direct MCP access
- "3200:3200"
renderer:
ports:
- "3100:3100" # Direct renderer access
- "3100:3100"
postgres:
ports:
- "5432:5432" # Direct DB access for dev tools
- "5432:5432"
redis:
ports:
- "6379:6379" # Direct Redis access for dev tools
- "6379:6379"

View file

@ -1,5 +1,3 @@
version: "3.9"
services:
# ─── Reverse Proxy ──────────────────────────────────────────
@ -7,10 +5,9 @@ services:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./services/nginx/conf:/etc/nginx/conf.d:ro
- ./services/nginx/certs:/etc/ssl/certs:ro
- renders:/renders:ro
depends_on:
api:
condition: service_healthy
@ -25,7 +22,6 @@ services:
dockerfile: Dockerfile
environment:
- VITE_API_URL=${VITE_API_URL:-http://localhost/api}
- VITE_MCP_URL=${VITE_MCP_URL:-http://localhost/mcp}
restart: unless-stopped
# ─── API (FastAPI) ──────────────────────────────────────────
@ -34,18 +30,18 @@ services:
context: ./services/api
dockerfile: Dockerfile
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-fracta}:${DB_PASS}@postgres:5432/${POSTGRES_DB:-fractafrag}
- DATABASE_URL_SYNC=postgresql://${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:-devpass}@postgres:5432/${POSTGRES_DB:-fractafrag}
- 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_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}
- TURNSTILE_SECRET=${TURNSTILE_SECRET}
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
- TURNSTILE_SECRET=${TURNSTILE_SECRET:-}
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY:-}
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET:-}
- RENDERER_URL=http://renderer:3100
- BYOK_MASTER_KEY=${BYOK_MASTER_KEY}
- BYOK_MASTER_KEY=${BYOK_MASTER_KEY:-dev-byok-key}
depends_on:
postgres:
condition: service_healthy
@ -56,6 +52,7 @@ services:
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
restart: unless-stopped
# ─── MCP Server ─────────────────────────────────────────────
@ -65,7 +62,7 @@ services:
dockerfile: Dockerfile
environment:
- 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}
depends_on:
api:
@ -80,7 +77,7 @@ services:
shm_size: "512mb"
environment:
- MAX_RENDER_DURATION=${MAX_RENDER_DURATION:-8}
- OUTPUT_DIR=${RENDER_OUTPUT_DIR:-/renders}
- OUTPUT_DIR=/renders
volumes:
- renders:/renders
restart: unless-stopped
@ -90,22 +87,20 @@ services:
build:
context: ./services/api
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:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-fracta}:${DB_PASS}@postgres:5432/${POSTGRES_DB:-fractafrag}
- DATABASE_URL_SYNC=postgresql://${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:-devpass}@postgres:5432/${POSTGRES_DB:-fractafrag}
- REDIS_URL=${REDIS_URL:-redis://redis:6379/0}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
- RENDERER_URL=http://renderer:3100
- BYOK_MASTER_KEY=${BYOK_MASTER_KEY}
- BYOK_MASTER_KEY=${BYOK_MASTER_KEY:-dev-byok-key}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
renderer:
condition: service_started
restart: unless-stopped
# ─── PostgreSQL + pgvector ──────────────────────────────────
@ -113,7 +108,7 @@ services:
image: pgvector/pgvector:pg16
environment:
- POSTGRES_USER=${POSTGRES_USER:-fracta}
- POSTGRES_PASSWORD=${DB_PASS}
- POSTGRES_PASSWORD=${DB_PASS:-devpass}
- POSTGRES_DB=${POSTGRES_DB:-fractafrag}
volumes:
- pgdata:/var/lib/postgresql/data

View file

@ -2,7 +2,7 @@ FROM python:3.12-slim
WORKDIR /app
# Install system deps
# Install system deps (curl for healthcheck)
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
build-essential \
@ -10,12 +10,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# Install Python deps
COPY pyproject.toml .
RUN pip install --no-cache-dir -e ".[dev]"
RUN pip install --no-cache-dir ".[dev]"
# Copy app code
COPY . .
EXPOSE 8000
# Default command (overridden in dev by docker-compose.override.yml)
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.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import jwt, JWTError
from passlib.context import CryptContext
import bcrypt
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
@ -17,18 +17,17 @@ from app.models import User
from app.redis import get_redis
settings = get_settings()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
bearer_scheme = HTTPBearer(auto_error=False)
# ── Password Hashing ──────────────────────────────────────
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:
return pwd_context.verify(plain, hashed)
return bcrypt.checkpw(plain.encode("utf-8"), hashed.encode("utf-8"))
# ── 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)
event_type = Column(String, nullable=False)
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)

View file

@ -80,7 +80,7 @@ async def report_dwell(
shader_id=body.shader_id,
event_type="dwell",
dwell_secs=body.dwell_secs,
metadata={"replayed": body.replayed},
event_metadata={"replayed": body.replayed},
)
db.add(event)
# 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 sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from passlib.context import CryptContext
import bcrypt
from app.database import get_db
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
router = APIRouter()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def generate_api_key() -> tuple[str, str, str]:
"""Generate an API key. Returns (full_key, prefix, hash)."""
raw = secrets.token_bytes(32)
# base58-like encoding using alphanumeric chars
import base64
encoded = base64.b32encode(raw).decode().rstrip("=").lower()
full_key = f"ff_key_{encoded}"
prefix = full_key[:16] # ff_key_ + 8 chars
key_hash = pwd_context.hash(full_key)
prefix = full_key[:16]
key_hash = bcrypt.hashpw(full_key.encode("utf-8"), bcrypt.gensalt(rounds=10)).decode("utf-8")
return full_key, prefix, key_hash

View file

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

View file

@ -2,16 +2,13 @@ FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY package.json ./
RUN npm install
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
# 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",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {

View file

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