Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,45 @@ STUDIES_DEFAULT_TIMEOUT_S=60
#
# Single-proxy setup (most common Artifactory layout):
# BASE_REGISTRY=artifactory.example.com/
# UV_REGISTRY=artifactory.example.com/
# GHCR_REGISTRY=artifactory.example.com/
#
# Separate proxies per upstream:
# BASE_REGISTRY=docker.proxy.corp/
# UV_REGISTRY=ghcr.proxy.corp/
# GHCR_REGISTRY=ghcr.proxy.corp/
#
# Empty / unset → unchanged behavior (Docker Hub + ghcr.io/astral-sh/uv).
# Defaults live in docker-compose.yml's `build.args` block.
# BASE_REGISTRY=
# UV_REGISTRY=ghcr.io/
# GHCR_REGISTRY=ghcr.io/

# --- Corporate HTTP proxy (apt / PyPI / npm + runtime egress) ----------
# Routes outbound HTTP through a corporate proxy. Honored at BOTH:
# - build time — apt-get, uv sync, npm/pnpm install RUN steps
# - run time — FastAPI → OpenAI, Arq → GitHub, ES/OpenSearch/Solr HTTP
#
# Empty / unset → no proxy (unchanged behavior).
#
# IMPORTANT — `no_proxy` MUST include the Compose service names. Without
# them, the worker's HTTP call to `http://elasticsearch:9200` (and similar
# in-network calls) gets routed to the corporate proxy, which has no path
# to those Compose-internal hostnames. The default value below bakes them
# in; if you set `no_proxy` manually, include
# `postgres,redis,elasticsearch,opensearch,solr,api,worker,migrate`.
#
# Common gotchas:
# - `169.254.169.254` is the EC2/cloud metadata service — always include
# so cloud-instance metadata lookups skip the proxy.
# - `10.0.0.0/8` covers internal VPC traffic.
# - `host.docker.internal` — local-LLM dev (Ollama / LM Studio / vLLM via
# `OPENAI_BASE_URL=http://host.docker.internal:…`) needs this exempted
# or the proxy intercepts the local-machine call.
#
# Both case variants (http_proxy + HTTP_PROXY etc.) are written by Compose
# build.args and the Dockerfile ENV block — Linux tooling is split on
# which convention it reads.
# http_proxy=http://http.proxy.your-corp.com:8000
# https_proxy=http://http.proxy.your-corp.com:8000
# no_proxy=your-corp.com,.your-corp-cloud.com,localhost,127.0.0.1,10.0.0.0/8,169.254.169.254,host.docker.internal,postgres,redis,elasticsearch,opensearch,solr,api,worker,migrate

# --- Build-time only --------------------------------------------------
# RELYLOOP_GIT_SHA is injected at `docker buildx build` via --build-arg.
Expand Down
38 changes: 33 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,47 @@
# # Single proxy fronting both Docker Hub and GHCR (typical Artifactory):
# docker build \
# --build-arg BASE_REGISTRY=artifactory.example.com/ \
# --build-arg UV_REGISTRY=artifactory.example.com/ \
# --build-arg GHCR_REGISTRY=artifactory.example.com/ \
# -t relyloop/api:dev .
#
# # Separate proxies per upstream:
# docker build \
# --build-arg BASE_REGISTRY=docker.proxy.corp/ \
# --build-arg UV_REGISTRY=ghcr.proxy.corp/ \
# --build-arg GHCR_REGISTRY=ghcr.proxy.corp/ \
# -t relyloop/api:dev .
#
# Trailing slash is REQUIRED on non-empty values — the FROM/COPY lines
# concatenate `${BASE_REGISTRY}python:…` without a separator.
ARG BASE_REGISTRY=
ARG UV_REGISTRY=ghcr.io/
ARG GHCR_REGISTRY=ghcr.io/

# ---------------------------------------------------------------------------
# Corporate HTTP proxy — handled via BuildKit predefined ARGs + Compose
# `environment:`, NOT via Dockerfile ARG/ENV.
# ---------------------------------------------------------------------------
# Docker treats `http_proxy`, `https_proxy`, `no_proxy`, and their UPPERCASE
# siblings as **predefined ARGs**: BuildKit forwards them from `--build-arg`
# into every RUN step's environment automatically — no `ARG` declaration
# needed — and intentionally excludes them from `docker history` so the proxy
# URL never gets baked into the image. The `build.args:` block in
# `docker-compose.yml` is wired through; runtime egress is handled
# separately via each service's `environment:` block (also in
# docker-compose.yml). Override via `.env` or shell:
#
# http_proxy=http://http.proxy.your-corp.com:8000
# https_proxy=http://http.proxy.your-corp.com:8000
# no_proxy=your-corp.com,.your-corp-cloud.com,localhost,127.0.0.1,
# 10.0.0.0/8,169.254.169.254,host.docker.internal,
# postgres,redis,elasticsearch,opensearch,solr,api,worker,migrate
Comment on lines +58 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add host.docker.internal to the documented no_proxy example in the Dockerfile comments to keep it in sync with .env.example and support local LLM development behind a proxy.

#   no_proxy=your-corp.com,.your-corp-cloud.com,localhost,127.0.0.1,
#            10.0.0.0/8,169.254.169.254,host.docker.internal,
#            postgres,redis,elasticsearch,opensearch,solr,api,worker,migrate

#
# IMPORTANT — `no_proxy` MUST include the Compose service names
# (postgres / redis / elasticsearch / opensearch / solr / api / worker /
# migrate) AND `host.docker.internal`. Without the Compose names, the
# worker's call to `http://elasticsearch:9200` (and similar in-network HTTP)
# gets routed to the corporate proxy, which has no path to those
# Compose-internal hostnames. Without `host.docker.internal`, local-LLM dev
# (Ollama / LM Studio / vLLM via `OPENAI_BASE_URL=http://host.docker.internal:…`)
# breaks. The recommended `.env.example` value bakes both in.

# ---------------------------------------------------------------------------
# Stage 1 — base: Python + uv + system deps for healthcheck (curl)
Expand All @@ -55,11 +83,11 @@ ARG UV_REGISTRY=ghcr.io/

# Alias the upstream uv image as a named stage so the COPY --from= below can
# reference it by stage name. Going through an aliased FROM (where ARG
# substitution is fully supported) instead of `COPY --from=${UV_REGISTRY}…`
# substitution is fully supported) instead of `COPY --from=${GHCR_REGISTRY}…`
# (where buildx's parser treats the ARG literally and rejects the reference
# as "invalid reference format") is the canonical workaround. Scorecard
# still credits the inline digest pin on this FROM line.
FROM ${UV_REGISTRY}astral-sh/uv:0.5.7@sha256:23272999edd22e78195509ea3fe380e7632ab39a4c69a340bedaba7555abe20a AS uv-source
FROM ${GHCR_REGISTRY}astral-sh/uv:0.5.7@sha256:23272999edd22e78195509ea3fe380e7632ab39a4c69a340bedaba7555abe20a AS uv-source

FROM ${BASE_REGISTRY}python:3.14-slim@sha256:c845af9399020c7e562969a13689e929074a10fd057acd1b1fad06a2fb068e97 AS base

Expand Down
74 changes: 71 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,17 @@ services:
# env vars in `.env` or inline (`BASE_REGISTRY=artifactory.example.com/ make up`)
# to route base-image pulls through a proxy. See top of Dockerfile.
BASE_REGISTRY: ${BASE_REGISTRY:-}
UV_REGISTRY: ${UV_REGISTRY:-ghcr.io/}
GHCR_REGISTRY: ${GHCR_REGISTRY:-ghcr.io/}
# Corporate HTTP proxy — empty defaults preserve current behavior.
# These are BuildKit predefined ARGs (no Dockerfile ARG declaration
# needed) and are forwarded into RUN steps automatically. Runtime
# egress is set separately via each service's `environment:` block
# so the proxy URL never gets baked into the image. `no_proxy` MUST
# include the Compose service names + `host.docker.internal` (see
# .env.example).
http_proxy: ${http_proxy:-}
https_proxy: ${https_proxy:-}
no_proxy: ${no_proxy:-}
Comment on lines +77 to +79

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Passing these as build.args is correct for build-time proxying (e.g., apt-get and uv sync). However, to support runtime proxying (especially when using pre-built images or when running without rebuilding), these variables must also be declared in the environment section of the services.

Add them to the environment section of migrate, api, worker, and ui services:

    environment:
      - http_proxy
      - https_proxy
      - no_proxy
      - HTTP_PROXY
      - HTTPS_PROXY
      - NO_PROXY

command:
- sh
- -c
Expand All @@ -77,6 +87,14 @@ services:
environment:
DATABASE_URL_FILE: /run/secrets/database_url
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
# Corporate HTTP proxy (runtime egress). Both case variants because
# Linux tooling is split. Empty default → no proxy.
http_proxy: ${http_proxy:-}
https_proxy: ${https_proxy:-}
no_proxy: ${no_proxy:-}
HTTP_PROXY: ${http_proxy:-}
HTTPS_PROXY: ${https_proxy:-}
NO_PROXY: ${no_proxy:-}
secrets:
- postgres_password
- database_url
Expand All @@ -95,7 +113,17 @@ services:
args:
RELYLOOP_GIT_SHA: ${RELYLOOP_GIT_SHA:-dev}
BASE_REGISTRY: ${BASE_REGISTRY:-}
UV_REGISTRY: ${UV_REGISTRY:-ghcr.io/}
GHCR_REGISTRY: ${GHCR_REGISTRY:-ghcr.io/}
# Corporate HTTP proxy — empty defaults preserve current behavior.
# These are BuildKit predefined ARGs (no Dockerfile ARG declaration
# needed) and are forwarded into RUN steps automatically. Runtime
# egress is set separately via each service's `environment:` block
# so the proxy URL never gets baked into the image. `no_proxy` MUST
# include the Compose service names + `host.docker.internal` (see
# .env.example).
http_proxy: ${http_proxy:-}
https_proxy: ${https_proxy:-}
no_proxy: ${no_proxy:-}
depends_on:
postgres:
condition: service_healthy
Expand All @@ -113,6 +141,14 @@ services:
OPENAI_MODEL_CHAT: ${OPENAI_MODEL_CHAT:-gpt-4o-mini-2024-07-18}
CLUSTER_CREDENTIALS_FILE: /run/secrets/cluster_credentials
RELYLOOP_GIT_SHA: ${RELYLOOP_GIT_SHA:-dev}
# Corporate HTTP proxy (runtime egress — OpenAI, GitHub, registered
# cluster HTTP). Empty default → no proxy. Both case variants.
http_proxy: ${http_proxy:-}
https_proxy: ${https_proxy:-}
no_proxy: ${no_proxy:-}
HTTP_PROXY: ${http_proxy:-}
HTTPS_PROXY: ${https_proxy:-}
NO_PROXY: ${no_proxy:-}
ports:
- "127.0.0.1:8000:8000"
secrets:
Expand Down Expand Up @@ -156,7 +192,17 @@ services:
args:
RELYLOOP_GIT_SHA: ${RELYLOOP_GIT_SHA:-dev}
BASE_REGISTRY: ${BASE_REGISTRY:-}
UV_REGISTRY: ${UV_REGISTRY:-ghcr.io/}
GHCR_REGISTRY: ${GHCR_REGISTRY:-ghcr.io/}
# Corporate HTTP proxy — empty defaults preserve current behavior.
# These are BuildKit predefined ARGs (no Dockerfile ARG declaration
# needed) and are forwarded into RUN steps automatically. Runtime
# egress is set separately via each service's `environment:` block
# so the proxy URL never gets baked into the image. `no_proxy` MUST
# include the Compose service names + `host.docker.internal` (see
# .env.example).
http_proxy: ${http_proxy:-}
https_proxy: ${https_proxy:-}
no_proxy: ${no_proxy:-}
command: ["arq", "backend.workers.all.WorkerSettings"]
depends_on:
postgres:
Expand All @@ -176,6 +222,13 @@ services:
OPENAI_MODEL: ${OPENAI_MODEL:-gpt-4o-2024-08-06}
OPENAI_MODEL_CHAT: ${OPENAI_MODEL_CHAT:-gpt-4o-mini-2024-07-18}
CLUSTER_CREDENTIALS_FILE: /run/secrets/cluster_credentials
# Corporate HTTP proxy (runtime egress — same shape as api).
http_proxy: ${http_proxy:-}
https_proxy: ${https_proxy:-}
no_proxy: ${no_proxy:-}
HTTP_PROXY: ${http_proxy:-}
HTTPS_PROXY: ${https_proxy:-}
NO_PROXY: ${no_proxy:-}
secrets:
- postgres_password
- database_url
Expand Down Expand Up @@ -208,11 +261,26 @@ services:
BASE_REGISTRY: ${BASE_REGISTRY:-}
# Forwarded for the OCI `org.opencontainers.image.revision` label.
RELYLOOP_GIT_SHA: ${RELYLOOP_GIT_SHA:-dev}
# Corporate HTTP proxy — same shape as the backend services.
# BuildKit predefined ARGs (no Dockerfile ARG/ENV); runtime is set
# via the `environment:` block below.
http_proxy: ${http_proxy:-}
https_proxy: ${https_proxy:-}
no_proxy: ${no_proxy:-}
container_name: relyloop-ui-1
restart: unless-stopped
depends_on:
api:
condition: service_healthy
environment:
# Corporate HTTP proxy (runtime egress). Empty default → no proxy.
# SSR calls to the API are Compose-internal — covered by `api` in no_proxy.
http_proxy: ${http_proxy:-}
https_proxy: ${https_proxy:-}
no_proxy: ${no_proxy:-}
HTTP_PROXY: ${http_proxy:-}
HTTPS_PROXY: ${https_proxy:-}
NO_PROXY: ${no_proxy:-}
ports:
- "127.0.0.1:3000:3000"
healthcheck:
Expand Down
31 changes: 29 additions & 2 deletions docs/01_architecture/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,13 +198,13 @@ Operators behind a corporate network — where Docker Hub and GHCR are reachable
| Env var | Default | Purpose |
|---|---|---|
| `BASE_REGISTRY` | empty (Docker Hub) | Prefix prepended to `python:3.14-slim@…` (backend `Dockerfile`) and `node:26-bookworm-slim@…` (`ui/Dockerfile`). |
| `UV_REGISTRY` | `ghcr.io/` | Prefix for the `astral-sh/uv:0.5.7@…` COPY-from stage in the backend image. |
| `GHCR_REGISTRY` | `ghcr.io/` | Prefix prepended to every GHCR-hosted image the build references. Currently used by the `astral-sh/uv:0.5.7@…` `uv-source` alias stage in the backend image; any future GHCR image lands under the same prefix. |

**Two override patterns:**

```bash
# Single proxy fronting both Docker Hub and GHCR (typical Artifactory setup)
BASE_REGISTRY=artifactory.example.com/ UV_REGISTRY=artifactory.example.com/ make up
BASE_REGISTRY=artifactory.example.com/ GHCR_REGISTRY=artifactory.example.com/ make up

# Persistent: uncomment the two lines in .env, set the values, then `make up`
```
Expand All @@ -215,6 +215,33 @@ BASE_REGISTRY=artifactory.example.com/ UV_REGISTRY=artifactory.example.com/ make

See `.env.example` for the canonical comment + the override examples, and the top of the backend `Dockerfile` for the in-file rationale.

### Corporate HTTP proxy (apt / PyPI / npm + runtime egress)

A corp-proxy install almost always also needs an outbound HTTP proxy — the registry override above only fixes Docker image pulls; `apt-get`, `uv sync`, and `pnpm install` still reach Debian / PyPI / npm during the build, and the runtime API still calls OpenAI / GitHub / registered clusters. Three more env vars feed every service's `build.args` and end up as `ENV` in every stage of both Dockerfiles:

| Env var | Purpose |
|---|---|
| `http_proxy` / `HTTP_PROXY` | Outbound HTTP egress |
| `https_proxy` / `HTTPS_PROXY` | Outbound HTTPS egress |
| `no_proxy` / `NO_PROXY` | Comma-separated exemption list |

Both case variants are written by Compose because Linux tooling is split on the convention (apt + curl prefer lowercase; uv + pip + Python `requests` accept either; npm + pnpm prefer uppercase). The Dockerfile ENV blocks set both from the single lowercase ARG.

**The `no_proxy` gotcha — Compose service names + `host.docker.internal`.** Without `postgres,redis,elasticsearch,opensearch,solr,api,worker,migrate` in `no_proxy`, the worker's `http://elasticsearch:9200` call (and similar in-network HTTP) gets routed through the corporate proxy, which has no path to those Compose-internal hostnames. Similarly, `host.docker.internal` must be exempted so a local-LLM setup pointing `OPENAI_BASE_URL` at Ollama / LM Studio / vLLM on the host doesn't get intercepted by the proxy. The recommended value in `.env.example` bakes all of these + `169.254.169.254` (EC2/cloud metadata) + `10.0.0.0/8` (internal VPC) into the default; if you set `no_proxy` manually, include them.

**Architecture: build-time vs runtime.** Build-time proxying uses Docker's [predefined proxy ARGs](https://docs.docker.com/build/building/variables/#predefined-args) (`http_proxy`/`https_proxy`/`no_proxy` + uppercase) — BuildKit forwards them from `--build-arg` into every `RUN` step's environment automatically, with no `ARG` declaration needed in the Dockerfile, and intentionally excludes them from `docker history` so the proxy URL never gets baked into the image. Runtime proxying is set via each Compose service's `environment:` block (also wired through to `${http_proxy:-}` / etc.), keeping the image portable. The two paths read the same `.env` values.

Example for the most common shape (HTTP proxy in front of open egress):

```bash
# In .env
http_proxy=http://http.proxy.your-corp.com:8000
https_proxy=http://http.proxy.your-corp.com:8000
no_proxy=your-corp.com,.your-corp-cloud.com,localhost,127.0.0.1,10.0.0.0/8,169.254.169.254,host.docker.internal,postgres,redis,elasticsearch,opensearch,solr,api,worker,migrate
```

**The deeper Artifactory-mirror case.** If the corp network has no direct egress at all and Artifactory hosts virtual repos for Debian / PyPI / npm, `HTTP_PROXY` won't help — the build would need apt-source overrides, `UV_INDEX_URL` set to Artifactory's PyPI mirror, and `npm config set registry` pointing at Artifactory's npm mirror. That's a bigger change and isn't currently wired through the Dockerfiles; file an issue if you hit it.

## Reserved for later releases

The umbrella spec §25 lists the full GA v1 deployment (which includes Caddy, Langfuse, ClickHouse, SigNoz). MVP1 ships only the 6 containers above. The remaining services activate at:
Expand Down
2 changes: 1 addition & 1 deletion docs/03_runbooks/local-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ Common causes:
- **Behind a corporate proxy (Artifactory etc.)** — if `make up` fails on
`failed to resolve source metadata for docker.io/library/python:…` or
`ghcr.io/astral-sh/uv:…`, your network blocks direct registry access. Set
`BASE_REGISTRY` + `UV_REGISTRY` to your proxy URL (with trailing slash) in
`BASE_REGISTRY` + `GHCR_REGISTRY` to your proxy URL (with trailing slash) in
`.env` and re-run `make up` — see
[`docs/01_architecture/deployment.md` §"Corporate registry proxy support"](../01_architecture/deployment.md).

Expand Down
14 changes: 14 additions & 0 deletions ui/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@
# PinnedDependencies check still credits the pin on every FROM line.
ARG BASE_REGISTRY=

# ---------------------------------------------------------------------------
# Corporate HTTP proxy — handled via BuildKit predefined ARGs + Compose
# `environment:`, NOT via Dockerfile ARG/ENV.
# ---------------------------------------------------------------------------
# Docker treats `http_proxy`/`https_proxy`/`no_proxy` (+ uppercase) as
# predefined ARGs: BuildKit forwards them from `--build-arg` into every RUN
# step's environment automatically and excludes them from `docker history`
# so the URL never gets baked into the image. The `build.args:` block in
# `docker-compose.yml` is wired through; runtime egress is handled
# separately via each service's `environment:` block (also in
# docker-compose.yml). See the backend `Dockerfile` + `.env.example` for
# the full rationale and override examples. `no_proxy` must include the
# Compose service names AND `host.docker.internal`.

# node:26-bookworm-slim, digest-pinned (PinnedDependencies / OSSF Scorecard).
# The digest is written literally on every FROM (not via an ARG) because
# Scorecard's static parser only credits a pin it can see inline as
Expand Down
Loading