Skip to content
Galley

Stacks / Python

Preview environments for Python.

FastAPI or Flask with Postgres and a Celery worker.

  • Python 3.12
  • Postgres 16
  • Redis 7

Python previews are straightforward for web-only apps and get interesting the moment you add background workers, ML dependencies, or native extensions. Galley handles the first case out of the box. The second takes one Dockerfile trick.

The config

version: 1

services:
  api:
    kind: api
    build:
      path: ./
    expose: 8000
    depends_on: [postgres, cache]
    env:
      DATABASE_URL: postgres://app:pw@postgres:5432/app
      REDIS_URL: redis://cache:6379/0
    health:
      path: /healthz
      status: 200

  worker:
    kind: worker
    build:
      path: ./
      start: "celery -A app.tasks worker -l info"
    depends_on: [postgres, cache]
    env:
      DATABASE_URL: postgres://app:pw@postgres:5432/app
      REDIS_URL: redis://cache:6379/0

  postgres:
    kind: database
    image: postgres:16-alpine
    expose: 5432
    env:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: pw
      POSTGRES_DB: app

  cache:
    kind: cache
    image: redis:7-alpine
    expose: 6379

The worker reuses the same build (build.path: ./) but overrides the start command. kind: worker skips the public route — it shouldn’t be reachable from outside the env.

Migrations: run them on api start (alembic upgrade head && uvicorn ...) or as a one-shot kind: worker with restart: never.

The usual gotcha

Python base images with ML deps get enormous fast. numpy, torch, transformers push a base image past three gigabytes. Preview builds pay this cost every time unless the Dockerfile is structured correctly.

The fix:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Copying requirements.txt (or pyproject.toml + uv.lock, or poetry.lock) before the rest of the source keeps the install layer stable across source changes — only the source-copy layer rebuilds when you push a commit.

For Celery: run the worker as its own service with its own start command. Shipping web + worker in one container via a process manager works, but it hides worker crashes from the dashboard’s per-service state and per-service logs.