Docker dev stack
The dev stack is for one user on one machine. It builds the dashboard into a static bundle, serves it through nginx on http://localhost, and runs the backend with hot reload. It is the quickest way to try CodeSage without installing Python and Node, but it is not for sharing: the bundle contains your API key, so every port binds to 127.0.0.1.
If you want HTTPS, a login and a bundle without a key, use Production deployment. The files for this page are deployment/docker-compose.yml and deployment/nginx.conf. The longer reference is deployment/DOCKER.md.
flowchart TB
you(["Your browser"]) --> nginx["codesage_nginx<br/>127.0.0.1:80, plain HTTP"]
nginx -- "static files" --> spa["Built dashboard<br/>API key inside the bundle"]
nginx -- "/api, /ws, /health" --> backend["codesage_mcp_server<br/>127.0.0.1:8001<br/>uvicorn --reload, one worker"]
bridge(["Claude Desktop bridge"]) --> backend
backend --> vol[("backend_data volume<br/>/app/data/codesage.db")]
backend --> algolia[("Algolia<br/>codesage_code_entities_docker")]
other(["Any other Host header"]) -. "closed with 444" .-> nginxWhat you need
Section titled “What you need”- Docker with Compose v2 (
docker compose). - An Algolia application ID and Admin API key. Without them the stack starts, but
/healthreportsdegradedand adding a repository returns503. - A GitHub token is optional. It raises GitHub rate limits and lets you read private github.com repositories.
Start it
Section titled “Start it”-
Create the env file.
Terminal window cp .env.example backend/.env # or run `make setup`Fill in
ALGOLIA_APP_ID,ALGOLIA_ADMIN_API_KEYand, if you want it,GITHUB_TOKEN. The backend container reads this file throughenv_file. A value left empty, or left as ayour-...-hereplaceholder, counts as unset. -
Make a key and export it. The backend refuses to start without
CODESAGE_API_KEY, because the container listens on0.0.0.0. The dashboard build also needs the same value.Terminal window python -c "import secrets; print(secrets.token_urlsafe(32))" # put this in backend/.envexport CODESAGE_API_KEY=<the same key> # used at build timeIf you ran
make setup, the key is already inbackend/.env. Export it from there:Terminal window export CODESAGE_API_KEY=$(sed -n 's/^CODESAGE_API_KEY=//p' backend/.env) -
Build and start.
Terminal window make build # docker compose -f deployment/docker-compose.yml buildmake up # same file, up -d -
Open it. The app is at
http://localhost. The backend is athttp://127.0.0.1:8001, with API docs at/docs. -
Check it.
Terminal window make health # curls /health and the MCP capabilities; needs jq
To connect Claude Desktop to this stack, run make install-claude. It points the bridge at http://127.0.0.1:8001, which the stack publishes. See Claude Desktop.
What each container does
Section titled “What each container does”codesage_mcp_server (backend) |
codesage_nginx (nginx) |
|
|---|---|---|
| Image | Built from backend/Dockerfile (python:3.13-slim with build-essential, curl, gcc and git, running as a non-root appuser) |
Built from frontend/Dockerfile: a node:22-alpine stage runs the Vite build, then nginx:alpine serves the result |
| Job | FastAPI: REST, WebSocket, MCP tools and the indexing jobs | Serves the built dashboard, and proxies /api/, /ws and /health to the backend |
| Published port | 127.0.0.1:8001 |
127.0.0.1:80 |
| Config | deployment/docker-compose.yml and backend/.env |
deployment/nginx.conf mounted read-only over /etc/nginx/nginx.conf |
| Health check | curl -f http://localhost:8001/health every 30 s, 10 s timeout, 3 retries |
None. It only waits for the backend container to start |
| Restart | unless-stopped |
unless-stopped |
The backend starts with uvicorn app.main:app --host 0.0.0.0 --port 8001 --reload --reload-dir app. It has no --workers option, which means exactly one worker. Job state, WebSocket subscriptions and rate-limit counters live in the process, and SQLite allows one writer. See Architecture.
The nginx image build
Section titled “The nginx image build”VITE_API_BASE_URL is set to /api/v1 and VITE_CODESAGE_API_KEY to your exported CODESAGE_API_KEY. Vite inlines both into the JavaScript at build time. The WebSocket URL is not a build setting: the dashboard builds it from the page’s own address (ws:// or wss://), so it works behind any host.
Ports and who can reach them
Section titled “Ports and who can reach them”Both published ports bind to 127.0.0.1:
| Port | Service | Reachable from |
|---|---|---|
127.0.0.1:80 |
nginx, plain HTTP | Your machine only |
127.0.0.1:8001 |
The backend: REST, WebSocket, /docs |
Your machine only |
| 443 | Not published | Nobody. There is no TLS in the dev stack |
The dev nginx has two server blocks on port 80. The first is the default_server, with server_name _, and it does one thing: return 444, which closes the connection with no reply. The second answers only for localhost and 127.0.0.1. So a request with any other Host header gets an empty reply.
This protects the key. A hostile web page can re-point its own domain at 127.0.0.1 (DNS rebinding). That page keeps its own Host, so it lands in the first block and never receives the bundle. The backend repeats the check for its own port: with a key set, it answers only to loopback hosts and to the hosts named in CORS_ORIGINS. See Security model.
nginx behaviour worth knowing
Section titled “nginx behaviour worth knowing”/wskeepsHostwith its port ($http_host), because the backend compares it with the browser’sOrigin. A remapped port must still match./wsallows idle connections for an hour (proxy_read_timeoutandproxy_send_timeoutof 3600 s)./api/times out after 60 s.- Request bodies are capped at 2 MB. The API only takes small JSON, so there are no uploads.
- Responses are gzipped above 1 KB.
- Everything else falls back to
index.html, so a refresh on/repository/3works.
Settings the Compose file overrides
Section titled “Settings the Compose file overrides”The environment block wins over backend/.env. These come from the Compose file:
| Variable | Value | Why |
|---|---|---|
MCP_SERVER_HOST |
0.0.0.0 |
Docker publishes the port, so the process must listen on all interfaces |
MCP_SERVER_PORT |
8001 |
Matches the published port and nginx’s upstream |
DATABASE_URL |
sqlite:///./data/codesage.db |
Puts the database in the named volume |
ALGOLIA_INDEX_NAME |
${DOCKER_ALGOLIA_INDEX_NAME:-codesage_code_entities_docker} |
A separate index from a local run. Records are keyed by repository ID, so two databases on one index would delete each other’s records |
CORS_ORIGINS |
http://localhost, http://localhost:3000, http://localhost:5173 |
The origins allowed to write. Edit the file to change it |
Not overridden: CODESAGE_API_KEY, ALGOLIA_APP_ID, ALGOLIA_ADMIN_API_KEY and GITHUB_TOKEN come from backend/.env. ENVIRONMENT stays development, so /docs is on.
To use a different Algolia index, export DOCKER_ALGOLIA_INDEX_NAME=your_index before make up.
Hot reload
Section titled “Hot reload”backend/ is bind-mounted at /app, and uvicorn runs with --reload --reload-dir app. Edit a file under backend/app/ and uvicorn restarts the server within a moment.
The dashboard has no hot reload in this stack, because the bundle is built into the nginx image. For front-end work, run the dev servers directly with make start instead. See Installation.
Make targets
Section titled “Make targets”| Target | What it does |
|---|---|
make build |
docker compose -f deployment/docker-compose.yml build |
make up |
Start the stack detached (up -d) |
make down |
Stop the stack. Data is kept |
make restart |
Restart both containers |
make logs |
Follow the logs of both containers |
make logs-backend |
Follow the backend’s logs only |
make status |
docker compose ps for the dev stack |
make shell |
Open bash in the backend container |
make health |
Curl http://127.0.0.1:8001/health and the MCP capabilities. Needs jq. Prints “Backend not responding” if the backend is down |
make clean |
down -v --remove-orphans: delete the dev containers, network and volume |
make clean-all |
Same as clean, plus --rmi local: also remove the locally built images |
make db-reset, make db-migrate and make db-migration act on the local backend/codesage.db, not on the containers. make prod-build and make prod-up are for the prod stack.
Data and cleanup
Section titled “Data and cleanup”- Where the data is. The SQLite file is in the named volume
backend_data, mounted at/app/data. It survivesmake downand rebuilds. The image makes/app/datawritable byappuser, and a new named volume inherits that. make cleandeletes the volume, so the dev database is gone. Repositories must be added again.make clean-allalso removes the images this stack built.- Scoped to this project. Neither target runs a host-wide
docker system prune, so other projects’ containers, images and volumes are untouched. Neither touches the prod stack. - Algolia is not cleaned. The index keeps its records. Delete repositories in the dashboard before
make clean, or remove the index in Algolia.
Dev and prod do not run together
Section titled “Dev and prod do not run together”Both stacks publish port 80, so start only one. Stop the other with docker compose -f <file> down.
What CI checks
Section titled “What CI checks”CI validates the dev Compose file with docker compose config -q and the dev nginx config with nginx -t. It builds the frontend image. It does not build the dev backend image (backend/Dockerfile) and never starts a container, so expect to fix small runtime problems yourself.
When something goes wrong
Section titled “When something goes wrong”| Symptom | Fix |
|---|---|
| Backend exits immediately | CODESAGE_API_KEY is empty and the container binds 0.0.0.0. Set the key and run make logs-backend |
Dashboard shows 401, or the WebSocket is rejected |
The key in the bundle differs from the backend’s. Re-export CODESAGE_API_KEY, then make build && make up |
| Empty reply on another hostname | The dev nginx drops unknown hosts. Use localhost or 127.0.0.1 |
| Port 80 already in use | The prod stack or another service holds it |
The full list is in Troubleshooting.