Local Development
docsfy has two useful local workflows:
| Workflow | Best for | Open in your browser |
|---|---|---|
| Split development | React UI work, fast feedback, hot reload | http://localhost:5173 |
| Built-SPA check | Production-like checks, backend serving the UI itself | http://localhost:8000 |
In split development, FastAPI runs on port 8000 and Vite runs on port 5173. You use the Vite URL in the browser, and Vite proxies API, docs, health, and WebSocket traffic back to FastAPI.
In built-SPA mode, FastAPI serves the built frontend from frontend/dist, so you only use port 8000.
Prerequisites
- Python
3.12+.pyproject.tomlsetsrequires-python = ">=3.12". uvfor Python commands.- Node and npm for the frontend. The Docker image builds the SPA with Node
20, so matching that locally is the safest option. - A writable data directory for the database and generated docs.
Where settings go
Not every setting is loaded the same way, which matters for local development.
- Put app settings such as
ADMIN_KEY,DATA_DIR,SECURE_COOKIES,AI_PROVIDER, andAI_MODELin the repo root.env. - Pass backend launcher settings such as
DEBUG,HOST, andPORTin the shell when startingdocsfy-server. - Pass
API_TARGETin the shell when starting the Vite dev server. DEV_MODEis handled byentrypoint.sh, so it matters for container startup, not for a host-localuv run docsfy-server.
The app settings loader in src/docsfy/config.py reads .env automatically:
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
The backend launcher in src/docsfy/main.py reads DEBUG, HOST, and PORT directly from the process environment:
def run() -> None:
import uvicorn
reload = os.getenv("DEBUG", "").lower() == "true"
host = os.getenv("HOST", "127.0.0.1")
port = int(os.getenv("PORT", "8000"))
uvicorn.run("docsfy.main:app", host=host, port=port, reload=reload)
Configure .env
Start from the values in .env.example:
# Required: Admin password (minimum 16 characters)
ADMIN_KEY=
# AI provider and model defaults
AI_PROVIDER=cursor
AI_MODEL=gpt-5.4-xhigh-fast
AI_CLI_TIMEOUT=60
# Logging
LOG_LEVEL=INFO
# Data directory for database and generated docs
DATA_DIR=/data
# Cookie security (set to false for local HTTP development)
SECURE_COOKIES=true
# Development mode: starts Vite dev server on port 5173 alongside FastAPI
# DEV_MODE=true
For host-local development, the most important changes are:
- Set
ADMIN_KEYto a real value with at least 16 characters. - Change
SECURE_COOKIES=falseif you are using plainhttp://localhost. - Consider changing
DATA_DIRto something writable and local to the repo, such as./data, instead of the container-oriented default/data.
Warning: With
SECURE_COOKIES=true, browser login over plain local HTTP will not persist because the session cookie is markedsecure.Note: The login screen expects username
adminand theADMIN_KEYvalue from your environment.
Run the backend locally
From the repo root, start the backend with docsfy-server:
DEBUG=true uv run docsfy-server
That gives you:
- FastAPI on
127.0.0.1:8000by default - automatic reload when
DEBUG=true - the
/healthendpoint athttp://localhost:8000/health
If you need the backend to listen on a different interface or port, pass HOST and PORT in the shell when you start it.
Tip:
DEBUGis not read from.envbydocsfy-server. Set it in the shell when you launch the backend.
Run the frontend locally
The frontend scripts in frontend/package.json are:
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"test": "vitest run"
}
Start the frontend from frontend/:
cd frontend
npm ci
npm run dev
Then open http://localhost:5173.
This is the best workflow for day-to-day UI development because Vite handles hot reload and the browser stays on the frontend dev server.
How the Vite proxy changes the workflow
When Vite is running, the browser still uses paths like /api/..., /docs/..., and /api/ws. The difference is that Vite forwards those requests to the backend for you.
In frontend/vite.config.ts:
const API_TARGET = process.env.API_TARGET || 'http://localhost:8000'
export default defineConfig({
plugins: [react(), tailwindcss()],
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'/api': {
target: API_TARGET,
changeOrigin: true,
ws: true,
},
'/docs': {
target: API_TARGET,
changeOrigin: true,
},
'/health': {
target: API_TARGET,
changeOrigin: true,
},
},
},
})
The frontend code is written around same-origin browser requests:
const response = await fetch(`${path}`, config)
And the WebSocket client connects back to the current browser host:
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const url = `${protocol}//${window.location.host}/api/ws`
this.ws = new WebSocket(url)
In practice, that means:
- Use
http://localhost:5173in the browser while Vite is running. - API requests still reach FastAPI on port
8000through the proxy. - WebSocket updates still work in local development because the
/apiproxy enablesws: true. - Generated docs under
/docs/...still open correctly because Vite also proxies/docs.
Tip: If your backend is not running on
http://localhost:8000, setAPI_TARGETin the shell before startingnpm run dev.
When to build the SPA
You only need npm run build when FastAPI itself should serve the frontend from frontend/dist.
That includes:
- checking the backend-served UI on port
8000 - running without Vite
- building the production container image
FastAPI serves the built SPA from frontend/dist in src/docsfy/main.py:
_frontend_dist = Path(__file__).parent.parent.parent / "frontend" / "dist"
_assets_dir = _frontend_dist / "assets"
if _assets_dir.exists():
app.mount(
"/assets", StaticFiles(directory=str(_assets_dir)), name="frontend-assets"
)
@app.get("/{path:path}")
async def spa_catch_all(path: str) -> FileResponse:
if path.startswith(("api/", "docs/")) or path in ("api", "docs"):
raise HTTPException(status_code=404, detail="Not found")
index = _frontend_dist / "index.html"
if index.exists():
return FileResponse(str(index))
raise HTTPException(
status_code=404,
detail="Frontend not built. Run: cd frontend && npm run build",
)
Build it with:
cd frontend
npm run build
After that, uv run docsfy-server can serve the app directly on port 8000.
Note: You do not need to build the SPA for normal React development on port
5173.npm run devis enough.
The Docker build already does this for you. In Dockerfile:
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
COPY --chown=appuser:0 --from=frontend-builder /app/frontend/dist /app/frontend/dist
So a normal non-dev container run already has a built frontend/dist.
What DEV_MODE does
DEV_MODE is handled by entrypoint.sh. It changes how the container starts:
if [ "$DEV_MODE" = "true" ]; then
echo "DEV_MODE enabled - installing frontend dependencies..."
cd /app/frontend || exit 1
npm ci
echo "Starting Vite dev server on port 5173..."
npm run dev &
VITE_PID=$!
trap 'kill $VITE_PID 2>/dev/null; wait $VITE_PID 2>/dev/null' SIGTERM SIGINT
cd /app
echo "Starting FastAPI with hot reload on port 8000..."
uv run --no-sync uvicorn docsfy.main:app \
--host 0.0.0.0 --port 8000 \
--reload --reload-dir /app/src
else
exec uv run --no-sync uvicorn docsfy.main:app \
--host 0.0.0.0 --port 8000
fi
With DEV_MODE=true inside the container:
- Vite starts on port
5173 - FastAPI starts on port
8000 - the backend uses
--reload - you should browse to
http://localhost:5173
Without DEV_MODE:
- only FastAPI starts
- the container expects a built
frontend/dist - you browse to
http://localhost:8000
Warning: If you are running the app directly on your host,
DEV_MODE=truedoes not start Vite for you. Start the backend and frontend as separate processes instead.
Using DEV_MODE with docker-compose
The sample docker-compose.yaml already shows the pieces needed for container-based development:
services:
docsfy:
ports:
- "8000:8000"
# Uncomment for development (DEV_MODE=true)
# - "5173:5173"
volumes:
- ./data:/data
# Uncomment for development (hot reload)
# - ./frontend:/app/frontend
env_file:
- .env
environment:
- ADMIN_KEY=${ADMIN_KEY}
# Uncomment for development
# - DEV_MODE=true
For container-based frontend development:
- Enable
DEV_MODE=true. - Expose port
5173. - Bind-mount
./frontend:/app/frontend. - Open
http://localhost:5173.
Warning: The sample compose file only comments in a frontend bind mount.
entrypoint.shenables backend reload with--reload --reload-dir /app/src, but host-side Python edits will only hot-reload if the container can also see updatedsrc/files.Tip: If you plan to change backend Python code often, host-local development is the simpler workflow unless you also add a backend source bind mount to your container setup.
Quick reference
- React/UI work: run the backend on
8000, run Vite on5173, and browse tohttp://localhost:5173. - Backend-served UI check: build the SPA with
npm run build, then browse tohttp://localhost:8000. DEV_MODEis for the container entrypoint workflow.- The Vite proxy is what makes
/api,/docs,/health, and/api/wswork fromhttp://localhost:5173without extra CORS setup.