Files
resolutionflow/DEV-ENV.md
Michael Chihlas f1be3abcc5
Some checks failed
CI / e2e (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / backend (push) Has been cancelled
Mirror to GitHub / mirror (push) Has been cancelled
feat: self-serve signup Phase 2 (frontend cutover) (#162)
Co-authored-by: Michael Chihlas <michael@resolutionflow.com>
Co-committed-by: Michael Chihlas <michael@resolutionflow.com>
2026-05-07 18:42:20 +00:00

30 KiB
Raw Permalink Blame History

ResolutionFlow — Dev Environment Setup & Operations Guide

Scope: Stand up a working ResolutionFlow dev environment from scratch on any Linux host (VPS, on-prem Proxmox LXC/VM, bare metal). Self-contained — do not read another doc to get the dev stack running. Last rewritten: April 2026, post-Hostinger-VPS deprecation, ahead of Proxmox migration. Audience: You (returning to the project), a teammate, or a fresh Claude Code session.

If you're picking up mid-migration and need to know what code state is on the current branch, read docs/FlowAssist_Migration/MIGRATION-HANDOFF.md first.


1. What this project needs, regardless of host

These are non-negotiable. If your host can't provide them, fix that before anything else.

Component Required version Notes
Linux any mainstream distro Ubuntu 22.04+ / Debian 12+ tested; Alpine fine for containers
Python 3.11+ Backend and migrations
Node.js 20.19+ Vite 7 fails on older versions — CLAUDE.md Lesson 63
PostgreSQL 16 gen_random_uuid() + jsonb + RLS are all leaned on
Docker + Docker Compose recent Only if you are running Postgres and/or backend as containers
Git recent

Optional but recommended:

Tool Why
code-server Browser-based VS Code; how this project has historically been edited
gh CLI Mirror repo is on GitHub via Gitea; gh reads issues and PRs
bun Required for the gstack /browse + /qa skills (CLAUDE.md Lesson 82)
npx gitnexus analyze Code-graph for Phase 2+ work that touches unified_chat_service
Claude Code CLI If you want to run Claude Code locally on the host

2. Architectural shape

The project is three services plus your editor. Keep these facts in mind regardless of topology:

Your browser
   ├─► code-server         (editor, optional — usually port 8080 or behind TLS)
   ├─► frontend (Vite)     (dev server, port 5173)
   └─► backend (FastAPI)   (dev server, port 8000)
                               │
                               └─► PostgreSQL (port 5432)

The frontend calls the backend by URL at runtime. The frontend does not proxy through the backend. Whatever URL your browser uses to reach the backend is what VITE_API_URL must be set to, baked in at build time. Changing VITE_API_URL requires rebuilding the frontend.

The backend calls the database by URL at runtime. The URL depends on where Postgres is relative to the backend — Docker service name if both are in the same compose network, localhost if Postgres is native on the same host, or a DNS name if they're in separate containers/VMs.

CORS is configured explicitly. The backend's CORS_ORIGINS list must include every origin your browser will use to reach the frontend. A missing origin shows up as failed preflight requests.


3. Topology choices — pick one before you start

The project is agnostic to topology, but each shape has different setup steps.

Option A — all-in-one LXC/VM/host (simplest)

Postgres, backend, and frontend all run on one Linux host. code-server runs on the same host or a sibling. No Docker required. Best for a single-developer Proxmox LXC.

Option B — Docker Compose on one host

Postgres, backend, and frontend run as Docker containers on one host. code-server runs outside the compose network (on the host or in another container). This is how the old Hostinger VPS was configured. Best if you want reproducible container images.

Option C — split services across containers/VMs

Postgres in one container/VM, backend and frontend in another, code-server in a third. Most complex; requires explicit networking between them. Use only if you have a specific reason.

Pick one and stick with it for the entire setup. Mixing Options A and B halfway through is where setup runs off the rails.


4. Per-host configuration

These values are specific to your host. Fill them in once and reference them by name throughout the rest of the doc.

DEV_HOST              = <hostname or IP your browser uses, e.g. dev.internal, 10.0.0.42>
DEV_HOST_SCHEME       = <http or https; http is fine for internal dev, https if behind a TLS proxy>
FRONTEND_PORT         = 5173
BACKEND_PORT          = 8000
POSTGRES_PORT         = 5433  # host-side port. 5433 is the recommended default on any shared host to avoid collision with a host-level Postgres. The container's internal port stays 5432.
POSTGRES_DB_NAME      = resolutionflow
POSTGRES_USER         = postgres
POSTGRES_PASSWORD     = <local-dev-password; anything, this is not prod>
SECRET_KEY            = <openssl rand -hex 32 — generate fresh per host, do not reuse>
ANTHROPIC_API_KEY     = <from https://console.anthropic.com>
GOOGLE_AI_API_KEY     = <optional, only if using Gemini as a fallback>

Store these somewhere you can copy from during setup. Do not commit them.

Naming note: the canonical database name is resolutionflow. If you see patherly in a config file, that's drift from an earlier rename and is being swept in a separate commit — use resolutionflow. CLAUDE.md tracks the live-code files that still reference patherly.


5. Setup procedure

Run these in order. Stop at the first failure and investigate.

5.1 Install system dependencies

# Ubuntu / Debian
sudo apt update && sudo apt install -y \
  git curl build-essential \
  python3.12 python3.12-venv python3-pip \
  postgresql-client   # not the server — only if running Postgres natively

# Node 20 via nvm (survives container rebuilds if stored in a volume)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh"
nvm install 20
nvm alias default 20

For Option B (Docker Compose), also:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER   # log out and back in for this to take effect

5.2 Clone the repo

git clone https://gitea.resolutionflow.com/chihlasm/resolutionflow.git
# or the GitHub mirror:
# git clone https://github.com/chihlasm/resolutionflow.git
cd resolutionflow

# Check out the working branch if you're continuing mid-migration.
git fetch origin
git checkout feat/flowpilot-migration

5.3 Start PostgreSQL

Option A (native Postgres on the host):

sudo apt install -y postgresql-16
sudo -u postgres psql -c "CREATE DATABASE resolutionflow;"
sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres';"
# Adjust pg_hba.conf if you need non-local connections.

Option B (Postgres via Docker Compose): The repo has a docker-compose.dev.yml at the root. Check its Postgres service for the container name, port mapping, and volume. The local compose defaults use container name resolutionflow_postgres, database resolutionflow, and host-side port 5433 (mapped to the container's internal 5432) — see CLAUDE.md Lesson 65. The host-side 5433 is the recommended default on any shared host: it keeps the port free for a host-level Postgres if you ever need one. The compose file also defines explicit command: directives on both backend and frontend to force --host 0.0.0.0, and expects the caller to pass REPO_ROOT (see 5.4) for bind-mount resolution. Confirm what the compose file actually says on your branch before trusting these values.

docker compose -f docker-compose.dev.yml up -d db
docker compose -f docker-compose.dev.yml logs db    # wait for "ready to accept connections"

Verify:

# From the host (Option A) or the backend container/LXC (Option B):
psql -h <db-host> -p <POSTGRES_PORT> -U postgres -d resolutionflow -c "SELECT now();"

5.4 Write the .env files

The repo expects three env files. Create each one:

backend/.env — backend source of truth:

APP_NAME=ResolutionFlow
DEBUG=true

# DB URLs — `<db-host>` is `localhost` for Option A, the Docker service name
# (e.g. `db`) for Option B, or the DB container/VM hostname for Option C.
DATABASE_URL=postgresql+asyncpg://postgres:postgres@<db-host>:<POSTGRES_PORT>/resolutionflow
DATABASE_URL_SYNC=postgresql://postgres:postgres@<db-host>:<POSTGRES_PORT>/resolutionflow

# Auth
SECRET_KEY=<SECRET_KEY>
ACCESS_TOKEN_EXPIRE_MINUTES=5
REFRESH_TOKEN_EXPIRE_DAYS=7
REQUIRE_INVITE_CODE=true

# AI providers
AI_PROVIDER=anthropic
ANTHROPIC_API_KEY=<ANTHROPIC_API_KEY>
GOOGLE_AI_API_KEY=<GOOGLE_AI_API_KEY or leave unset>

# FlowPilot MCP telemetry — leave on so the Phase 0.5 baseline data keeps accruing
ENABLE_MCP_MICROSOFT_LEARN=true

# CORS + frontend URL
FRONTEND_URL=<DEV_HOST_SCHEME>://<DEV_HOST>:<FRONTEND_PORT>
CORS_ORIGINS=["http://localhost:5173","http://127.0.0.1:5173","<DEV_HOST_SCHEME>://<DEV_HOST>:<FRONTEND_PORT>"]

frontend/.env.local — frontend build-time config:

VITE_API_URL=<DEV_HOST_SCHEME>://<DEV_HOST>:<BACKEND_PORT>

Optional PostHog (CLAUDE.md Lesson 64 — enables product analytics locally):

VITE_PUBLIC_POSTHOG_KEY=<from PostHog project settings>
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

Repo root .env — only needed for Option B (Docker Compose interpolation):

SECRET_KEY=<SECRET_KEY>
ANTHROPIC_API_KEY=<ANTHROPIC_API_KEY>
GOOGLE_AI_API_KEY=<GOOGLE_AI_API_KEY or leave unset>
POSTGRES_PORT=<POSTGRES_PORT>
# Absolute host-side path to the repo root. REQUIRED whenever docker-compose is
# invoked from inside a container (e.g. a code-server container with the host
# Docker socket mounted in). Without it, the bind mounts in
# docker-compose.dev.yml (`${REPO_ROOT}/backend:/app`, `${REPO_ROOT}/frontend:/app`)
# resolve against the CLI's CWD — a path the host daemon cannot see — and
# Docker silently creates empty directories there instead of mounting the code.
# If you run docker compose directly on the host shell, you can set this to `.`
# or the absolute path of the repo; being explicit is safer either way.
REPO_ROOT=/absolute/path/to/resolutionflow

Never commit any .env file. The .gitignore already covers this.

5.5 Run the backend setup

Option A (native):

cd backend
python3.12 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

# Migrate the DB to head.
alembic upgrade head

Option B (Docker):

docker compose -f docker-compose.dev.yml up -d backend
docker compose -f docker-compose.dev.yml run --rm backend alembic upgrade head

Expected alembic head (as of feat/flowpilot-migration): f07010f17b01. If alembic current shows anything else after upgrade head, something has gone wrong — stop and investigate.

5.6 Seed test users

# Option A
cd backend && source venv/bin/activate
python -m scripts.seed_test_users

# Option B
docker exec resolutionflow_backend python -m scripts.seed_test_users

Test users (all share password TestPass123!):

Email Role
admin@resolutionflow.example.com super admin
teamadmin@resolutionflow.example.com team admin
engineer@resolutionflow.example.com engineer
pro@resolutionflow.example.com solo pro

5.7 Run the backend

Option A:

cd backend && source venv/bin/activate
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

Option B: Already running from docker compose up -d backend. Tail logs:

docker compose -f docker-compose.dev.yml logs -f backend

Verify: curl <DEV_HOST_SCHEME>://<DEV_HOST>:<BACKEND_PORT>/api/docs — OpenAPI docs page loads.

5.8 Run the frontend

Option A:

cd frontend
npm install
npm run dev -- --host 0.0.0.0 --port 5173

Option B:

docker compose -f docker-compose.dev.yml up -d --build frontend

Verify: Open <DEV_HOST_SCHEME>://<DEV_HOST>:<FRONTEND_PORT> in your browser. Log in with one of the test users. Navigate to /pilot — the FlowPilot session page should render.


6. Verification — proof the env actually works

Run these after setup. Every item has a concrete expected outcome.

6.1 Database schema is at the right version

# Option A
cd backend && source venv/bin/activate && alembic current
# Option B
docker compose -f docker-compose.dev.yml run --rm backend alembic current

Expected: f07010f17b01 (head) on the feat/flowpilot-migration branch. On main, expected: 074 (head).

6.2 Alembic reversibility

alembic downgrade -1    # should complete cleanly
alembic upgrade head    # should return to f07010f17b01

If either step fails, the migration has a bug and Phase 2 cannot start.

6.3 Prompt-cache hit verification (the deferred Phase 0 TODO)

backend/app/core/ai_provider.py module docstring has a TODO(phase0-verify) note describing this. Procedure:

  1. Confirm AI_PROVIDER=anthropic and ANTHROPIC_API_KEY is set in backend/.env.

  2. Start the backend with log level INFO or lower.

  3. In the UI, open /pilot and send a chat message. Wait a few seconds for the response.

  4. Send a second chat message in the same session, within 5 minutes of the first.

  5. In backend logs, grep for lines containing anthropic.cache:

    # Option A
    grep 'anthropic.cache' <log-path>
    # Option B
    docker compose -f docker-compose.dev.yml logs backend | grep 'anthropic.cache'
    
  6. Expected: two anthropic.cache log events. First has cache_creation_input_tokens > 0. Second has cache_read_input_tokens > 0.

  7. If the second shows zero reads, inspect the prompt prefix for silent invalidators (timestamps, unsorted JSON keys, varying tool list ordering). Fix before proceeding with any Phase 2 work.

6.4 Frontend build is TypeScript-clean

cd frontend
npx tsc -b        # no errors
npm run build     # no errors

CLAUDE.md Lesson 105 notes that npm run build may fail with an EACCES on dist/ inside code-server — that is a Docker filesystem permission issue, not a real build error. Use npx tsc -b to verify TypeScript cleanliness in that case.

6.5 /assistant/pilot redirect

Open <DEV_HOST_SCHEME>://<DEV_HOST>:<FRONTEND_PORT>/assistant/<some-real-session-id> in the browser. Expected: URL changes to /pilot/<that-id>; the FlowPilot session page renders. Bare /assistant redirects to bare /pilot.

6.6 Dispatcher de-branching

Navigate to the dashboard. Click a session in ActiveFlowPilotSessions or RecentFlowPilotSessions. Expected: routes to /pilot/:id regardless of the session's session_type value. (Check the browser URL bar.)

6.7 CORS

Open the browser DevTools Network tab, navigate to any backend-hitting page. Expected: no CORS errors. If you see "blocked by CORS policy," the missing origin needs adding to backend/.env's CORS_ORIGINS.


7. Runbook

Day-to-day commands after setup is complete.

Restart services

# Option A
# backend — Ctrl-C and re-run uvicorn
# frontend — Ctrl-C and re-run npm run dev

# Option B
docker compose -f docker-compose.dev.yml restart backend
docker compose -f docker-compose.dev.yml up -d --build frontend    # rebuild required if VITE_* changed
docker compose -f docker-compose.dev.yml down && docker compose -f docker-compose.dev.yml up -d   # full restart

Apply a new migration

# Option A
cd backend && source venv/bin/activate && alembic upgrade head
# Option B
docker compose -f docker-compose.dev.yml run --rm backend alembic upgrade head

Create a new migration

# Option A
cd backend && source venv/bin/activate
alembic revision -m "short description"        # manual, preferred per CLAUDE.md Lesson 77
# OR
alembic revision --autogenerate -m "description"  # pulls in drift; review carefully

Never pass --rev-id — let Alembic generate the hex hash.

Inspect the database

# Option A (native Postgres)
psql -h localhost -p 5432 -U postgres -d resolutionflow

# Option B (Docker)
docker exec -it resolutionflow_postgres psql -U postgres -d resolutionflow

Run tests

# Option A
cd backend && source venv/bin/activate
pytest --override-ini="addopts="

# Option B
docker compose -f docker-compose.dev.yml run --rm backend pytest --override-ini="addopts="

First time only, create the test database:

# Option A
sudo -u postgres psql -c "CREATE DATABASE resolutionflow_test;"

# Option B
docker exec -it resolutionflow_postgres psql -U postgres -c "CREATE DATABASE resolutionflow_test;"

View backend logs

# Option A: wherever you ran uvicorn
# Option B
docker compose -f docker-compose.dev.yml logs -f --tail=100 backend

Structured events to grep for:

  • anthropic.cache — prompt-cache hit/creation telemetry (Phase 0.1)
  • mcp.turn — per-turn MCP availability/invocation (Phase 0.5)
  • mcp.fallback — MCP silent-retry fallback fired (Phase 0.5)

8. Troubleshooting

CORS errors in the browser

The backend did not accept the origin your browser used. Check backend/.env's CORS_ORIGINS — it must include the exact scheme + host + port the browser sent. Restart the backend after editing.

VITE_API_URL points at the wrong place

The frontend was built with a stale value. Rebuild the frontend. Option B: docker compose up -d --build frontend. Option A: restart npm run dev.

alembic upgrade head fails with "target database is not up to date"

Your DB migration chain is out of sync with the code. On a dev box, the safe recovery is to drop the DB and re-migrate from scratch:

# Option A
sudo -u postgres psql -c "DROP DATABASE resolutionflow;" -c "CREATE DATABASE resolutionflow;"
cd backend && source venv/bin/activate && alembic upgrade head

# Option B
docker exec resolutionflow_postgres psql -U postgres -c "DROP DATABASE resolutionflow;" -c "CREATE DATABASE resolutionflow;"
docker compose -f docker-compose.dev.yml run --rm backend alembic upgrade head

Only do this on a dev box — it destroys all local data.

alembic heads shows more than one head

Only on a local branch that has diverged from origin/main. Production main has a single head. If this happens on a fresh clone, one of your local migration files has the wrong down_revision. Inspect each file's down_revision and reconnect the chain.

Frontend build fails with "EACCES: permission denied" on dist/

Filesystem permission issue inside the code-server container (CLAUDE.md Lesson 105). TypeScript compilation itself completes — use npx tsc -b to verify cleanliness without needing to write to dist/.

Backend/frontend containers start but /app is empty (no code mounted)

Almost always a REPO_ROOT problem. docker-compose.dev.yml uses ${REPO_ROOT}/backend:/app and ${REPO_ROOT}/frontend:/app bind mounts. If REPO_ROOT is unset, or set to a path that doesn't exist on the Docker host (not inside the code-server container), Docker silently creates an empty directory at that path and mounts it — the containers come up but have no source code. Symptom: backend returns import errors, or frontend serves a default Vite page. Fix: set REPO_ROOT in the repo-root .env to the absolute host-side path to the repo, then docker compose down && docker compose up -d. See 5.4 for the full note. This matters specifically when docker compose is invoked from inside a container (e.g. code-server with the host Docker socket mounted) — the CLI's CWD is container-local but the daemon resolves paths against the host filesystem.

Frontend shows "Blocked request. This host is not allowed" in the browser

Vite 5+ ships DNS-rebinding protection that rejects any Host: header not in server.allowedHosts. The browser's hostname must be in that list. Edit frontend/vite.config.ts — the server.allowedHosts array should include every hostname you reach the dev server from (e.g. 'docker-01', 'localhost', .ts.net as a wildcard for Tailscale MagicDNS). Restart the Vite dev server (for Option B: docker compose restart frontend). This is unrelated to CORS — Vite blocks the request before any app code runs.

docker command not found inside code-server

If your code-server is itself inside a container, Docker is probably not exposed to it. CLAUDE.md Lesson 103 was written for this case on the old VPS. On Proxmox, the fix depends on topology — either SSH to the host to run Docker commands, or mount the host's Docker socket into the code-server container.

Backend returns 500 with InsufficientPrivilegeError: new row violates row-level security policy

RLS is enabled on a table your code wrote to without the right account_id. CLAUDE.md Lessons 107, 108, 110 cover this family of bugs. The fix is always at the service layer: make sure every model creation passes account_id= explicitly, and that startup routines that touch tenant-isolated tables use _admin_session_factory() rather than get_db().

Anthropic cache reads are zero on the second turn

Something in the cached prefix is changing between turns. Inspect the system-block list and the first N history messages for timestamps, datetime.now(), unsorted dict keys in JSON prompts, or varying tool-list order. The anthropic.cache telemetry shows exactly how many tokens were read vs created — use it to narrow down the invalidator.


9. Security posture for dev environments

This doc is about dev, not production. But:

  • Never commit .env files. The .gitignore covers this.
  • SECRET_KEY should be generated per-host, not reused across environments.
  • ANTHROPIC_API_KEY is billable — rotate if leaked into logs or chat.
  • Postgres on a dev host should not be exposed to the internet. Bind it to 127.0.0.1 or to a private network interface only.
  • If you expose the frontend or backend publicly (for teammates to test against), put it behind TLS with a real certificate. Do not let dev credentials travel over plain HTTP on the public internet.

10. What's not in this doc

  • Production deployment. This is a dev-env doc. Production lives on Railway — see CLAUDE.md's Deployment section.
  • How to set up Traefik or any particular reverse proxy. Whichever proxy you use is your choice; the dev stack just needs something that routes <host>:5173 and <host>:8000 to the right services. Direct port exposure over a private network (Tailscale, WireGuard, a VPN, or a LAN behind a firewall) is a fully supported option for dev and is what the homelab reference topology in Section 11 uses — no reverse proxy, no TLS, just http://<host>:5173 and http://<host>:8000 reachable only from the private network. That's a perfectly reasonable choice; it's just not the only one.
  • How to configure code-server itself. Install it however you prefer (native, Docker, LXC); point it at the repo, and the rest of this doc applies.
  • Where to host the Proxmox instance. Up to you.

If something in this doc turns out to be wrong on your host, fix the doc. This is a living document — the whole point of rewriting it from the Hostinger-specific version was to make it survive host changes.


11. Reference topology: homelab Proxmox + code-server (Option B)

This section documents the first concrete host instantiation since the April 2026 host-agnostic rewrite. It's a worked example, not the canonical topology — Section 3's Option A/B/C framing still stands. If your setup looks different, follow Sections 110 and ignore this appendix.

11.1 Host

  • Hypervisor: Proxmox (homelab).
  • VM: docker-01, Debian 13, running Docker Engine + Docker Compose natively.
  • Tailscale IP: 100.64.78.44. MagicDNS hostname: docker-01 (and the full .ts.net FQDN).
  • code-server: runs on the same VM in its own container, with the host's Docker socket mounted in so it can drive docker compose. Its workspace bind-mounts the repo at /opt/docker/code-server/workspace/resolutionflow.

This is a concrete instance of Option B from Section 3: Postgres, backend, and frontend all run as containers from docker-compose.dev.yml; the editor lives outside that compose network.

11.2 Access pattern — direct port over Tailscale, no reverse proxy

The browser reaches the dev stack directly:

  • Frontend: http://docker-01:5173
  • Backend: http://docker-01:8000
  • Backend API docs: http://docker-01:8000/api/docs

There is no Caddy, no Traefik, no nginx, no TLS, no basic auth in front of either service. The tailnet provides the wire encryption and access control — only devices on the tailnet can resolve docker-01 or reach 100.64.78.44, and Tailscale ACLs decide which of those devices are allowed to connect.

Why this choice:

  • Zero routing config to maintain. There is no proxy rulebook to keep in sync with new services. Add a container, expose a port, you're done.
  • Backend-to-backend services stay private. Redis, Celery workers, the planned ConnectWise proxy, the MCP server — none of them need to be reachable from the browser, so none of them need proxy rules. They stay inside the resolutionflow Docker network and talk by service name. The proxy would only ever have carried frontend and backend traffic, so the proxy's value was small relative to its maintenance cost.
  • Debuggability. curl http://docker-01:8000/api/docs from any tailnet device works without auth headers, TLS handshakes, or DNS shenanigans.

Tradeoff: this only works because every client device is on the tailnet. If someone needed to test from a non-tailnet device, they'd either join the tailnet or we'd need to front the stack with a proxy. For the current single-developer setup, the tailnet-only assumption holds.

11.3 Per-host config values (as actually configured on docker-01)

Plugging these into Section 4's template:

DEV_HOST              = docker-01
DEV_HOST_SCHEME       = http
FRONTEND_PORT         = 5173
BACKEND_PORT          = 8000
POSTGRES_PORT         = 5433         # host-side; container-internal stays 5432
POSTGRES_DB_NAME      = resolutionflow
POSTGRES_USER         = postgres
POSTGRES_PASSWORD     = postgres     # local-dev only
SECRET_KEY            = <generated per host; do not reuse>
ANTHROPIC_API_KEY     = <from console.anthropic.com>
GOOGLE_AI_API_KEY     = <unset; Anthropic is sole provider in dev>

And the repo-root .env that docker-compose.dev.yml interpolates from:

SECRET_KEY=<redacted>
ANTHROPIC_API_KEY=<redacted>
POSTGRES_PORT=5433
REPO_ROOT=/opt/docker/code-server/workspace/resolutionflow

11.4 Why REPO_ROOT is non-optional on this host

code-server runs inside a container. When you open a terminal in code-server and run docker compose -f docker-compose.dev.yml up -d, the Docker CLI talks to the host daemon via the mounted socket — but the CWD it reports (/config/workspace/resolutionflow) is a path that only exists inside the code-server container. The host daemon has never heard of it.

Relative bind mounts like ./backend:/app therefore resolve against a path the host can't see, and Docker silently creates empty directories there rather than erroring out. The containers come up, but /app is empty.

docker-compose.dev.yml sidesteps this by using ${REPO_ROOT}/backend:/app and ${REPO_ROOT}/frontend:/app. REPO_ROOT must be set to the absolute path on the host (/opt/docker/code-server/workspace/resolutionflow), not the path inside the code-server container. Same contents, different mount point, different name.

If you ever run docker compose directly from a host shell (SSH'd into docker-01), set REPO_ROOT to . or the absolute host path. Being explicit is always safe; leaving it unset is the failure mode.

11.5 Vite server.allowedHosts — required for docker-01 to resolve

Vite 5+ rejects any Host: header not in server.allowedHosts (DNS-rebinding protection). frontend/vite.config.ts has:

server: {
  host: '0.0.0.0',
  allowedHosts: ['docker-01', '.ts.net', 'localhost'],
  ...
}
  • docker-01 — the MagicDNS short name the browser uses day-to-day.
  • .ts.net — wildcard for the full Tailscale MagicDNS FQDN, in case anyone uses it.
  • localhost — for the "am I serving anything at all" smoke-test from inside the container.

If you move this setup to a different host, add that host's hostname to allowedHosts or the browser will see "Blocked request. This host is not allowed." See Section 8's troubleshooting entry for the full symptom/fix.

11.6 CORS origins on this host

The backend service's CORS_ORIGINS environment variable is pinned in the compose file to:

["http://localhost:5173","http://127.0.0.1:5173","http://docker-01:5173","http://100.64.78.44:5173"]

The last two are what make browser calls from tailnet clients work — they cover both MagicDNS (docker-01) and the raw Tailscale IP. If you add a new hostname to reach the frontend from, also add the matching origin here and restart the backend.

11.7 Compose file shape (as of this writing)

docker-compose.dev.yml has been through a round of cleanup for this topology. Specifics worth knowing if you're comparing against older revisions of the file:

  • No Traefik labels. They were removed — nothing in this topology uses Traefik.
  • No Hostinger-VPS-era origins in CORS_ORIGINS.
  • Dockerfile.dev for both backend and frontend is still the build source — this didn't change.
  • Explicit command: directives on both backend (uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload) and frontend (npm run dev -- --host 0.0.0.0 --port 5173) — this guarantees --host 0.0.0.0 regardless of what's baked into the image, so the services listen on all interfaces and are reachable from outside the container.
  • REPO_ROOT is interpolated into both service volume mounts (see 11.4).

If you're adapting the file for a different host, the things most likely to need editing are REPO_ROOT (see 11.4), CORS_ORIGINS (see 11.6), FRONTEND_URL, VITE_API_URL, and POSTGRES_PORT if you want something other than 5433.

11.8 End-to-end sanity check for this topology

From any device on the tailnet:

# Backend reachable
curl -sSf http://docker-01:8000/api/docs >/dev/null && echo OK

# Frontend reachable
curl -sSf http://docker-01:5173 >/dev/null && echo OK

# Alembic head matches the branch expectation
docker exec resolutionflow_backend alembic current
# expect f07010f17b01 on feat/flowpilot-migration, 074 on main

# Postgres is alive inside the compose network
docker exec resolutionflow_postgres psql -U postgres -d resolutionflow -c "SELECT now();"

All four passing = the dev environment is live end-to-end.