Phlox ships as a single image that runs both the FastAPI backend and the built
React SPA in one process on one port (8000). Persistent state and the runtime
config live outside the image on mounted volumes, so the same image works across
deployments and your data survives docker rm / podman rm / image rebuilds.
Docker and Podman are interchangeable here — Podman's CLI is Docker-compatible, so
every docker … command below works as podman …. Where the two genuinely differ
(rootless volume permissions, SELinux labels, the host alias), it's called out inline and
summarised in Podman notes. If you like, alias it once: alias docker=podman.
The container uses the project's own backend/config.yml and backend/data/ — the
exact same files you'd edit to run Phlox directly on the host. There is no separate
container config.
# from the repo root
cp backend/config.yml.example backend/config.yml # if you don't already have one
$EDITOR backend/config.yml # set provider, auth, and sandbox (see below)
mkdir -p backend/data
# Linux Docker only, if the bind mount is not writable:
sudo chown -R 10001:10001 backend/data
export PHLOX_JWT_SECRET=$(openssl rand -hex 32) # keep this stable across restarts
docker compose up -d --build
docker compose logs -f phlox # watch it bootBefore starting, choose the sandbox posture in config.yml: shared/authenticated
production requires runner: agentcore in this container image; a deliberately trusted
single-user container may use runner: local only with auth.enabled: false. The default
auth-enabled + local combination is rejected at production startup.
Podman: use podman compose (Podman 4.x+, or install podman-compose) — same file,
same commands (podman compose up -d --build).
On rootless Podman, prepare the bind mount with
podman unshare chown -R 10001:10001 backend/data. See Podman notes for SELinux labeling. The image always runs as non-root uid/gid10001:10001.
Open http://localhost:8000. On a clean database, docker compose logs phlox shows the
random one-time administrator password; the first login must replace it before continuing.
Treat that first-run output as sensitive.
backend/config.ymlmust exist before you start (the compose file bind-mounts it). If it's missing, create it fromconfig.yml.examplefirst — otherwise the container boots with no provider profiles.
The image holds only code. Everything that changes per-deployment or must persist is the
project's own backend/ files, bind-mounted into the container:
| Thing | Location (host → container) | In image? | Why |
|---|---|---|---|
| App code + built SPA | baked into image | ✅ yes | immutable, rebuilt per release |
config.yml |
backend/config.yml → /app/backend/config.yml |
❌ no (mounted) | environment-specific endpoints + secrets; editable without a rebuild |
SQLite DB (phlox.db) |
backend/data/ → /app/backend/data |
❌ no (mounted) | your data must persist |
Embedded Qdrant (backend/data/qdrant) |
backend/data/ → /app/backend/data |
❌ no (mounted) | vector index persists |
| workspaces / uploads / attachments | backend/data/ → /app/backend/data |
❌ no (mounted) | agent files + uploads persist |
The container deliberately mounts the project's own backend/config.yml (and
backend/data/) rather than introducing a separate container-only config. So there's a
single source of truth: the same file works whether you run Phlox in a container or directly
on the host. The image's defaults (PHLOX_CONFIG=/app/backend/config.yml,
PHLOX_DATA=/app/backend/data) already match the app's native paths, so the bind mounts line
up with no env-var overrides.
The one value that depends on how you run it is the provider endpoint: use
host.docker.internal when Phlox is containerized, or localhost when running directly on
the host (see the next section). Secrets stay out of the image because the file is mounted,
not baked in.
Don't run a host instance and the container against this same
backend/data/at the same time — the SQLite DB and embedded Qdrant lock to a single process. Run one at a time (the app is single-process by design), or give one of them a separate data dir.
Inside a container, localhost is the container, not your host. If Ollama/LM Studio
runs on the host, point Phlox at host.docker.internal instead of localhost in
config.yml:
default_profile: local-ollama
profiles:
local-ollama:
type: openai
label: "Ollama (host)"
endpoint: http://host.docker.internal:11434/v1 # NOT localhost
api_key: ollama
model: qwen3:8b
supports_tools: true- Docker Desktop (macOS/Windows):
host.docker.internalresolves automatically. - Docker on Linux: the compose file adds
extra_hosts: ["host.docker.internal:host-gateway"], which makes it resolve. (With plaindocker run, add--add-host=host.docker.internal:host-gateway.) - Podman: also honours the
host.docker.internal:host-gatewaymapping in the compose file. In addition, Podman provideshost.containers.internalout of the box (noextra_hostsneeded) — you can use either hostname inconfig.yml. - Alternative on Linux only:
--network host(--network=hostforpodman run) lets the container use the host's network stack solocalhost:11434works directly (not supported on Docker Desktop).
Also make sure the server listens on all interfaces, not just loopback — e.g. run Ollama with
OLLAMA_HOST=0.0.0.0.
⚠️ Editingconfig.ymlmay not be enough — the database overlay wins. Once a provider profile has been saved or edited in the app's Settings → (Admin) Configuration panel, Phlox stores it in the database, and that overlay takes precedence overconfig.yml.config.ymlis only the seed for a fresh database. So if you change theendpointinconfig.ymlbut the app keeps hitting the old address (symptom: "Model error: Connection error." because it's still dialinglocalhostfrom inside the container), fix it in Settings → (Admin) Configuration → provider profiles instead — that applies live, no restart. This catches almost everyone who containerizes an existing Phlox instance.
Identical flags for both — swap docker for podman. On rootless Podman with SELinux
(Fedora/RHEL/CentOS), append :Z to the bind mounts so the container can read/write them
(it relabels the host dirs). Harmless to include elsewhere; omit on systems without SELinux.
docker build -t phlox:latest . # or: podman build -t phlox:latest .
# Config must exist on the host first (it's bind-mounted, not seeded):
cp backend/config.yml.example backend/config.yml # if you don't have one
$EDITOR backend/config.yml # endpoint -> host.docker.internal, set auth
docker run -d --name phlox \
-p 8000:8000 \
-v "$PWD/backend/config.yml:/app/backend/config.yml" \
-v "$PWD/backend/data:/app/backend/data" \
-e PHLOX_JWT_SECRET="$(openssl rand -hex 32)" \
--add-host=host.docker.internal:host-gateway \
phlox:latestPodman equivalent (note :Z on the mounts for SELinux hosts):
podman run -d --name phlox \
-p 8000:8000 \
-v "$PWD/backend/config.yml:/app/backend/config.yml:Z" \
-v "$PWD/backend/data:/app/backend/data:Z" \
-e PHLOX_JWT_SECRET="$(openssl rand -hex 32)" \
--add-host=host.docker.internal:host-gateway \
phlox:latest| Variable | Default (in image) | Purpose |
|---|---|---|
PHLOX_JWT_SECRET |
(required) | Strong, stable 32B+ secret; production startup fails if it is missing or a placeholder |
SEARXNG_URL |
— | Optional: use SearXNG for web search instead of the default ddgs |
PHLOX_CONFIG |
/app/backend/config.yml |
Config path. Left at default so the mounted backend/config.yml is used — override only for an unusual layout |
PHLOX_DATA |
/app/backend/data |
Data root. Left at default so the mounted backend/data/ is used — override only for an unusual layout |
DATABASE_URL |
— (SQLite under PHLOX_DATA) |
Set to deploy against Postgres instead, e.g. postgresql+psycopg://user:pass@host:5432/phlox — see below |
Embedded Qdrant locks its on-disk directory to a single process, which is fine for one single-container instance. To move to a Qdrant server (e.g. before scaling out):
docker compose --profile with-qdrant up -d --buildThen in config.yml:
vector_store:
url: "http://qdrant:6333" # compose service DNS name; no host networking needed
collection: doc_chunksSQLite remains the source of truth, so you can rebuild the index after switching
(reindex_all). vector_store is a bootstrap setting — change it in the file and restart.
Phlox defaults to a SQLite file under backend/data/ — nothing to configure. To point it
at Postgres instead, set DATABASE_URL. The image already bundles the psycopg driver, so
no rebuild is needed to switch.
A postgres service is included behind a profile if you want Compose to run it for you:
export DATABASE_URL="postgresql+psycopg://phlox:phlox@postgres:5432/phlox"
docker compose --profile with-postgres up -d --buildPhlox connects via the service name postgres:5432 (Docker's internal DNS) — no
localhost involved. A Postgres running on the host instead would use
host.docker.internal:5432 (DATABASE_URL=postgresql+psycopg://user:pass@host.docker.internal:5432/phlox),
exactly like the local-LLM case above. Running Phlox directly on the host (no container)
works the same way — just export DATABASE_URL before starting uvicorn, pointed at
localhost:5432 instead.
Tables and any schema updates are created/applied automatically at startup — no separate migration step. Switching backends does not migrate existing data between them; pick one at deployment time (or migrate data out-of-band if you need to move later).
The application container is a useful process boundary, but it is not a safe per-user code
sandbox: model-executed local code shares the Phlox process environment, mounted database,
configuration, provider credentials, and every conversation workspace. Therefore an
auth-enabled production container refuses sandbox.runner: local.
| Deployment | Sandbox mode | What isolates the code |
|---|---|---|
| Dockerized, trusted single-user | local with auth.enabled: false, or agentcore |
Local trusts the whole app container; AgentCore executes off-host |
| Dockerized, shared/authenticated | agentcore |
Off-host managed microVM; no application secrets or mounts are exposed |
| On the host (run directly) | container or agentcore |
Ephemeral resource-limited container, or off-host microVM |
For a deliberately trusted, unauthenticated single-user container, local runs inside the
non-root Phlox container; execute_python and run_shell work out of the box. This posture
does not protect the mounted Phlox data or secrets from model-executed code.
sandbox.runner: container is not supported inside a container and is intentionally
left undocumented for that case: it would require Phlox to drive the host's container engine
over a mounted socket and keep workspace bind-mount paths identical inside the container
and on the host — fragile, and it silently mounts the wrong directory if you get it wrong.
If you set runner: container in a dockerized instance without a usable engine, Phlox fails
startup rather than silently running locally.
For shared use, configure
agentcorein the container, or run Phlox directly on the host withsandbox.runner: container. See SANDBOX.md. Readiness returns503when configured isolation is unavailable.
Swap docker for podman throughout.
docker compose ps # status
docker compose logs -f phlox # logs
docker compose restart phlox # restart (e.g. after editing config.yml)
docker compose down # stop & remove container (backend/config.yml + backend/data persist)
docker compose up -d --build # rebuild after pulling new code
# Back up everything: just copy the host files
tar czf phlox-backup.tgz backend/config.yml backend/dataPodman is a drop-in for the commands above; these are the only real differences.
- Compose provider:
podman composeships with Podman 4.x+. On older versions installpodman-compose(pip install podman-compose) and runpodman-compose …. Thedocker-compose.ymlhere needs no changes. - SELinux bind mounts (Fedora/RHEL/CentOS): add
:Zto volumes so the container can access them, e.g.-v "$PWD/backend/data:/app/backend/data:Z". With Compose, append it in the file (./backend/data:/app/backend/data:Z) or disable labeling per-service withsecurity_opt: ["label=disable"]. - Rootless volume ownership: rootless Podman maps container UID 0 to your host user via a
subordinate-UID range, so files in
backend/data/may show as owned by a high UID on the host. Options: add:Uto a mount to have Podman chown it into the namespace (-v "$PWD/backend/data:/app/backend/data:U"), or run with--userns=keep-idso the container user maps back to your host UID. Plain reads/writes work fine either way. - Host alias: use Podman's built-in
host.containers.internal(noextra_hostsneeded), or keephost.docker.internalvia thehost-gatewaymapping already in the compose file — both resolve to the host. - Rootless port binding: publishing
8000works rootless. To bind a privileged port (<1024) rootless, lowernet.ipv4.ip_unprivileged_port_startor run via a reverse proxy.
curl http://localhost:8000/api/health # -> {"status":"ok","tools":N}