Skip to main content

7.16 Deployment

# deploy/docker-compose.yml (pin every image by digest in production)
x-app: &app
build: { context: .., dockerfile: deploy/Dockerfile.server }
env_file: ../.env
environment: { LOCO_ENV: production }
volumes: ["../config:/app/config:ro"] # YAML is mounted, never baked into the image

services:
postgres:
image: pgvector/pgvector:pg17 # PostgreSQL License
environment: { POSTGRES_PASSWORD: "${PG_PASSWORD}", POSTGRES_DB: bo }
volumes: [pgdata:/var/lib/postgresql/data]
vllm-chat:
image: vllm/vllm-openai:${VLLM_TAG} # Apache-2.0; Qwen3.8 may need a recent build (VERIFY)
command: ["--model", "Qwen/Qwen3.8-27B-FP8", "--max-model-len", "65536", "--enable-auto-tool-choice",
"--tool-call-parser", "qwen3_coder", "--reasoning-parser", "qwen3", "--api-key", "${VLLM_API_KEY}"]
deploy: { resources: { reservations: { devices: [{ capabilities: [gpu] }] } } }
volumes: [hf:/root/.cache/huggingface]
vllm-embed:
image: vllm/vllm-openai:${VLLM_TAG}
command: ["--model", "Qwen/Qwen3-Embedding-0.6B", "--task", "embed", "--port", "8001", "--api-key", "${VLLM_API_KEY}"] # VERIFY --task flag
volumes: [hf:/root/.cache/huggingface]
docling:
image: quay.io/docling-project/docling-serve:${DOCLING_TAG} # MIT
environment: { DOCLING_SERVE_API_KEY: "${DOCLING_API_KEY}" }
otel-collector:
image: otel/opentelemetry-collector-contrib:${OTELCOL_TAG} # Apache-2.0
volumes: ["./otel-collector.yaml:/etc/otelcol-contrib/config.yaml:ro"]
jaeger:
image: jaegertracing/jaeger:${JAEGER_TAG} # Apache-2.0 (VERIFY)
ports: ["16686:16686"]

migrate:
<<: *app
command: ["db", "migrate"]
depends_on: [postgres]
server:
<<: *app
command: ["start"]
depends_on: [migrate, vllm-chat, vllm-embed, docling, otel-collector]
worker:
<<: *app
command: ["start", "--worker"]
depends_on: [migrate]
scheduler:
<<: *app
command: ["scheduler"]
depends_on: [migrate]
web:
build: { context: ../web, dockerfile: ../deploy/Dockerfile.web }
environment: { BO_AGENT_URL: "http://server:8080" }
ports: ["3000:3000"]
depends_on: [server]

# --- optional profiles; the core does not need these ---
valkey:
image: valkey/valkey:8 # BSD-3-Clause
profiles: [llmobs, ap]
seaweedfs:
image: chrislusf/seaweedfs:${SEAWEED_TAG} # Apache-2.0 core; never enable enterprise features
command: "server -s3 -dir=/data"
profiles: [llmobs]
volumes: [seaweed:/data]
# Opik or Langfuse OSS: add from the upstream compose file, pointing Redis at valkey and S3 at seaweedfs (VERIFY env names)

volumes: { pgdata: {}, hf: {}, seaweed: {} }
# deploy/otel-collector.yaml
receivers:
otlp: { protocols: { grpc: { endpoint: 0.0.0.0:4317 } } }
processors:
batch: {}
exporters:
otlp/jaeger: { endpoint: jaeger:4317, tls: { insecure: true } }
service:
pipelines:
traces: { receivers: [otlp], processors: [batch], exporters: [otlp/jaeger] }
# .env.example — secrets and URLs only; every value is read in YAML with get_env
LOCO_ENV=production
DATABASE_URL=postgres://postgres:change-me@postgres:5432/bo
PG_PASSWORD=change-me
JWT_SECRET=change-me
VLLM_CHAT_URL=http://vllm-chat:8000
VLLM_EMBED_URL=http://vllm-embed:8001
VLLM_API_KEY=change-me
DOCLING_URL=http://docling:5001
DOCLING_API_KEY=change-me
OTLP_ENDPOINT=http://otel-collector:4317
ACCOUNTING_URL=
ACCOUNTING_API_KEY=
# deploy/Dockerfile.server
FROM rust:1.97-bookworm AS build
WORKDIR /src
COPY . .
RUN cargo build --release -p bo-app --locked
FROM gcr.io/distroless/cc-debian12:nonroot
WORKDIR /app
COPY --from=build /src/target/release/bo-app /app/bo-app
USER nonroot
ENTRYPOINT ["/app/bo-app"]
# deploy/Dockerfile.web
FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci && npm run license:check
COPY . .
RUN npm run build
FROM node:22-bookworm-slim
WORKDIR /app
COPY --from=build /app ./
USER node
CMD ["npm", "start"]
# justfile
up: docker compose -f deploy/docker-compose.yml up -d
dev: LOCO_ENV=development cargo run -p bo-app -- start --server-and-worker
check: cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test
validate: LOCO_ENV=test cargo run -p bo-app -- task config_validate
schemas: cargo run -p bo-app -- task schema_export
licenses: cargo deny check licenses bans advisories sources && (cd web && npm run license:check)
images: for i in $(docker compose -f deploy/docker-compose.yml config --images); do syft $i -o spdx-json > sbom-$(echo $i | tr '/:' '__').json; grant check $i; done
eval: cargo test -p bo-app --test evals -- --ignored
# .github/workflows/ci.yml
name: ci
on: [push, pull_request]
jobs:
license-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
with: { command: check licenses bans advisories sources }
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: cd web && npm ci && npm run license:check
rust:
needs: license-gate
runs-on: ubuntu-latest
services:
postgres:
image: pgvector/pgvector:pg17
env: { POSTGRES_PASSWORD: test }
ports: ["5432:5432"]
env:
LOCO_ENV: test
DATABASE_URL: postgres://postgres:test@localhost:5432/postgres
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with: { components: "rustfmt, clippy" }
- run: cargo fmt --all --check
- run: cargo clippy --all-targets --all-features -- -D warnings
- run: cargo run -p bo-app -- task config_validate # every YAML definition must load and compile
- run: cargo test --all
web:
needs: license-gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: cd web && npm ci && npm run build

In Kubernetes, config/ becomes a ConfigMap. Changing a workflow is a ConfigMap change and a rolling restart.