On this page

Local development

Choose one environment, use one configuration profile consistently, and run the same module contracts in HTTP, CLI and workers. This guide covers the day-to-day loop after installation.

Choose your setup#

Setup Best fit Infrastructure
Host Go process Fast edit/restart and debugger use Tutorial PostgreSQL/Redis containers
Docker Compose A containerized app and retained local data Services in deployments/compose.yaml
Tilt Kubernetes development and resource inspection Local cluster, Docker and Tilt

Compose and Tilt publish overlapping host ports. Stop one before starting the other. Redis is required for tasks; the application has no memory queue driver. Events remain process-local in every setup.

Run on the host#

Start the tutorial dependencies, then select the same profile for migrations, HTTP and commands:

export APP_ENV_FILE=.env.quickstart.example
go run ./cmd/cli migrate up
go run ./cmd/app

After editing Go code, stop the app with Ctrl+C and rerun it. These entry points do not watch source files. In a second terminal:

APP_ENV_FILE=.env.quickstart.example go run ./cmd/cli route list

Expect a route table, with no new HTTP listener or workers. After configuring Identity and creating an account, inspect it with:

APP_ENV_FILE=.env go run ./cmd/cli identity show '<user-id>'

Use the profile that created the account; .env here represents your configured local profile, not a requirement to rename it.

Run with Docker Compose#

docker compose -f deployments/compose.yaml up --build -d
docker compose -f deployments/compose.yaml logs -f api

Compose runs migrations before the app and retains service data in named volumes. It rewrites infrastructure addresses for communication between containers. Check the deployment guide for credentials and all exposed ports.

Host and container addresses differ:

Caller PostgreSQL example Redis example
Host quickstart 127.0.0.1:55432 127.0.0.1:56379
App inside Compose postgres:5432 redis:6379

Do not copy a container-only hostname into a host app's connection URL.

Run with Tilt#

tilt up

Tilt builds the app for the local cluster and starts MinIO and observability by default. Configure the local application Secret before running enabled providers. The public ConfigMap and Secret have separate responsibilities; see deployment.

To omit optional stacks:

tilt up -- --storage=false --observability=false

Use the Tilt dashboard to inspect build failures, migration completion and app logs. This is the Kubernetes workflow; a successful image build alone does not prove that migrations or dependency connections succeeded.

Optional object storage#

For Compose, enable the MinIO profile and select its disk together:

FILESYSTEM_DISK=minio docker compose -f deployments/compose.yaml --profile storage up --build -d

The bucket initialization service runs before the app in this profile. Tilt does the equivalent by default. FILESYSTEM_DISK from an existing .env can affect Compose's selected disk. To explicitly run without storage:

FILESYSTEM_DISK=disabled docker compose -f deployments/compose.yaml up --build -d

Disabling storage preserves existing objects and volumes. See file storage for connections and Media for upload metadata, visibility and URLs. A public asset's direct URL also requires an appropriate bucket policy and a client-reachable address.

Read local logs#

LOG_CHANNELS=stderr

This uses readable terminal output. Select stderr,file to also write files, or include observability to export structured logs directly over OTLP. File collection is not needed for Loki.

The current file channel writes warnings and errors to storage/logs/app-YYYY-MM-DD.log, using the UTC date and retaining 14 days. Formats, levels, single/daily mode and retention are configured in internal/app/config/observability.go; see single or daily files.

Keep file logs with Tilt or Compose#

Tilt's Docker Desktop kubeadm setup mounts the checkout's storage/logs at /app/storage/logs. Apply through Tilt so the path and UID/GID are set correctly. The Kind provisioner does not expose Mac folders through the same hostPath arrangement. Compose instead shares the named app-storage volume; copy logs out when needed:

mkdir -p storage/logs
docker compose -f deployments/compose.yaml cp api:/app/storage/logs/. storage/logs/

For request-to-trace correlation and Grafana, follow observability.

Run separate processes#

First change the selection in cmd/app/main.go to app.Processes{HTTP: true}. Then use the same database and queue settings in each terminal:

APP_ENV_FILE=.env.quickstart.example go run ./cmd/app
APP_ENV_FILE=.env.quickstart.example go run ./cmd/worker

The HTTP process must keep its own subscribers: GoChannel cannot move an event to the worker process. Redis tasks can cross that boundary. Enable recurring schedules with one cmd/scheduler process; the dedicated worker never starts a scheduler. When metrics are enabled on several host processes, assign each listener a different port.

See entry points for explicit process selections and queues for task routing.

Optional brokers#

RabbitMQ and NATS are opt-in infrastructure experiments. Starting them does not install an application driver or change local event guarantees:

docker compose -f deployments/compose.yaml --profile extra-brokers up --build -d
tilt up -- --extra-brokers=true

Use one of these commands for your selected environment, not both. Default launches omit these brokers.

Stop development services#

Action Effect
Ctrl+C in a host app Drain that process, then close its clients
docker compose -f deployments/compose.yaml stop Stop containers and retain volumes
Compose down Remove the stack's containers/networks; retain named volumes
Compose down --volumes Also delete volume data
Stop the Tilt process Leave Kubernetes resources running
tilt down Remove managed resources; namespace/volume cleanup can delete development data

Host-mounted logs survive Pod removal. Database, broker and object-store data have their own volumes and backup needs. Do not use destructive cleanup to fix a configuration error in data you want to retain.

Diagnose startup problems#

Symptom Check
Redis connection refused Selected profile, host/container address and port
Unexpected dependency opens Shell overrides and enabled features
Address already in use Another host process or development stack
Enqueue succeeds but nothing runs Worker queue names and Redis database
Local event never arrives Handler registered and subscribers running in the producer process
No file entries after an INFO request The default file channel records WARN and ERROR
Grafana has no data Collector/export settings as well as the dashboard

Continue with troubleshooting for detailed diagnosis, testing for disposable test services, or your first module to add application behavior.

Documentation

Search guides, sections, and API names.