run — runtime modes and operational semantics
cyoda-go version 0.6.2
run — supported ways to start cyoda-go: binary, Docker, Docker Compose, Kubernetes (Helm), and development scripts.
SYNOPSIS
Section titled “SYNOPSIS”# Binary (default mode — no flags)cyoda
# Dockerdocker run --rm -p 127.0.0.1:8080:8080 -p 127.0.0.1:9090:9090 -p 127.0.0.1:9091:9091 \ ghcr.io/cyoda-platform/cyoda:latest
# Docker Compose (bundled compose.yaml)docker compose -f deploy/docker/compose.yaml up
# Helm (Kubernetes)helm install cyoda ./deploy/helm/cyoda \ --set postgres.existingSecret=cyoda-pg \ --set jwt.existingSecret=cyoda-jwt
# Dev script (in-memory, mock auth)./scripts/dev/run-local.shDESCRIPTION
Section titled “DESCRIPTION”cyoda-go is a single-process, multi-tenant REST and gRPC API server. It starts in serving mode when invoked with no subcommand. All configuration is via environment variables with a CYODA_ prefix. The binary, Docker image, and Helm chart run the same binary; only the environment configuration differs across run modes.
The process binds three TCP listeners concurrently: the REST API (default port 8080), gRPC (default port 9090), and an admin server (default port 9091). The admin server hosts health probes and the Prometheus metrics endpoint. On receiving SIGINT or SIGTERM, the server drains in-flight HTTP and admin requests within a 10-second deadline, then closes the storage backend and exits.
No systemd unit files ship in the repository. Process supervision (systemd, runit, s6, etc.) is the operator’s responsibility when running the binary directly outside of Docker or Kubernetes.
RUN MODES
Section titled “RUN MODES”Binary
Section titled “Binary”The prebuilt binary is the canonical artifact. Build from source or download from the GitHub release page.
Build from source:
go build -o bin/cyoda ./cmd/cyodaRun (default — in-memory storage, mock auth):
./bin/cyodaRun with SQLite after init:
cyoda initcyodacyoda init writes ~/.config/cyoda/cyoda.env with CYODA_STORAGE_BACKEND=sqlite. The binary loads that file via app.LoadEnvFiles() at startup.
Required env vars for postgres + JWT mode:
export CYODA_STORAGE_BACKEND=postgresexport CYODA_POSTGRES_URL=postgres://user:pass@host:5432/dbnameexport CYODA_IAM_MODE=jwtexport CYODA_REQUIRE_JWT=trueexport CYODA_JWT_SIGNING_KEY_FILE=/run/secrets/signing.pemcyodaThe binary accepts env vars from the process environment, from .env files loaded by CYODA_PROFILES, and from the user config written by cyoda init. The CYODA_PROFILES variable selects which .env profile files to load from the current working directory. For example, CYODA_PROFILES=postgres,jwt loads .env.postgres then .env.jwt from the working directory. The user config at ~/.config/cyoda/cyoda.env (written by cyoda init) is always loaded automatically as a separate step — it is not a profile file.
Docker
Section titled “Docker”Image: ghcr.io/cyoda-platform/cyoda:latest
The image uses gcr.io/distroless/static as its base. The binary is placed at /cyoda. The container runs as UID/GID 65532:65532 (non-root). /var/lib/cyoda is pre-staged with ownership 65532:65532 and is the intended mount point for persistent SQLite data.
Exposed ports: 8080 (HTTP), 9090 (gRPC), 9091 (admin).
The entrypoint is /cyoda with no default arguments. Subcommands (init, health, migrate) are passed as Docker CMD arguments.
Minimal run (in-memory + mock auth):
docker run --rm \ -p 127.0.0.1:8080:8080 \ -p 127.0.0.1:9090:9090 \ -p 127.0.0.1:9091:9091 \ -e CYODA_ADMIN_BIND_ADDRESS=0.0.0.0 \ ghcr.io/cyoda-platform/cyoda:latestCYODA_ADMIN_BIND_ADDRESS=0.0.0.0 is required when running in Docker so the health probes on port 9091 are reachable from outside the container. Without it, the admin server binds to loopback (127.0.0.1) inside the container and /livez and /readyz are unreachable.
SQLite with persistent volume:
docker run --rm \ -p 127.0.0.1:8080:8080 \ -p 127.0.0.1:9090:9090 \ -p 127.0.0.1:9091:9091 \ -e CYODA_STORAGE_BACKEND=sqlite \ -e CYODA_SQLITE_PATH=/var/lib/cyoda/cyoda.db \ -e CYODA_ADMIN_BIND_ADDRESS=0.0.0.0 \ -v cyoda-data:/var/lib/cyoda \ ghcr.io/cyoda-platform/cyoda:latestPostgres + JWT (production-shaped):
docker run --rm \ -p 127.0.0.1:8080:8080 \ -p 127.0.0.1:9090:9090 \ -p 127.0.0.1:9091:9091 \ -e CYODA_STORAGE_BACKEND=postgres \ -e CYODA_POSTGRES_URL=postgres://cyoda:secret@db:5432/cyoda \ -e CYODA_IAM_MODE=jwt \ -e CYODA_REQUIRE_JWT=true \ -e CYODA_JWT_SIGNING_KEY_FILE=/run/secrets/signing.pem \ -v /path/to/signing.pem:/run/secrets/signing.pem:ro \ -e CYODA_ADMIN_BIND_ADDRESS=0.0.0.0 \ ghcr.io/cyoda-platform/cyoda:latestDocker Compose
Section titled “Docker Compose”The repository ships a bundled compose file at deploy/docker/compose.yaml.
Default compose configuration:
services: cyoda: image: ghcr.io/cyoda-platform/cyoda:latest ports: - "127.0.0.1:8080:8080" - "127.0.0.1:9090:9090" - "127.0.0.1:9091:9091" environment: CYODA_STORAGE_BACKEND: sqlite CYODA_SQLITE_PATH: /var/lib/cyoda/cyoda.db CYODA_ADMIN_BIND_ADDRESS: 0.0.0.0 volumes: - cyoda-data:/var/lib/cyoda healthcheck: test: ["CMD", "/cyoda", "health"] interval: 10s timeout: 3s start_period: 30s retries: 3The bundled compose file uses SQLite + mock auth by default. The compose healthcheck calls cyoda health, which GETs /readyz on the admin port with a 2-second client timeout (see cmd/cyoda/health.go).
Run the bundled compose file:
docker compose -f deploy/docker/compose.yaml upEnable JWT auth before starting compose (file-mount approach):
Do not pass CYODA_JWT_SIGNING_KEY as an inline environment variable through docker compose — multi-line PEM content does not survive shell interpolation or YAML env-var parsing reliably. Always use CYODA_JWT_SIGNING_KEY_FILE with a volume mount.
# 1. Place the signing key on the host (outside the compose dir if gitignored):# 2. docker-compose.yaml snippet (add to the cyoda service):# volumes:# - ./secrets/signing.pem:/run/secrets/signing.pem:ro# environment:# CYODA_IAM_MODE: jwt# CYODA_REQUIRE_JWT: "true"# CYODA_JWT_SIGNING_KEY_FILE: /run/secrets/signing.pem# CYODA_JWT_ISSUER: https://auth.example.com# CYODA_JWT_AUDIENCE: cyoda-api# 3. Launch:docker compose upUse a custom image (e.g. a local dev build):
CYODA_IMAGE=ghcr.io/cyoda-platform/cyoda:dev \ docker compose -f deploy/docker/compose.yaml upThe compose file reads ${CYODA_IMAGE:-ghcr.io/cyoda-platform/cyoda:latest} for the image name.
Adjust start_period for slow environments (Postgres migrations, cluster mode) by editing the healthcheck.start_period field in deploy/docker/compose.yaml.
Kubernetes (Helm)
Section titled “Kubernetes (Helm)”cyoda-go ships a Helm chart at deploy/helm/cyoda/. The chart renders a StatefulSet (with Parallel pod management policy), ClusterIP Service, headless Service for gossip, ConfigMap for non-sensitive env vars, projected-volume Secrets for credentials, and optional Gateway API routes, Ingress, HorizontalPodAutoscaler, PodDisruptionBudget, NetworkPolicy, ServiceAccount, ServiceMonitor, and a pre-upgrade migration Job.
The chart is not yet published to a Helm repository. Install directly from the local path or from a cloned repository. See helm for the complete Helm reference.
Minimal install:
helm install cyoda ./deploy/helm/cyoda \ --set postgres.existingSecret=cyoda-pg \ --set jwt.existingSecret=cyoda-jwtUpgrade:
helm upgrade --install cyoda ./deploy/helm/cyoda \ --set postgres.existingSecret=cyoda-pg \ --set jwt.existingSecret=cyoda-jwtDevelopment Scripts
Section titled “Development Scripts”Developer convenience scripts live under scripts/dev/. These are not canonical provisioning artifacts. Canonical artifacts are in deploy/.
scripts/dev/run-local.sh— runscyoda-goviago run ./cmd/cyodausing thelocalprofile (in-memory storage, mock auth). Override withCYODA_PROFILES=postgres,otel ./scripts/dev/run-local.sh.scripts/dev/run-docker-dev.sh— builds the binary from source for the host platform (linux/amd64orlinux/arm64), builds a local Docker image taggedghcr.io/cyoda-platform/cyoda:dev, and runs it viadocker compose -f deploy/docker/compose.yaml up. Generates a fresh JWT signing key and randomized bootstrap client secret per run. Intended for contributors testing local changes in a container before they land.
Run with in-memory storage and mock auth (go run):
./scripts/dev/run-local.shRun with a custom profile set:
CYODA_PROFILES=postgres,otel ./scripts/dev/run-local.shBuild and run a local Docker image:
./scripts/dev/run-docker-dev.shSIGNALS
Section titled “SIGNALS”SIGINT(Ctrl+C) — triggers graceful shutdown. HTTP and admin servers drain in-flight requests within a 10-second deadline. The storage backend is closed. The process exits with code 0.SIGTERM— same behavior asSIGINT. Kubernetes sendsSIGTERMwhen a pod is evicted or deleted.SIGPIPE— ignored. When the binary is piped throughtee(e.g../bin/cyoda | tee log) and Ctrl+C killsteefirst, the broken pipe would cause the binary to exit immediately before theSIGINThandler runs. IgnoringSIGPIPElets the write fail silently while the graceful shutdown proceeds. (Source:cmd/cyoda/main.go,signal.Ignore(syscall.SIGPIPE).)
Signal handling is established in main() before the listeners start. The signal channel has buffer size 1.
HEALTH PROBES
Section titled “HEALTH PROBES”Both probes are served on CYODA_ADMIN_PORT (default 9091) at CYODA_ADMIN_BIND_ADDRESS (default 127.0.0.1). Both endpoints are unauthenticated — authentication is not applied to /livez or /readyz regardless of CYODA_METRICS_BEARER or CYODA_METRICS_REQUIRE_AUTH.
GET /livez— liveness probe. Returns200 OKwith bodyokwhen the admin server is accepting connections. No business logic check is performed.GET /readyz— readiness probe. Returns200 OKwith bodyokwhen the server has completed startup and is ready to serve requests. Returns a non-200 status while the storage backend is initializing or migrations are pending.
The cyoda health subcommand calls /readyz on the admin port with a 2-second HTTP client timeout and exits 0 on 200 OK, 1 otherwise. This is the implementation behind Docker’s HEALTHCHECK: CMD /cyoda health and is valid as a readiness check for any init system.
Admin bind address in container environments: set CYODA_ADMIN_BIND_ADDRESS=0.0.0.0 to make health probes reachable from outside the container. The Kubernetes Helm chart sets this value in its ConfigMap. Without it, /livez and /readyz are inaccessible from the kubelet or Docker healthcheck daemon.
SHUTDOWN TIMING
Section titled “SHUTDOWN TIMING”The graceful shutdown deadline is 10 seconds, applied separately to the HTTP server and the admin server. This value is hardcoded in cmd/cyoda/main.go:
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)After both HTTP servers shut down, app.Close() is called to release backend resources (database connection pools, cluster membership). There is no separate configurable timeout for app.Close() — it runs to completion after the HTTP deadline.
In Kubernetes, the pod terminationGracePeriodSeconds (default 30s) must be greater than 10s to allow the HTTP drain to complete before the kubelet sends SIGKILL.
PORT LAYOUT
Section titled “PORT LAYOUT”All ports are configurable via environment variables. The defaults:
- HTTP REST API — port
8080, bind address0.0.0.0(all interfaces). Controlled byCYODA_HTTP_PORT. All entity, model, workflow, search, and auth endpoints. Context path prefix:CYODA_CONTEXT_PATH(default/api). - gRPC — port
9090, bind address0.0.0.0(all interfaces). Controlled byCYODA_GRPC_PORT. Externalized-processor streaming (processor and criteria dispatch). The bind expression isfmt.Sprintf(":%d", cfg.GRPC.Port)— all interfaces, not loopback. - Admin — port
9091, bind address127.0.0.1(loopback) by default. Controlled byCYODA_ADMIN_PORTandCYODA_ADMIN_BIND_ADDRESS. Hosts/livez,/readyz, and/metrics. SetCYODA_ADMIN_BIND_ADDRESS=0.0.0.0in Docker/Kubernetes to make probes reachable. - Gossip (cluster mode only) — port
7946TCP+UDP. Controlled byCYODA_GOSSIP_ADDR(default:7946). Used by the memberlist gossip protocol for cluster membership and SWIM health checking. Active only whenCYODA_CLUSTER_ENABLED=true.
EXAMPLES
Section titled “EXAMPLES”Binary — postgres + JWT, production-shaped:
export CYODA_STORAGE_BACKEND=postgresexport CYODA_POSTGRES_URL_FILE=/run/secrets/postgres-urlexport CYODA_IAM_MODE=jwtexport CYODA_REQUIRE_JWT=trueexport CYODA_JWT_SIGNING_KEY_FILE=/run/secrets/signing.pemexport CYODA_JWT_ISSUER=https://auth.example.comexport CYODA_JWT_AUDIENCE=cyoda-api./bin/cyodaDocker — in-memory + OTel tracing:
docker run --rm \ -p 127.0.0.1:8080:8080 \ -p 127.0.0.1:9090:9090 \ -p 127.0.0.1:9091:9091 \ -e CYODA_ADMIN_BIND_ADDRESS=0.0.0.0 \ -e CYODA_OTEL_ENABLED=true \ -e OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:4318 \ -e OTEL_SERVICE_NAME=cyoda \ ghcr.io/cyoda-platform/cyoda:latestDocker — suppress banner (CI):
docker run --rm \ -e CYODA_SUPPRESS_BANNER=true \ -e CYODA_ADMIN_BIND_ADDRESS=0.0.0.0 \ ghcr.io/cyoda-platform/cyoda:latestCheck health from outside the container:
curl -s http://localhost:9091/readyzBinary — check readiness via subcommand:
./bin/cyoda healthecho $? # 0 = ready, 1 = not ready or errorDocker Compose — production JWT (file-mount):
# Mount the PEM file and reference it via CYODA_JWT_SIGNING_KEY_FILE.# Do not export CYODA_JWT_SIGNING_KEY inline — multi-line PEM does not# survive shell → docker-compose env interpolation reliably.# See the Docker Compose section above for the full snippet.docker compose upSEE ALSO
Section titled “SEE ALSO”- cli
- cli.serve
- cli.init
- cli.health
- quickstart
- helm
- config
- config.database
- config.auth
- telemetry
See also
Section titled “See also”cyoda help cli— cyoda is a Go binary that embeds the full platform: API server, schema engine, workflow runner, and storage plugins. Invoked with no subcommand, it starts the server using environment-provided configuration. Subcommands provide operational affordances —initfor first-run bootstrap,healthfor liveness probes,migratefor schema migrations.cyoda help cli serve— Starting with no subcommand loads configuration from environment variables, validates the IAM mode, and binds the REST, gRPC, and admin listeners. The server is single-process, multi-tenant, and stateful — storage is provided by one of the pluggable backends (memory,sqlite, orpostgres); seecyoda help configfor backend selection.cyoda help cli init—cyoda initis the recommended first step for local desktop use. It writes a minimal user config file that setsCYODA_STORAGE_BACKEND=sqlite, enabling persistent local storage without requiring a database server.cyoda help cli health—cyoda healthsends an HTTP GET tohttp://127.0.0.1:<port>/readyzand exits 0 if the server responds with HTTP 200. Any non-200 response or connection error causes exit 1.cyoda help quickstart— cyoda-go is a single-process, multi-tenant REST and gRPC API server backed by a pluggable embedded database management system. Storage backends arememory,sqlite, andpostgres; authentication modes aremockandjwt. All configuration is via environment variables with aCYODA_prefix.cyoda help helm— The chart atdeploy/helm/cyodadeploys cyoda-go on Kubernetes as a StatefulSet backed by an external PostgreSQL database. The chart requires Kubernetes>=1.31.0. Chart version:0.1.0. App version:0.1.0(synchronized to the binary by thebump-chart-appversion.ymlCI workflow).cyoda help config— Environment variables beat default values. The_FILEsuffix variant takes precedence over the plain variable when both are set — for example,CYODA_POSTGRES_URL_FILE=/etc/secrets/db-urlwins overCYODA_POSTGRES_URL. There are no command-line flags for configuration values; env vars are the sole configuration surface.cyoda help config database— config.database — storage backend selection and per-backend connection settings.cyoda help config auth— config.auth — IAM mode, JWT issuer, HMAC secret, and admin bootstrap controls.cyoda help telemetry— cyoda-go integrates the OpenTelemetry Go SDK (go.opentelemetry.io/otel). WhenCYODA_OTEL_ENABLED=true, the binary initializes a trace provider and a meter provider at startup using OTLP HTTP exporters. WhenCYODA_OTEL_ENABLED=false(default), no OTel SDK is initialized and no spans or metrics are emitted; the global OTel provider remains a no-op.
Raw formats
Section titled “Raw formats”/help/run.json— full descriptor (matchesGET /help/{topic}envelope)/help/run.md— body only