Docker
Octopus is published as a multi-arch image at ghcr.io/xraph/octopus, built for
linux/amd64 and linux/arm64 on every release. The image is a small Debian-based runtime
containing only the octopus binary and its TLS/CA dependencies; it runs as a non-root user
(uid 1000) and its entrypoint is the octopus binary.
By default the container runs:
octopus serve --config /etc/octopus/config.yamlSo the one thing you must provide is a config file at /etc/octopus/config.yaml.
Run the image01
Write a config file
Create a config.yaml on the host. The only required section is gateway with a listen
address — bind to 0.0.0.0 inside the container, not 127.0.0.1, or the port will not be
reachable from outside:
gateway:
listen: "0.0.0.0:8080"
workers: 0 # 0 = one worker per CPU core
upstreams:
- name: user-service
lb_policy: round_robin
instances:
- id: user-1
host: 10.0.0.5
port: 8081
routes:
- path: /api/users/*
methods: [GET, POST, PUT, DELETE]
upstream: user-service
strip_prefix: /apiRun the container
Mount the config read-only and publish the listen port:
docker run --rm \
-p 8080:8080 \
-v "$(pwd)/config.yaml:/etc/octopus/config.yaml:ro" \
ghcr.io/xraph/octopus:latestPin a release tag (for example :0.1.0) instead of :latest for reproducible deployments.
Verify it is serving
The gateway exposes Kubernetes-style probes and Prometheus metrics on the same listen port:
curl -fsS http://localhost:8080/livez # liveness: 200 while the process is alive
curl -fsS http://localhost:8080/readyz # readiness: 200 only when ready to serve
curl -s http://localhost:8080/metrics # Prometheus text-format metricsMounting configuration02
The container expects a single file at /etc/octopus/config.yaml. Mount yours there. The image
also ships an annotated reference at /etc/octopus/config.example.yaml you can copy from.
To split a config across several files (a base plus environment overrides), point serve at a
directory — every *.yaml, *.yml, *.json, and *.toml file in it is merged in order:
docker run --rm \
-p 8080:8080 \
-v "$(pwd)/conf.d:/etc/octopus/conf.d:ro" \
ghcr.io/xraph/octopus:latest \
serve --config /etc/octopus/conf.dThe trailing serve --config ... overrides the image's default command. See
Configuration → loading and merging for merge semantics.
The container runs as uid 1000 with no privileges. Mount config read-only (:ro); the
process only needs to read it.
Environment-variable substitution03
Octopus expands ${VAR} and ${VAR:-default} in the config file at load time, so you can keep
secrets and per-environment values out of the file and inject them via the container
environment.
gateway:
listen: "0.0.0.0:${PORT:-8080}"
auth_providers:
internal-jwt:
type: jwt
secret: "${JWT_SECRET}" # required — fails to load if unset and no default
algorithm: "${JWT_ALG:-HS256}"docker run --rm \
-p 8080:8080 \
-e PORT=8080 \
-e JWT_SECRET="$JWT_SECRET" \
-v "$(pwd)/config.yaml:/etc/octopus/config.yaml:ro" \
ghcr.io/xraph/octopus:latestSet the log level with the RUST_LOG environment variable (for example RUST_LOG=info). See
Environment variable substitution for the full syntax.
Ports04
Octopus serves everything on one listener — proxied traffic, the /livez, /readyz, and
/startupz probes, and the /metrics endpoint all share the gateway's listen address
(default 0.0.0.0:8080). Publishing 8080 is all you need.
The image declares EXPOSE 8080 9090, and you may see older examples publish -p 9090:9090.
That second port is reserved for an admin surface — the manifests label it admin, and the
default observability.metrics.endpoint in the example config is 0.0.0.0:9090. Nothing
binds a second listener on 9090 today:
The admin dashboard the
9090port is named for is not fully wired yet.observability.metrics.endpointis inert — metrics are served at the fixed/metricspath on the gateway listen port regardless of that value (andobservability.metrics.enableddoes not gate it). See the metrics endpoint and the configuration implementation status.
So mapping 9090 is harmless but unnecessary; scrape /metrics on 8080.
| Port | What it is | What to do |
8080 | Gateway listener: traffic, probes, /metrics. | Publish it. The exact number follows gateway.listen. |
9090 | Reserved admin port (admin dashboard / inert metrics-endpoint default). | No active listener today; you can omit it. |
Health check05
The image defines a Docker HEALTHCHECK that polls http://127.0.0.1:8080/livez every 30s. If
you change gateway.listen to a different port, override or disable the built-in health check
(--health-cmd / --no-healthcheck) so it targets the right address.
Next steps06
Kubernetes deployment — the recommended way to run in a cluster.
Production checklist — before you go live.
Graceful shutdown — send SIGTERM, don't kill the container.
TLS configuration — terminate HTTPS on the gateway.