Deploy the App
This chapter builds the App for production and runs it the way a server does. You will learn how to:
- Version the App with a git tag, and build it with
act build - Run the binary in the production Mode, with its configuration in the environment
- Probe the health check routes the skeleton registers
- Run the App's Commands from the binary, where
actisn't installed - Stop the App gracefully
Overview
An App builds into one binary that holds everything it needs: your code, Tinka and the standard library. Deploying it means copying that file to a server and running it with its configuration in the environment. The same binary serves requests when it runs with no arguments, and runs a Command, such as posts:list, when it runs with one.
Version the App
act build records the App's version in the binary, from the git tag of the commit it builds, and marks a build with uncommitted changes as +dirty. Commit the App, then tag the commit:
git add -A
git commit -m "Build the blog API"
git tag v0.1.0.gitignore came with the skeleton, so the commit leaves out .env, which holds your machine's APP_KEY, and bin/, where the build goes.
Build the binary
Run act build from the App's directory:
act build$ GOWORK=off CGO_ENABLED=0 go build -trimpath -o bin/blog .
Built bin/blog (14.9 MB, v0.1.0, linux/amd64, revision 339228dbfeee)act build prints the go build command it runs, so a CI runner or a container build without act runs the same command with Go alone. CGO_ENABLED=0 makes the binary static, so it runs on any machine of the same operating system and architecture, even in a minimal container image. Deployment covers building for another system with --target, and a Dockerfile.
Generate a production key
Production needs an APP_KEY of its own, never the one in your .env: anyone who has the key can forge the cookies and signed URLs it signs. With APP_ENV=production, act generate:key prints a new key instead of writing it to .env:
APP_ENV=production act generate:keyIt prints the key on a line of its own, APP_KEY= followed by 43 characters, then a reminder:
APP_ENV is production, which reads no .env file: set APP_KEY in your platform's environmentRun in production
APP_ENV=production puts the App in the production Mode. In production, the App reads no .env file at all, so every variable comes from the real environment: set them in your platform's settings or your service definition. Everything else in .env.example has a default: the App listens on port 3000 unless PORT says otherwise, and APP_DEBUG stays off, so a server error's cause never reaches a client. Run the binary with the key you generated:
APP_ENV=production APP_KEY=your_app_key ./bin/blog{"time":"2026-10-21T01:13:52.967764859+02:00","level":"INFO","msg":"server started","addr":"[::]:3000","took":823476}Outside development, the App logs one JSON object per line, ready for a log collector. A variable the App needs and can't find stops it at boot, before it listens, with every missing variable listed together:
APP_ENV=production ./bin/blogenv: APP_KEY is missingThe binary exits with status 1, so your platform reports a failed deployment instead of an App that fails its first request.
Probe the health checks
Load balancers and orchestrators such as Kubernetes probe an App to decide whether to send it requests, and whether to restart it. The skeleton registers both probes in start/routes.go, with app.Health. The liveness probe checks only that the App answers:
curl localhost:3000/health/live{"status":"ok"}The readiness probe runs the App's checks: here, one that the disk isn't full. It answers 200 while every check passes and 503 when one fails, so a load balancer stops sending requests to this instance without restarting it:
curl localhost:3000/health/ready{"healthy":true,"status":"ok","finished_at":"2026-10-21T01:13:53.994157515+02:00","checks":[{"name":"disk space /","status":"ok","cached":false,"duration_ns":215302}]}When a database replaces the in-memory store, add a check for it in start/routes.go, so an instance that can't reach the database stops receiving requests. The routes answer as they did under act serve:
curl localhost:3000/posts[{"id":1,"title":"Hello, Tinka","body":"The first post on the blog."},{"id":2,"title":"Routes and handlers","body":"How a request finds its handler."}]Run a Command from the binary
act isn't installed on a server, and doesn't need to be: the binary runs its own Commands. Run posts:list with the same environment:
APP_ENV=production APP_KEY=your_app_key ./bin/blog posts:list 1 Hello, Tinka
2 Routes and handlersStop the App
Platforms stop an App with SIGTERM when they replace it, and Ctrl+C sends SIGINT. On either, the App stops taking new connections, lets the requests in flight finish, runs its shutdown hooks, and exits:
{"time":"2026-10-21T01:13:54.020346272+02:00","level":"INFO","msg":"server shutting down"}
{"time":"2026-10-21T01:13:54.020535836+02:00","level":"INFO","msg":"server stopped"}Recap
act build built the tagged App into one static binary, bin/blog. It runs in production with APP_ENV=production and its APP_KEY from the environment, answers its health checks for load balancers, runs posts:list without act, and shuts down gracefully on SIGTERM. That completes the tutorial: the blog App lists, shows and creates posts, shares its store through a service provider, has a Command for people and agents, and tests for all of it.
From here, the guides cover each part in depth. Middleware adds behavior around the routes, Logger writes your own log records, and Emitter lets one part of the App react when another creates a post.
See also: Deployment, Configuration & Environment, Health checks, Application lifecycle