# Deploy the App

> Build the blog App into one binary with act build, run it in production with its environment, probe its health checks, and run its Command from the binary.

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 `act` isn'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:

```sh
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:

```sh
act build
```

```text title="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](../deployment.md) 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`:

```sh
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:

```text title="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:

```sh
APP_ENV=production APP_KEY=your_app_key ./bin/blog
```

```text title="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:

```sh
APP_ENV=production ./bin/blog
```

```text title="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:

```sh
curl localhost:3000/health/live
```

```text title="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:

```sh
curl localhost:3000/health/ready
```

```text title="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`:

```sh
curl localhost:3000/posts
```

```text title="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:

```sh
APP_ENV=production APP_KEY=your_app_key ./bin/blog posts:list
```

```text title="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:

```text title="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](../guides/basics/middleware.md) adds behavior around the routes, [Logger](../guides/digging-deeper/logger.md) writes your own log records, and [Emitter](../guides/digging-deeper/emitter.md) lets one part of the App react when another creates a post.

See also: [Deployment](../deployment.md), [Configuration & Environment](../configuration.md), [Health checks](../guides/digging-deeper/health-checks.md), [Application lifecycle](../guides/concepts/application-lifecycle.md)
