Build And Deploy
Deployment Checklist
Run a built GoLazy binary with production address, assets, and smoke checks.
Run the binary
Build the app:
go build -o .tmp/build/my-app ./cmd/my_app
Run it with the production address:
ADDR=0.0.0.0:8080 .tmp/build/my-app
ListenAndServe reads ADDR, then PORT, then defaults to
127.0.0.1:3000.
Set CONTROL_PLANE_ADDR when the deployment platform expects operational
endpoints on a separate listener:
ADDR=0.0.0.0:8080 CONTROL_PLANE_ADDR=127.0.0.1:9090 .tmp/build/my-app
That activates the default control plane with /livez and /readyz. Read
Control Plane when you need readiness checks,
metrics, or same-listener pprof. Separate control-plane listeners mount pprof
automatically.
Run bundled migrations
When an app configures lazyapp.Config.Migrations, the built binary can run its
own migrations:
LAZYAPP_MIGRATE=up .tmp/build/my-app
LAZYAPP_MIGRATE=auto ADDR=0.0.0.0:8080 .tmp/build/my-app
up applies pending migrations and exits 0. Use it for a deploy job or init
step before starting replicas. auto applies pending migrations and then
continues startup, so jobs and request handlers see the current schema. Unset,
empty, or off skips migration. Setting LAZYAPP_MIGRATE before migrations
exist is safe: no configured migrations or no pending migrations is a successful
no-op.
When CONTROL_PLANE_ADDR is set to a separate listener during up or auto,
GoLazy starts the real control plane before applying migrations. /livez
returns OK and /readyz returns not ready until migration handling finishes.
In auto mode, later startup stages add their handlers to that same listener
and it remains active while the app runs. When the control plane shares the app
listener, the process waits for migrations and starts that listener only after
the app is ready.
LAZYAPP_MIGRATE=auto with multiple replicas requires every configured
migration backend to implement the lazymigrate synchronization contract. For a
rollout where all replicas should continue cleanly, the backend must also treat
stale concurrent plans as a successful no-op or retryable refresh. If that
behavior has not been proven, prefer a single LAZYAPP_MIGRATE=up job before
scaling the application.
Enable Kubernetes observability
When a Kubernetes deployment should expose GoLazy metrics, run the app with a control-plane listener and Prometheus telemetry enabled:
env:
- name: CONTROL_PLANE_ADDR
value: "0.0.0.0:3001"
- name: OTEL_METRICS_EXPORTER
value: "prometheus"
Add a named control container port for that listener and point probes at it.
If the cluster uses Prometheus Operator, add a PodMonitor that selects the app
pods and scrapes the control port at /metrics. Pod annotations such as
prometheus.io/scrape: "true" are useful only for annotation-based scrape
configs.
For ingress-nginx tracing, set
nginx.ingress.kubernetes.io/enable-opentelemetry: "true" on the application
Ingress, then configure otlp-collector-host, otlp-collector-port,
otel-sampler, and otel-sampler-ratio on the ingress controller ConfigMap.
Those collector and sampler settings are controller configuration, not Ingress
annotations. Read Telemetry for the complete
Kubernetes example.
Provide secrets through the platform
Application code reads environment variables:
key := os.Getenv("SECURE_COOKIE_KEY")
Production deployments should provide those variables through the hosting
platform or secret store. Development files such as secrets/development.env
are examples, not production configuration.
Unpack assets when needed
Most deployments can serve assets from the Go binary. If a platform needs static files beside the app, use the asset registry:
if err := appinit.App().Assets.Unpack(
"public",
lazyassets.WithUnpackMode(lazyassets.UnpackPermanent),
); err != nil {
log.Fatal(err)
}
UnpackBoth writes logical and permanent paths. UnpackLogical writes only
logical paths. UnpackPermanent writes only content-hashed permanent paths.
Upload assets to object storage
For deployments that serve assets from object storage or a filer-backed ingress, upload the registered assets from the built app:
storage := s3.New(
s3.WithEndpoint(os.Getenv("ASSETS_S3_ENDPOINT")),
s3.WithBucket(os.Getenv("ASSETS_S3_BUCKET")),
s3.WithCredentials(
os.Getenv("ASSETS_S3_ACCESS_KEY_ID"),
os.Getenv("ASSETS_S3_SECRET_ACCESS_KEY"),
),
s3.WithPublicBaseURL(os.Getenv("ASSETS_PUBLIC_URL")),
)
if err := appinit.App().Assets.Upload(
context.Background(),
storage,
lazyassets.WithUploadMode(lazyassets.UnpackPermanent),
); err != nil {
log.Fatal(err)
}
Run this from the built binary or container image before starting the HTTP server so the exported hashed files match the embedded application assets.
Apps that already use lazyapp.Config.Migrations can instead opt in to
golazy.dev/lazyassets/assetmigrate and run the upload through
LAZYAPP_MIGRATE=up or LAZYAPP_MIGRATE=auto. The asset migration uses the
app build version, asset manifest hash, and upload options as its migration
identity, stores lease and done markers under .migrations/, and uses
conditional object-store writes so one replica uploads while the others observe
or wait. Bucket creation remains an app or deployment task; the migration
backend only uploads the registered assets.
Smoke test the deployment
After starting the deployed process, request the app and one public asset:
curl -i http://127.0.0.1:8080/
curl -i http://127.0.0.1:8080/styles.css
curl -i http://127.0.0.1:9090/livez
curl -i http://127.0.0.1:9090/readyz
Check that the app route returns HTML and the asset response includes cache
headers and an ETag. If CONTROL_PLANE_ADDR is not set, production builds do
not expose /livez or /readyz on the public app listener.
Build the sample container
The generated full app includes a Dockerfile for single-binary container deployments. Refresh generated assets first, then build and run the image:
lazy tailwind
lazy js
docker build -t sample-app .
docker run --rm -p 127.0.0.1:3000:3000 sample-app
The container listens on 0.0.0.0:3000 through ADDR. Provide production
secrets such as SECURE_COOKIE_KEY through the container environment.
Keep platform concerns outside GoLazy
GoLazy leaves TLS termination, process supervision, log shipping, service discovery, and platform routing to the deployment environment. The application binary should stay a normal Go HTTP process.