Co-authored-by: Michael Chihlas <michael@resolutionflow.com> Co-committed-by: Michael Chihlas <michael@resolutionflow.com>
30 KiB
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 seepatherlyin a config file, that's drift from an earlier rename and is being swept in a separate commit — useresolutionflow. CLAUDE.md tracks the live-code files that still referencepatherly.
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
.envfile. The.gitignorealready 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!):
| 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:
-
Confirm
AI_PROVIDER=anthropicandANTHROPIC_API_KEYis set inbackend/.env. -
Start the backend with log level INFO or lower.
-
In the UI, open
/pilotand send a chat message. Wait a few seconds for the response. -
Send a second chat message in the same session, within 5 minutes of the first.
-
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' -
Expected: two
anthropic.cachelog events. First hascache_creation_input_tokens > 0. Second hascache_read_input_tokens > 0. -
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
.envfiles. The.gitignorecovers this. SECRET_KEYshould be generated per-host, not reused across environments.ANTHROPIC_API_KEYis 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.1or 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>:5173and<host>:8000to 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, justhttp://<host>:5173andhttp://<host>:8000reachable 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 1–10 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.netFQDN). - 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
resolutionflowDocker 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/docsfrom 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.devfor bothbackendandfrontendis still the build source — this didn't change.- Explicit
command:directives on bothbackend(uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload) andfrontend(npm run dev -- --host 0.0.0.0 --port 5173) — this guarantees--host 0.0.0.0regardless of what's baked into the image, so the services listen on all interfaces and are reachable from outside the container. REPO_ROOTis 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.