tinka alpha
tinkaalpha

Deploy the App

This chapter builds the App for production and runs it the way a server does. You will learn how to:

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:

Terminal
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:

Terminal
act build
Output
$ 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:

Terminal
APP_ENV=production act generate:key

It prints the key on a line of its own, APP_KEY= followed by 43 characters, then a reminder:

Output
APP_ENV is production, which reads no .env file: set APP_KEY in your platform's environment

Run 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:

Terminal
APP_ENV=production APP_KEY=your_app_key ./bin/blog
Output
{"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:

Terminal
APP_ENV=production ./bin/blog
Output
env: APP_KEY is missing

The 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:

Terminal
curl localhost:3000/health/live
Output
{"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:

Terminal
curl localhost:3000/health/ready
Output
{"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:

Terminal
curl localhost:3000/posts
Output
[{"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:

Terminal
APP_ENV=production APP_KEY=your_app_key ./bin/blog posts:list
Output
   1  Hello, Tinka
   2  Routes and handlers

Stop 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:

Output
{"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