SDKs
Deployment
Docker, Fly.io, and production deployment
Local Development (Docker Compose)
The docker-compose.yml provides PostgreSQL and Redis for local development:
docker compose up -d # start in background
docker compose down # stop
docker compose down -v # stop + delete volumes (full reset)Services:
| Service | Image | Port | Purpose |
|---|---|---|---|
postgres | postgres:16-alpine | 127.0.0.1:5432 | Spend logs, wallet budgets |
redis | redis:7-alpine | 127.0.0.1:6379 | Response cache, replay protection |
Warning
Default credentials (solvela / solvela_dev_password) are for local development only. Never use in production.
Ports are bound to 127.0.0.1 only -- not accessible from the network.
Redis is configured with 256MB LRU eviction and no RDB persistence (the cache is ephemeral).
Migrations in migrations/ are mounted into the PostgreSQL container and run automatically on first start.
Docker Build
The Dockerfile uses a 2-stage build. Dependency caching is achieved by copying workspace manifests first and compiling against dummy source files, then bringing in the real source for the final compile:
# Stage 1: Build
FROM rust:1.88-slim-bookworm AS builder
RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy workspace manifests first for dependency caching
COPY Cargo.toml Cargo.lock ./
COPY crates/protocol/Cargo.toml crates/protocol/Cargo.toml
COPY crates/x402/Cargo.toml crates/x402/Cargo.toml
COPY crates/router/Cargo.toml crates/router/Cargo.toml
COPY crates/gateway/Cargo.toml crates/gateway/Cargo.toml
COPY crates/cli/Cargo.toml crates/cli/Cargo.toml
# Compile against dummy sources so deps land in the cache layer
RUN mkdir -p crates/protocol/src crates/x402/src crates/router/src crates/gateway/src crates/cli/src && \
echo "pub fn _dummy() {}" > crates/protocol/src/lib.rs && \
echo "pub fn _dummy() {}" > crates/x402/src/lib.rs && \
echo "pub fn _dummy() {}" > crates/router/src/lib.rs && \
echo "pub fn _dummy() {}" > crates/gateway/src/lib.rs && \
echo "fn main() {}" > crates/gateway/src/main.rs && \
echo "pub fn _dummy() {}" > crates/cli/src/lib.rs && \
echo "fn main() {}" > crates/cli/src/main.rs
RUN cargo build --release --bin solvela-gateway 2>/dev/null || true
# Copy real source + config + migrations, then rebuild
COPY crates/ crates/
COPY config/ config/
COPY migrations/ migrations/
RUN touch crates/protocol/src/lib.rs crates/x402/src/lib.rs crates/router/src/lib.rs crates/gateway/src/lib.rs crates/gateway/src/main.rs
RUN cargo build --release --bin solvela-gateway
# Stage 2: Runtime
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
# Create non-root runtime user (no home, no login shell)
RUN useradd --uid 1001 --no-create-home --shell /usr/sbin/nologin solvela
WORKDIR /app
COPY --from=builder --chown=solvela:solvela /app/target/release/solvela-gateway .
COPY --from=builder --chown=solvela:solvela /app/config/ config/
USER solvela
EXPOSE 8402
CMD ["./solvela-gateway"]Key points:
- Dependency caching is layer-based: workspace
Cargo.tomls + dummy sources compile first; the real source is copied in a later layer so unchanged dependencies don't rebuild. - Migration SQL files in
migrations/are read at compile time bysqlx::migrate!, so they must be present in the build context. - Runtime image is
debian:bookworm-slimwith onlyca-certificatesinstalled. - Exposes port 8402.
- Runs as non-root user
solvela(UID 1001). Files copied from the build stage use--chown=solvela:solvelaso the binary and config are owned by the runtime user.
Build the image:
docker build -t solvela .Run:
docker run -p 8402:8402 \
-e OPENAI_API_KEY=sk-... \
-e SOLVELA_SOLANA__RPC_URL=https://api.devnet.solana.com \
-e SOLVELA_SOLANA__RECIPIENT_WALLET=your-wallet \
solvelaThe canonical env-var separator is double underscore (Fly.io convention; see crates/gateway/src/main.rs:70). The legacy single-underscore form (SOLVELA_SOLANA_RPC_URL) is also accepted as a fallback so existing deployments keep working.
Fly.io Deployment
The gateway is configured for Fly.io with fly.toml:
app = "solvela-gateway"
primary_region = "ord"
[build]
dockerfile = "Dockerfile"
[env]
PORT = "8402"
RUST_LOG = "gateway=info,tower_http=info"
[http_service]
internal_port = 8402
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 1
[[http_service.checks]]
grace_period = "10s"
interval = "15s"
method = "GET"
path = "/health"
protocol = "http"
timeout = "5s"
[vm]
size = "shared-cpu-1x"
memory = "512mb"Deploy
# First time
fly launch
# Subsequent deploys
fly deploySet Secrets
fly secrets set OPENAI_API_KEY=sk-...
fly secrets set ANTHROPIC_API_KEY=sk-ant-...
fly secrets set SOLVELA_SOLANA__RPC_URL=https://api.mainnet-beta.solana.com
fly secrets set SOLVELA_SOLANA__RECIPIENT_WALLET=your-wallet-pubkey
fly secrets set SOLVELA_SOLANA__FEE_PAYER_KEY=your-fee-payer-key
fly secrets set SOLVELA_ADMIN_TOKEN=your-admin-token
fly secrets set DATABASE_URL=postgres://...
fly secrets set REDIS_URL=redis://...
fly secrets set SOLVELA_SESSION_SECRET=your-session-secretHealth Checks
Fly.io probes GET /health every 15 seconds with a 5-second timeout and 10-second grace period on deploy.
Production Checklist
Before deploying to production:
- Set
SOLVELA_ENV=production(disables localhost CORS origins) - Set
SOLVELA_ADMIN_TOKEN(protects/metrics,/v1/escrow/health, and the org-management endpoints) - Set
SOLVELA_SESSION_SECRETto a stable value (auto-generated secret changes on restart) - Set
SOLVELA_SOLANA__RPC_URLto a mainnet-beta endpoint - Configure
DATABASE_URLwith production PostgreSQL credentials - Configure
REDIS_URLwith production Redis - Set
SOLVELA_CORS_ORIGINSto your dashboard domain - Verify all provider API keys are set
- Set
SOLVELA_SOLANA__RECIPIENT_WALLETto your production wallet - Configure fee payer keys if using escrow (
SOLVELA_SOLANA__FEE_PAYER_KEYplus optional_2through_8for rotation) - Run
cargo testandcargo clippybefore deploying - Review
RUST_LOGlevel (avoiddebugin production)