GitHub-first, one-shot setup

Here’s a GitHub-first, one-shot setup that includes Postgres + Odoo + ChromaDB + portal-api via Docker Compose for local development, plus the Minikube/Kubernetes path and CI smoke tests.


0) Repository layout (suggested)

.
├─ app/                         # your FastAPI source (portal-api)
├─ docker/
│  └─ portal-api/Dockerfile
├─ k8s/                         # Kubernetes (minikube/cluster)
│  ├─ chroma.yaml
│  └─ portal-api.yaml
├─ scripts/
│  ├─ dev-up.sh                 # one-shot local (Compose)
│  ├─ dev-down.sh               # clean local stack
│  └─ bootstrap-portal-dev.sh   # one-shot minikube
├─ .github/workflows/
│  └─ smoke.yml                 # PR smoke on minikube
├─ requirements.txt
├─ constraints.txt
├─ compose.yaml                 # local stack (Postgres+Odoo+Chroma+portal-api)
├─ .env.example                 # do NOT commit secrets; copy to .env.local
└─ .gitignore

1) Pinned dependencies (avoid SQLAlchemy / NumPy breakage)

requirements.txt

fastapi==0.110.0
uvicorn==0.29.0
SQLAlchemy==1.4.54
psycopg2-binary==2.9.9
# add your libs here…

constraints.txt

SQLAlchemy==1.4.54
numpy<2

docker/portal-api/Dockerfile

FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt constraints.txt ./
RUN pip install --no-cache-dir -r requirements.txt -c constraints.txt
COPY app/ /app/app/
EXPOSE 8000
CMD ["uvicorn","app.main:app","--host","0.0.0.0","--port","8000"]

Keep your DB code on SQLAlchemy Core when building dynamic filters. Avoid if clause: on ClauseElements.


2) Local env files (never commit secrets)

.env.example (commit this)

# ---------- Postgres for portal-api ----------
# Compose service name is "postgres"
DATABASE_URL=postgresql+psycopg2://portal:portal@postgres:5432/portal

# ---------- Odoo ----------
ODOO_URL=http://odoo:8069
ODOO_DB=odoo
ODOO_USER=admin
ODOO_PASSWORD=admin

# ---------- ChromaDB ----------
CHROMA_URL=http://chroma:8000

# ---------- OpenAI ----------
OPENAI_API_KEY=sk-... # or "dummy"

# ---------- Local host ports ----------
PORTAL_LOCAL_PORT=18080
ODOO_LOCAL_PORT=8069
CHROMA_LOCAL_PORT=8000
POSTGRES_LOCAL_PORT=5432

.gitignore (commit this)

.env
.env.local
.env.*.local
.env.development
.env.production
.env.dev
.env.prod

3) One-shot local stack (Docker Compose)

compose.yaml

version: "3.9"

x-health-interval: &hi { interval: 5s, timeout: 3s, retries: 20, start_period: 10s }

services:
  postgres:
    image: postgres:16
    container_name: portal-postgres
    environment:
      POSTGRES_DB: portal
      POSTGRES_USER: portal
      POSTGRES_PASSWORD: portal
    ports:
      - "${POSTGRES_LOCAL_PORT:-5432}:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
      <<: *hi

  odoo:
    image: odoo:16
    container_name: portal-odoo
    depends_on:
      postgres: { condition: service_healthy }
    environment:
      # Odoo official image understands these db flags via options; we pass them in command.
      # Admin password is required at first boot for database creation.
      ADMIN_PASSWORD: "${ODOO_PASSWORD:-admin}"
    command: >
      odoo
      --db_host=postgres --db_port=5432
      --db_user=portal  --db_password=portal
      --proxy-mode
    ports:
      - "${ODOO_LOCAL_PORT:-8069}:8069"
    volumes:
      - odoo-data:/var/lib/odoo
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8069/web/health || exit 1"]
      <<: *hi

  chroma:
    image: ghcr.io/chroma-core/chroma:0.4.24
    container_name: portal-chroma
    environment:
      PERSIST_DIRECTORY: /chroma
      CHROMA_SERVER_HOST: 0.0.0.0
      CHROMA_SERVER_HTTP_PORT: "8000"
    ports:
      - "${CHROMA_LOCAL_PORT:-8000}:8000"
    volumes:
      - chroma-data:/chroma
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8000/api/v1/heartbeat | grep -q ok"]
      <<: *hi

  portal-api:
    build:
      context: .
      dockerfile: docker/portal-api/Dockerfile
    container_name: portal-api
    depends_on:
      postgres: { condition: service_healthy }
      chroma:   { condition: service_healthy }
    environment:
      DATABASE_URL: "${DATABASE_URL}"
      CHROMA_URL:   "${CHROMA_URL}"
      OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
      ODOO_URL: "${ODOO_URL:-http://odoo:8069}"
      ODOO_DB:  "${ODOO_DB:-odoo}"
      ODOO_USER: "${ODOO_USER:-admin}"
      ODOO_PASSWORD: "${ODOO_PASSWORD:-admin}"
    ports:
      - "${PORTAL_LOCAL_PORT:-18080}:8000"
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8000/healthz || exit 1"]
      <<: *hi

volumes:
  pgdata:
  odoo-data:
  chroma-data:

Notes:

  • depends_on.condition: service_healthy ensures portal-api only starts after Postgres & Chroma pass health checks.
  • Odoo can be accessed at http://localhost:${ODOO_LOCAL_PORT} (defaults to 8069).

4) Local one-shot scripts

scripts/dev-up.sh

#!/usr/bin/env bash
set -euo pipefail

ENV_FILE="${ENV_FILE:-.env.local}"

if [ ! -f "$ENV_FILE" ]; then
  echo "Missing $ENV_FILE. Copy .env.example -> .env.local and fill values."
  exit 2
fi

echo "== Building & starting local stack (Postgres + Odoo + Chroma + portal-api) =="
docker compose --env-file "$ENV_FILE" up -d --build

echo "== Waiting for portal-api to pass healthz =="
for i in $(seq 1 60); do
  if curl -fsS "http://127.0.0.1:${PORTAL_LOCAL_PORT:-18080}/healthz" >/dev/null 2>&1; then
    echo "OK"; break
  fi
  sleep 2
  if [ "$i" -eq 60 ]; then echo "portal-api healthz timeout"; exit 1; fi
done

echo "== Chroma heartbeat =="
curl -fsS "http://127.0.0.1:${CHROMA_LOCAL_PORT:-8000}/api/v1/heartbeat"

echo "== /chroma/upsert dry-run (should return processed/upserted/skipped/failures) =="
curl -fsS -X POST "http://127.0.0.1:${PORTAL_LOCAL_PORT:-18080}/chroma/upsert" \
  -H 'Content-Type: application/json' \
  -d '{"limit":10,"dry_run":true}'

echo
echo "== DONE: Local dev is ready =="
echo "portal-api  : http://127.0.0.1:${PORTAL_LOCAL_PORT:-18080}"
echo "ChromaDB    : http://127.0.0.1:${CHROMA_LOCAL_PORT:-8000}"
echo "Odoo        : http://127.0.0.1:${ODOO_LOCAL_PORT:-8069}"
echo "Postgres    : localhost:${POSTGRES_LOCAL_PORT:-5432} (user/pass: portal/portal, db=portal)"

scripts/dev-down.sh

#!/usr/bin/env bash
set -euo pipefail
docker compose down -v

Usage:

cp .env.example .env.local
# edit values if needed
chmod +x scripts/dev-*.sh
./scripts/dev-up.sh
# ... later:
./scripts/dev-down.sh

5) Minikube / Kubernetes (optional local cluster) — one-shot

(If you also want the K8s route locally, keep these.)

k8s/chroma.yaml and k8s/portal-api.yaml (as given earlier) deploy Chroma and portal-api in namespace portal-dev.
One-shot script:

scripts/bootstrap-portal-dev.sh

#!/usr/bin/env bash
set -euo pipefail

NS=portal-dev
API_IMAGE=portal-api:dev
LOCAL_ENV="${LOCAL_ENV:-.env.local}"

need() { command -v "$1" >/dev/null 2>&1 || { echo "Missing: $1"; exit 2; }; }

echo "== preflight =="; need docker; need kubectl; need minikube

echo "== start minikube (if not running) =="
if ! minikube status >/dev/null 2>&1; then
  minikube start --kubernetes-version=v1.30.0 --driver=docker
fi
kubectl get ns "$NS" >/dev/null 2>&1 || kubectl create ns "$NS"

echo "== build portal-api image & load to minikube =="
docker build -t "$API_IMAGE" -f docker/portal-api/Dockerfile .
minikube image load "$API_IMAGE"

echo "== create/update secrets from ${LOCAL_ENV} =="
[ -f "$LOCAL_ENV" ] || { echo "Missing $LOCAL_ENV"; exit 2; }
set -a; . "$LOCAL_ENV"; set +a

kubectl -n "$NS" delete secret portal-secrets --ignore-not-found
kubectl -n "$NS" create secret generic portal-secrets \
  --from-literal=DATABASE_URL="${DATABASE_URL:-}" \
  --from-literal=OPENAI_API_KEY="${OPENAI_API_KEY:-}" \
  --from-literal=ODOO_URL="${ODOO_URL:-}" \
  --from-literal=ODOO_DB="${ODOO_DB:-}" \
  --from-literal=ODOO_USER="${ODOO_USER:-}" \
  --from-literal=ODOO_PASSWORD="${ODOO_PASSWORD:-}"

echo "== apply manifests =="
kubectl -n "$NS" apply -f k8s/chroma.yaml
kubectl -n "$NS" apply -f k8s/portal-api.yaml

echo "== wait for rollouts =="
kubectl -n "$NS" rollout status sts/chroma --timeout=300s
kubectl -n "$NS" rollout status deploy/portal-api --timeout=300s

echo "== smoke (chroma & portal-api) =="
kubectl -n "$NS" run c1 --rm --restart=Never \
  --image=curlimages/curl:8.10.1 -- \
  curl -fsS http://chroma:8000/api/v1/heartbeat

kubectl -n "$NS" port-forward svc/portal-api ${PORTAL_LOCAL_PORT:-18080}:80 >/tmp/pf.log 2>&1 &
PF=$!; sleep 5; trap "kill $PF || true" EXIT
curl -fsS http://127.0.0.1:${PORTAL_LOCAL_PORT:-18080}/healthz
curl -fsS -X POST http://127.0.0.1:${PORTAL_LOCAL_PORT:-18080}/chroma/upsert \
  -H 'Content-Type: application/json' -d '{"limit":10,"dry_run":true}'

echo "== DONE: portal-dev (minikube) is ready =="

6) PR smoke on GitHub Actions (minikube)

.github/workflows/smoke.yml

name: pr-smoke
on: { pull_request: {} }

jobs:
  smoke:
    runs-on: ubuntu-22.04
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
      - uses: azure/setup-kubectl@v4
        with: { version: "v1.30.5" }
      - uses: medyagh/setup-minikube@v0.0.18
        with:
          minikube-version: "v1.33.1"
          kubernetes-version: "v1.30.0"
          driver: docker

      - name: Namespace
        run: kubectl create ns portal-dev || true

      - name: Build API & load to minikube
        run: |
          docker build -t portal-api:dev -f docker/portal-api/Dockerfile .
          minikube image load portal-api:dev

      - name: Create in-cluster secrets (from GH secrets)
        run: |
          kubectl -n portal-dev delete secret portal-secrets --ignore-not-found
          kubectl -n portal-dev create secret generic portal-secrets \
            --from-literal=DATABASE_URL="${{ secrets.DATABASE_URL_CI }}" \
            --from-literal=OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY }}" \
            --from-literal=ODOO_URL="${{ secrets.ODOO_URL }}" \
            --from-literal=ODOO_DB="${{ secrets.ODOO_DB }}" \
            --from-literal=ODOO_USER="${{ secrets.ODOO_USER }}" \
            --from-literal=ODOO_PASSWORD="${{ secrets.ODOO_PASSWORD }}"

      - name: Apply k8s
        run: |
          kubectl -n portal-dev apply -f k8s/chroma.yaml
          kubectl -n portal-dev apply -f k8s/portal-api.yaml
          kubectl -n portal-dev rollout status sts/chroma --timeout=300s
          kubectl -n portal-dev rollout status deploy/portal-api --timeout=300s

      - name: Smoke
        run: |
          kubectl -n portal-dev run c1 --rm --restart=Never \
            --image=curlimages/curl:8.10.1 -- \
            curl -fsS http://chroma:8000/api/v1/heartbeat
          kubectl -n portal-dev port-forward svc/portal-api 18080:80 >/tmp/pf.log 2>&1 &
          PF=$!; sleep 5; trap "kill $PF || true" EXIT
          curl -fsS http://127.0.0.1:18080/healthz
          curl -fsS -X POST http://127.0.0.1:18080/chroma/upsert \
            -H 'Content-Type: application/json' -d '{"limit":10,"dry_run":true}'

Add these Actions secrets:
DATABASE_URL_CI, OPENAI_API_KEY, ODOO_URL, ODOO_DB, ODOO_USER, ODOO_PASSWORD.


7) “One-shot perfection” message (share with offshore team)

Task: Make the one-shot dev environment perfect.

How to run (local Compose):

  1. Install Docker.
  2. cp .env.example .env.local (fill values; do not commit).
  3. Run ./scripts/dev-up.sh.

The script will build the app, start Postgres + Odoo + Chroma + portal-api, wait for health, call Chroma heartbeat, and dry-run /chroma/upsert.

Kubernetes route (optional):

  1. Install Docker, kubectl, minikube.
  2. cp .env.example .env.local
  3. Run ./scripts/bootstrap-portal-dev.sh.

If anything fails, please paste the terminal transcript. We’ll fix the scripts/manifests so it stays truly one-shot.


Notes & tips

  • Odoo may require creating/selecting the database on first login (http://localhost:8069). The portal-api doesn’t need Odoo to be fully initialized for its own health checks; it just needs the URL and credentials when endpoints touch Odoo.
  • If port conflicts occur, change the *_LOCAL_PORT variables in .env.local.
  • For real clusters, keep secrets out of git: use GitHub Environment Secrets → render to K8s Secrets during deploy.
  • If you want Codespaces/Dev Containers, we can add a .devcontainer/ that boots the same stack with Docker in Docker.

Comments

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です