tinka alpha

Deployment

This guide covers deploying a Tinka App. You will learn how to:

Overview

A Tinka App builds into one binary that holds everything it needs: your code, Tinka, and any files you embed. There's nothing to install next to it, no runtime and no dependencies to copy, so deploying the App means copying one file and running it with its configuration in the environment.

act build runs go build with the settings a production binary needs, and prints the command it runs, so a machine without act, such as a CI runner or a container build, runs the same command with Go alone.

Build the App

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.7 MB, v1.2.0, linux/amd64, revision 58d23ba966e3)

The binary is named after the last part of the App's module path and written to bin/. It is built with:

The version comes from the git tag of the commit, such as v1.2.0, and ends in +dirty when the App has uncommitted changes. Go records it in the binary, along with the commit, so act about and your own code read it without a build step.

act build warns when go.mod replaces a module with a local directory, because that build differs from one made from the same commit on another machine, and when the App has uncommitted changes.

Ship static files

Files the App serves, such as stylesheets and images, go inside the binary with embed, so they always match the code that serves them:

assets.go
package main

import (
	"embed"
	"io/fs"
	"net/http"

	"github.com/mucolabs/tinka"
)

//go:embed public
var public embed.FS

// assets serves the files in public/ under /assets/.
func assets(app *tinka.App) error {
	files, err := fs.Sub(public, "public")
	if err != nil {
		return err
	}
	app.Handle(http.MethodGet, "/assets/", http.StripPrefix("/assets/", http.FileServerFS(files)))
	return nil
}

act serve rebuilds the App when an embedded file changes, as it does for Go files.

Build for another system

Pass --target to build for another operating system and architecture, such as a Linux server from a Mac:

Terminal
act build --target linux/arm64

The first build for a new target takes longer, while Go builds its standard library for it; later builds are as fast as usual. Pass --cgo only when a dependency needs C, such as a SQLite driver written in C: the binary then needs the system's C library at run time.

Run the App in production

Copy the binary to the server, and run it with its configuration in the environment. Production reads no .env file, so set every variable in your platform's settings or your service definition:

Terminal
APP_ENV=production APP_KEY=your_app_key ./blog

Run with no arguments, the binary serves requests; with arguments, it runs one of the App's Commands, such as ./blog about. It shuts down gracefully on SIGTERM, which is what platforms send when they replace an instance. See Application lifecycle for the shutdown timeout and Health checks for readiness probes and draining.

On a server managed with systemd, socket activation keeps the port open while the App restarts. See Socket activation.

Package the App in a container

A container build has Go but not act, so it runs the go build command that act build prints. Build in a Go image, then copy the static binary into a minimal one:

Dockerfile
FROM golang:1.27 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -o /blog .

FROM gcr.io/distroless/static-debian12
COPY --from=build /blog /blog
ENV APP_ENV=production
USER nonroot
EXPOSE 3000
ENTRYPOINT ["/blog"]

A .dockerignore that leaves out .git also leaves out the version: the binary then reports (devel). Copy .git into the build stage when you want the version recorded.

See also: Configuration & Environment, Commands reference, Health checks