Docker & Deployment
The Nectar keeper is a single static Go binary. It is stateless — every cycle reads its state from chain, so restarts are always safe and there are no volumes to manage. This page covers the three supported ways to run it: locally with docker-compose, on Railway (the path the reference keeper-alpha / keeper-beta operators use), and the one-command packaging planned for Tranche 3.
New to keepers? Start with Keeper Setup to generate a keypair and stake, then come back here to containerize it. Every environment variable is documented in the Configuration Reference.
The image
There is no pre-published image in Tranche 2 — you build from the repo's keeper/Dockerfile. It is a two-stage build that produces a ~15 MB Alpine image with a static binary and CA certificates:
FROM golang:1.24-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o keeper .
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/keeper .
ENTRYPOINT ["./keeper"]
The binary serves an HTTP API (state, SSE event stream, performance, Prometheus metrics, and a health check) on API_PORT (default 8080). The container exposes nothing else.
Publishing a tagged multi-arch image to a public registry is part of the Tranche 3 one-command packaging work. Until then, build locally or let Railway build from the Dockerfile.
Run with docker-compose
The repo ships a docker-compose.yml at its root that builds and runs the two reference keepers (keeper-alpha on port 8080, keeper-beta on port 8081) plus the frontend (port 3000). All three pull shared values from a root .env file via env_file: .env.
1. Create your .env
Copy the template and fill in your addresses and secrets:
cp .env.example .env
The compose file reads the two keeper secrets from KEEPER_A_SECRET and KEEPER_B_SECRET (each container's KEEPER_NAME and API_PORT are set inline in the compose file). A minimal .env for the current testnet deployment looks like:
# Keeper secrets — one per container (keep these private)
KEEPER_A_SECRET=S...
KEEPER_B_SECRET=S...
# Current testnet (Tranche 1 hardened, 2026-05-24)
REGISTRY_CONTRACT=CDT257SL2IYDZJIDXEVKI67MYLCKE73JY6WGUTGZOEFXJHG26FJHJDRB
VAULT_CONTRACT=CDZR6VDCPQFOFFKKZ2KMVB67Z54LI5OY73NHBFVI6DR6RE6TL7NN7345
USDC_CONTRACT=CD34YC6FFI2KIE2U4ZPCGQIRPH7UPG5YY2QBYNP25ATSFOQSG73J4VBW
BLEND_POOL=CCEBVDYM32YNYCVNRXQKDFFPISJJCV557CDZEIRBEE4NCV4KHPQ44HGF
# DEX (collateral → USDC after fills); Soroswap primary, Phoenix optional
SOROSWAP_ROUTER=CCJUD55AG6W5HAI5LRVNKAE5WDP5XGZBUDS5WNTIVDU7O264UZZE7BRD
# Network (testnet defaults — override only if needed)
SOROBAN_RPC=https://soroban-testnet.stellar.org:443
HORIZON_URL=https://horizon-testnet.stellar.org
NETWORK_PASSPHRASE=Test SDF Network ; September 2015
# Tuning
POLL_INTERVAL=10
MIN_PROFIT=1.02
Always confirm these against the Contract Addresses page — the registry/vault/USDC IDs are redeployed between tranches, and pointing at a deprecated set will silently fail to register or draw.
.env holds your keeper secret keys. Lock it down and never commit it:
chmod 600 .env
It is already in .gitignore. Set permissions before you populate it.
2. Bring up a keeper
Both keepers, the frontend, and rebuilds:
docker compose up --build
Just one keeper (matches docker-compose up keeper in the project layout — the real service name is keeper-alpha):
docker compose up keeper-alpha
Detached, with logs followed afterward:
docker compose up -d keeper-alpha
docker compose logs -f keeper-alpha
On startup a keeper attempts to register itself in the KeeperRegistry (idempotent — an AlreadyRegistered result is treated as success), starts the HTTP API, and begins polling the Blend pool every POLL_INTERVAL seconds. See Keeper Setup for the log lines you should expect.
3. Health and observability
Each keeper container declares a healthcheck that hits /healthz. The endpoint returns a bare 200 OK (empty body) whenever the API server is up:
curl -i http://localhost:8080/healthz
# HTTP/1.1 200 OK
The same server exposes the live operational endpoints (consumed by the dashboard):
| Endpoint | Purpose |
|---|---|
GET /healthz | Liveness probe — 200 OK, empty body |
GET /metrics | Prometheus metrics (cycles, liquidations, etc.) |
GET /api/state | Current pool positions + health factors (JSON) |
GET /api/performance | Vault state, depositors, keeper stats, liquidation history (JSON) |
GET /api/events | Server-Sent Events log stream (capped at 100 clients) |
Compose also configures JSON-file logging with rotation (max-size: 50m, max-file: 3) and restart: unless-stopped on every service.
A changed .env is not hot-reloaded. After editing it, recreate the container — docker compose up -d --force-recreate keeper-alpha — rather than relying on a plain up.
Build from source without compose
If you only want the container by itself:
git clone https://github.com/Nectar-Network/nectar.git
cd nectar/keeper
docker build -t nectar-keeper:local .
docker run -d \
--name nectar-keeper \
--restart unless-stopped \
-p 8080:8080 \
--env-file ../.env \
-e KEEPER_SECRET="$KEEPER_A_SECRET" \
-e KEEPER_NAME="my-keeper" \
nectar-keeper:local
Note that a standalone container needs KEEPER_SECRET (not KEEPER_A_SECRET) — the A/B split is a compose convention so two containers can share one .env. See the Configuration Reference for the full variable list and validation rules (e.g. POLL_INTERVAL must be 3–300, SLIPPAGE_BPS 0–10000).
Deploy to Railway
The reference operators (keeper-alpha, keeper-beta) run on Railway, one service per keeper. The repo's keeper/railway.toml tells Railway to build from the Dockerfile and run the binary:
[build]
builder = "DOCKERFILE"
dockerfilePath = "Dockerfile"
[deploy]
startCommand = "./keeper"
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 5
healthcheckPath = "/healthz"
healthcheckTimeout = 30
Railway uses the /healthz probe to gate deploys and the ON_FAILURE restart policy (max 5 retries) to recover crashes. Because the keeper is stateless, a restart re-reads everything from chain.
1. Link the service and push
From the keeper/ directory (it contains both the Dockerfile and railway.toml):
cd keeper
railway link # pick the keeper-alpha (or keeper-beta) service
railway up
railway up uploads the build context and Railway builds the image from the Dockerfile.
2. Set environment variables
Set the public (non-secret) variables with the helper script, which writes them to the linked service:
./scripts/railway-keeper-env.sh keeper-alpha # or keeper-beta
This sets KEEPER_NAME, REGISTRY_CONTRACT, VAULT_CONTRACT, USDC_CONTRACT, SOROBAN_RPC, HORIZON_URL, POLL_INTERVAL, MIN_PROFIT, and API_PORT. BLEND_POOL is set only when it is non-empty in your shell (the Railway CLI rejects empty values), so export it first or set it later:
BLEND_POOL=CCEBVDYM32YNYCVNRXQKDFFPISJJCV557CDZEIRBEE4NCV4KHPQ44HGF \
./scripts/railway-keeper-env.sh keeper-alpha
# or afterwards
railway variables --set BLEND_POOL=CCEBVDYM32YNYCVNRXQKDFFPISJJCV557CDZEIRBEE4NCV4KHPQ44HGF
The script deliberately does not set KEEPER_SECRET — CLI-set values land in your shell history. Add it in the Railway dashboard: Variables → New Variable → tick "Mark as secret". One distinct secret per service.
Confirm the contract addresses the script applies before you deploy. Older copies of railway-keeper-env.sh and railway.toml may carry deprecated registry/vault IDs from an earlier tranche. The authoritative current testnet set is on Contract Addresses; cross-check the values Railway ends up with via railway variables.
3. Verify
Each Railway service gets a public URL. Probe the API the same way you would locally:
curl -i https://keeper-alpha-production.up.railway.app/healthz
curl https://keeper-alpha-production.up.railway.app/api/performance
Within a poll interval your keeper appears on the dashboard leaderboard.
Without Docker
You don't need a container to run a keeper. A static binary under systemd (or any process manager) is fully supported and covered in Keeper Setup. Docker/Railway is simply the path the reference operators use and the one Tranche 3 packages.
Tranche 3: one-command packaging
Container packaging is hardened in Tranche 3. The goal is that a third-party operator can go from zero to a registered, running keeper with a single command, paired with the published keeper SDK. Planned deliverables:
- A tagged, multi-arch image published to a public registry so operators
docker runa pinned version instead of building from source. - A bootstrap script / compose profile that prompts for a secret, funds and stakes the keeper, and starts the container in one step.
- Hardened production defaults to accompany the mainnet deployment (Circle USDC, production registry/vault parameters) and the operator security work (rate limits, draw caps).
Until Tranche 3 ships, build from source — locally with docker compose up --build or on Railway via railway up — exactly as documented above. Nothing about the keeper's runtime contract changes; packaging only makes the build step disappear.
Common issues
- Container exits immediately — a required variable is missing. The keeper exits with
missing required env: <KEY>ifKEEPER_SECRET,REGISTRY_CONTRACT, orVAULT_CONTRACTis blank. Checkdocker compose logs keeper-alpha. - Out-of-range value —
POLL_INTERVAL(3–300),MIN_PROFIT(> 0),SLIPPAGE_BPS/DEFINDEX_DRIFT_BPS(0–10000) are validated at startup; a bad value prints to stderr and exits. - Registers but never fills — usually a deprecated
BLEND_POOLor contract address, or the keeper has no underwater positions to act on. WithBLEND_POOLempty the keeper runs in vault-monitor-only mode (API serves, no liquidation cycle). - Stale
.env— recreate the container after editing it; compose does not reload env on a plainup.
For everything else, see Troubleshooting.