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_healthyensures 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):
- Install Docker.
cp .env.example .env.local(fill values; do not commit).- 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):
- Install Docker, kubectl, minikube.
cp .env.example .env.local- 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.
コメントを残す