Skip to content

Deploying the server

The server is one process that serves the API and the web UI on a single port. It needs somewhere to keep its data and, once you want it to be more than a laptop toy, a PostgreSQL database.

From a checkout of the repository:

Terminal window
docker compose -f docker/docker-compose.yml up -d

Server and web UI on port 3789, no external database, no config file. Data persists in a horsie-data Docker volume.

Then open http://localhost:3789, sign in, and add a provider and a model — see Models & providers. A fresh server has none, and no session can run a turn without one.

Deploy to Render

The button runs the published image from render.yaml with a managed Render PostgreSQL database wired in as HORSIE_DATABASE_URL. No volume is needed — both the settings store and the session journal live in that database.

To use a different PostgreSQL instance — Neon, Supabase, RDS, anything speaking postgres:// — edit HORSIE_DATABASE_URL on the service after deploy, or fork render.yaml and drop its databases: block. Any connection string works as-is, including one with ?sslmode=require.

Fly has no one-click button, so this is a handful of commands from a checkout:

Terminal window
fly launch --no-deploy # reads fly.toml, creates the app
fly postgres create
fly postgres attach --app <app-name> --variable-name HORSIE_DATABASE_URL
fly deploy

fly launch assigns its own app name — the app value in fly.toml is a placeholder it overwrites. --variable-name HORSIE_DATABASE_URL sets the secret under the name horsie already reads, so nothing needs renaming afterwards.

For an external database, skip the two fly postgres commands and set the string directly:

Terminal window
fly secrets set HORSIE_DATABASE_URL=postgres://user:password@host/horsie

All three paths above run ghcr.io/blossomstack/horsie:latest, which moves with the default branch.

Every build also publishes an immutable sha-<short> tag, and a release publishes <version> and v<version>. Pin to one of those if you want an upgrade to be a deliberate step rather than a docker compose pull or a redeploy.

Terminal window
docker build -f docker/horsie.Dockerfile --target server -t horsie-server:latest .

Or, with a recent Rust toolchain:

Terminal window
make build-server # ./target/release/horsie-server
make install-server # optional, into ~/.local/bin

Run the binary directly with:

Terminal window
horsie-server --addr 0.0.0.0:3789 --web clients/web/dist

--web is what makes one process serve the UI as well as the API. Without it the binary serves the API only.

The default is SQLite in the server’s data directory, and it needs no configuration at all. Point database.url at PostgreSQL when you would rather the database were the thing that gets backed up than a container’s volume:

{
"database": { "url": "postgres://user:password@host/horsie" }
}

HORSIE_DATABASE_URL sets the same thing and takes precedence. Migrations run at startup on either backend, and max_connections — 10 by default — sizes the pool that settings reads and journal writes share.

Session and agent history lives in an actor journal: the journal_* tables in the same database. There is nothing to configure and nothing separate to back up.

data_dir holds plugin artifacts and, with the default SQLite database, the settings database and the journal under <data_dir>/server/. This is the one to back up, and the one to mount a volume at in a container. A PostgreSQL deployment keeps only plugin artifacts here.

state_dir is ephemeral. Losing it across a restart is fine.

Every field and its default is in the Configuration reference.