Release line: v3.x — latest tag v3.2.2. Production (gateway.trespies.dev, Hetzner) runs v3.2.2+dynmodels.
Runtime: a single Go binary (agentic-gateway) listening on port 7340 by default.
See README.md for the architecture overview and ARCHITECTURE.md for system design.
# 1. Clone and enter the repo
git clone https://github.com/DojoGenesis/gateway.git
cd gateway
# 2. Configure — copy the example env and add provider API keys
cp .env.example .env
# 3. Build the binary (outputs to bin/agentic-gateway)
make build
# 4. Run it
./bin/agentic-gateway # listening on :7340Or run without building: go run main.go.
Verify it is live:
curl http://localhost:7340/health# Build the image (multi-stage: Go 1.25 Alpine builder → distroless runtime, non-root UID 65534)
docker build -t agentic-gateway .
# Run it
docker run -p 7340:7340 --env-file .env agentic-gatewayThe image EXPOSEs 7340 and ships a self-contained health probe (agentic-gateway --health-check) so no curl/wget is needed inside the distroless container.
For a TLS-terminated production deployment on Hetzner (Caddy + systemd), see
VPS Production Deployment below and the dedicated guide at
deploy/README.md.
Configuration is layered, lowest precedence first:
.env— loaded on startup if present (existing environment variables are not overridden)- The YAML config file — see below
- Process environment variables — highest precedence
The gateway picks exactly one, in this order:
| Source | Missing file behaviour |
|---|---|
-config /path/to/config.yaml |
Startup fails (exit 1) |
CONFIG_PATH environment variable |
Startup fails (exit 1) |
config/config.yaml, relative to the working directory |
Ignored — normal for a deployment that names no file |
-configwas ignored until v3.3.2. The binary parsed no flags: it checkedos.Args[1]for--health-checkand--versionand discarded everything else. The systemd unit has always rundojo-gateway -config /etc/dojo/config.yaml, so that file was never opened and every setting in it was a no-op. Naming a path that cannot be read is now a startup failure rather than a silent fallback to defaults.
MCP_CONFIG_PATH is unrelated — it points at gateway-config.yaml for the MCP host.
Check a host's config before restarting it. --check-config loads the file,
prints exactly what it will and will not apply, and exits without binding a
port, opening a database or contacting a provider:
/usr/local/bin/dojo-gateway -config /etc/dojo/config.yaml --check-config
# exit 0 = the gateway will start on this file; exit 1 = it will notUnknown keys, and values whose shape does not fit, are reported by name and line at every startup and then ignored:
level=WARN msg=configuration detail="/etc/dojo/config.yaml: line 3: field data_dir not found in type config.Config — this key is not a gateway setting and has NO effect"
They are warnings rather than errors on purpose: several deployed config files carry keys that have never been parsed, and refusing to boot on them would take a working host down. A file that is not valid YAML at all is fatal — none of it can be applied, so pretending otherwise is what caused this class of bug. Values are redacted from these messages; only key names and line numbers appear.
Set the corresponding key in .env to enable a provider. Providers without a key are silently skipped at startup — no error, they just do not register.
| Provider | Env var |
|---|---|
| Anthropic (Claude) | ANTHROPIC_API_KEY |
| OpenAI | OPENAI_API_KEY (+ optional OPENAI_BASE_URL) |
| Google (Gemini) | GOOGLE_API_KEY |
| Groq | GROQ_API_KEY |
| Mistral | MISTRAL_API_KEY |
| DeepSeek | DEEPSEEK_API_KEY (+ optional DEEPSEEK_BASE_URL) |
| Kimi (Moonshot) | KIMI_API_KEY (+ optional KIMI_BASE_URL) |
| Ollama | OLLAMA_HOST (auto-detected on localhost:11434) |
The full set of variables is documented in .env.example.
| Variable | Default | Purpose |
|---|---|---|
PORT |
7340 |
HTTP listen port |
ENVIRONMENT |
development |
production enables structured/JSON logging and arms the JWT secret startup gate |
JWT_SECRET |
(dev fallback) | Signing secret for every bearer token. Required in production — see below |
ALLOWED_ORIGINS |
(none) | Comma-separated CORS origins (each must include scheme) |
MEMORY_DB_PATH |
~/.dojo/memory.db |
Conversation-memory SQLite path — use an absolute path |
DOJO_CAS_PATH |
~/.dojo/skills.db |
Content-addressable skill/workflow store |
AUTH_DB_DIR |
.dojo/ (CWD-relative) |
Auth DB directory — set absolute for deploys |
MCP_CONFIG_PATH |
gateway-config.yaml |
MCP host configuration file |
MCP_APPS_ENABLED |
false |
Enable the MCP Apps bridge |
REGISTRATION_ENABLED |
true |
Public sign-up on POST /auth/register. The only way to close it — see below |
Relative DB paths bite. A relative
MEMORY_DB_PATHorDOJO_CAS_PATHsilently creates a new empty database whenever the working directory changes (e.g. across restarts). Always use absolute paths in any non-local deployment.
The gateway signs and verifies every bearer token — human sessions and machine service tokens alike — with one HMAC secret.
JWT_SECRET is the only name that configures it.
JWT_SECRET=$(openssl rand -hex 32)| Source | Read by the gateway? |
|---|---|
JWT_SECRET env var |
Yes — this is the one. |
DOJO_JWT_SECRET env var |
Deprecated alias. Honoured as a fallback so older hosts are not left unconfigured; JWT_SECRET wins when both are set. Rename it. |
jwt_secret: in a config YAML |
No. Not wired to anything. Removed from deploy/gateway-config.yaml. |
If neither variable is set, the gateway falls back to a built-in development secret that is committed to this repository — anyone who has read the source could forge a token, including one carrying role: admin.
With ENVIRONMENT=production, that is now a hard startup failure: the gateway refuses to start and names the variable to set. Development is unaffected — local runs and tests keep working with no configuration.
Startup logs the name of the variable the secret came from and whether it is the built-in default. The secret value is never logged.
gateway-config.yaml controls runtime behaviour (feature flags, MCP servers, routing). Example feature block:
features:
tool_calling: true # agentic tool-calling loop
get_document_tool: true # document fetch endpoint
patch_intent: true # extract patch intents from responses
provider_key_management: true # accept API keys via settings endpoint
ollama_tool_fallback: true # text-mode fallback for OllamaThe gateway supports OpenTelemetry trace export and Langfuse:
OTEL_ENABLED=true
OTEL_EXPORTER_OTLP_ENDPOINT=otel-collector:4317
OTEL_SERVICE_NAME=agentic-gateway
Use docker-compose.example.yml for a full local observability stack (gateway + OTEL Collector + Langfuse + PostgreSQL).
curl http://localhost:7340/healthExample response:
{
"status": "healthy",
"version": "1.1.0",
"timestamp": "2026-07-19T12:00:00Z",
"providers": { "anthropic": "healthy" },
"dependencies": {
"memory_store": "healthy",
"tool_registry": "healthy",
"orchestration_engine": "healthy"
},
"uptime_seconds": 0,
"requests_processed": 0
}status is degraded if any registered provider fails its info probe. The version field is the server module version injected at build time via
-ldflags "-X github.com/DojoGenesis/gateway/server.Version=<version>"; an un-injected build reports the source default (1.1.0).
curl http://localhost:7340/metrics # Prometheus-style, unauthenticated
curl http://localhost:7340/admin/metrics/prometheus # admin surface (requires admin auth)Releases are cut as git tags vMAJOR.MINOR.PATCH (latest: v3.2.2). A running build may carry semver build metadata after a + — e.g. production runs v3.2.2+dynmodels.
Production runs on Hetzner behind Caddy (automatic Let's Encrypt TLS) with the Gateway managed by systemd. The full step-by-step guide, including the GitHub OAuth app setup and secret placement, is in deploy/README.md. Summary:
# On the server (Ubuntu 24.04), from the repo's deploy/ directory:
sudo bash deploy/provision.sh --dry-run # preview every step
sudo bash deploy/provision.sh # idempotent — safe to re-runprovision.sh installs Caddy, creates the unprivileged dojo system user, downloads the release binary to /usr/local/bin/dojo-gateway, installs the config + systemd unit, and starts the services.
Layout on the server:
| Path | Purpose |
|---|---|
/usr/local/bin/dojo-gateway |
The Gateway binary (release tarball agentic-gateway_<version>_linux_amd64.tar.gz) |
/etc/dojo/config.yaml |
Gateway config (deploy/gateway-config.yaml template) |
/etc/dojo/env |
Secrets — JWT_SECRET, provider keys, GitHub OAuth creds (chmod 640, root:dojo) |
/var/lib/dojo |
Data dir (memory + CAS SQLite) |
/etc/caddy/Caddyfile |
TLS reverse proxy → localhost:7340 (deploy/Caddyfile) |
dojo-gateway.service |
systemd unit (deploy/gateway.service) |
Service management:
systemctl status dojo-gateway
journalctl -u dojo-gateway -f # Gateway logs
journalctl -u caddy -f # TLS / proxy logs
curl https://gateway.trespies.dev/healthVersion pin:
deploy/provision.shsetsGATEWAY_VERSIONand only re-downloads when the installed binary's version differs. Bump that variable and re-run to upgrade.
- Generate a strong JWT secret:
openssl rand -hex 32→JWT_SECRETin/etc/dojo/env. WithENVIRONMENT=productionthe gateway refuses to start without it (see JWT signing secret).DOJO_JWT_SECRETis a deprecated alias — rename any host still using it. - Public registration is open by default and that is deliberate on this deployment — anyone may sign up and use the chat.
POST /auth/registerissues arole=userJWT that can reach/v1, including the completion endpoints that spendANTHROPIC_API_KEY, so keep the budget limits meaningful. The gateway states the setting on every boot (user registration is OPEN|CLOSED). - To close registration, set
REGISTRATION_ENABLED=falsein/etc/dojo/env.registration_enabled:in the YAML file can only open it: deployed copies of that file still carryregistration_enabled: falsefrom when no code parsed the key, and a restart must not close public sign-up on the strength of a line nobody knew was live. A file sayingfalseis reported at startup and not applied. (Remove that asymmetry —applyRegistrationFileValueinserver/config/config.go— once no host carries a stalefalse.) - The Docker image already runs as non-root (UID 65534) on a distroless base; the systemd unit adds
NoNewPrivileges,ProtectSystem=strict, andProtectHome. - Caddy sets HSTS,
X-Content-Type-Options,X-Frame-Options, andReferrer-Policy; keep only ports 80/443 open in the firewall.
Store provider keys and JWT_SECRET in /etc/dojo/env (systemd EnvironmentFile), not in the tracked YAML — the YAML is not read for secrets at all. For orchestrated deployments use the platform's secret store (Kubernetes secrets, cloud secret managers) rather than baking keys into images.
Providers without an API key are skipped silently at startup. Confirm the key is present in the process environment (.env is only read if it exists in the working directory) and re-check /health → providers.
ENVIRONMENT=production and no signing secret was found. Set JWT_SECRET in /etc/dojo/env and restart:
echo "JWT_SECRET=$(openssl rand -hex 32)" >> /etc/dojo/env
systemctl restart dojo-gatewayThis is deliberate. Before the gate existed, the gateway started anyway and signed every session with a secret published in this repository. If the host previously used DOJO_JWT_SECRET, it still works, but rename it to JWT_SECRET.
Changing the secret invalidates every existing session and service token. Rotate it during a maintenance window, and re-mint service tokens (
make service-token SERVICE=<name>) afterwards.
The default port is 7340. Override with PORT=<port> (the --health-check probe reads the same variable, so it follows the override).
Caddy reverse-proxies to localhost:7340. If Caddy returns 502, the Gateway process is down or bound to a different port — check journalctl -u dojo-gateway and confirm the PORT/config match the Caddyfile upstream.
Almost always a relative DB path. Set MEMORY_DB_PATH and DOJO_CAS_PATH to absolute paths (the systemd unit already points them at /var/lib/dojo).
README.md— architecture, providers, API routes, key commandsARCHITECTURE.md— system design deep-divedeploy/README.md— full VPS provisioning walkthrough.env.example— every environment variable, documentedCHANGELOG.md— release history
Questions? Open an issue on GitHub or consult the documentation above.