How to build for production

This guide walks through building a Piko application for production. Steps cover compiling assets, building the server binary, choosing a run mode, and handing off to a process manager. See the CLI reference for every piko subcommand and the bootstrap options reference for every With* option.

Project layout

A scaffolded Piko project has two entry points under cmd/:

my-app/
├── cmd/
│   ├── generator/
│   │   └── main.go    # Asset/manifest generator
│   └── main/
│       └── main.go    # Server entry point
├── actions/
├── components/
├── pages/
└── partials/

cmd/generator/main.go produces the template and asset manifests. cmd/main/main.go is the HTTP server.

Step 1: Generate assets

Build and run the generator first. The generator compiles every .pk template, processes assets, and emits a manifest the server reads at startup.

go build -o bin/generator cmd/generator/main.go
bin/generator all

all invokes piko.GenerateModeAll, which covers template compilation, SQL-querier generation, and asset processing. Other modes are available if you need finer control:

CommandMode constantEffect
bin/generator manifestpiko.GenerateModeManifestCompile templates and emit the manifest only.
bin/generator sqlpiko.GenerateModeSQLRun the querier generator only.
bin/generator assetspiko.GenerateModeAssetsAsset processing only.
bin/generator allpiko.GenerateModeAllEverything.

Step 2: Build the server binary

Build a statically linked, stripped binary:

CGO_ENABLED=0 go build \
  -ldflags="-s -w" \
  -o bin/app \
  cmd/main/main.go
  • CGO_ENABLED=0 produces a static binary with no C dependencies. Use this unless you are linking a CGO-only SQLite driver.
  • -ldflags="-s -w" strips debug symbols and DWARF information for a smaller binary.

Step 3: Configure the application

Configure the framework in func main with With* options. Branch on a build flag, environment variable, or the run-mode argument if you need different settings between environments.

ssr := piko.New(
    piko.WithPublicDomain("yourdomain.com"),
    piko.WithForceHTTPS(true),
    piko.WithAutoNextPort(false),
)

The first command-line argument to ssr.Run carries the run mode (dev, prod) and controls watch mode automatically. Use piko.WithWatchMode(...) if you need to override that default.

Step 4: Run the binary

Piko applications accept the run mode as the first command-line argument:

./bin/app prod
Run modeConstantBehaviour
devpiko.RunModeDevHot reload, file watching, verbose dev output.
dev-ipiko.RunModeDevInterpretedYaegi interpreter: no recompilation, slower runtime.
prodpiko.RunModeProdCompiled templates, AST caching, production HTTP stack.

The scaffolded cmd/main/main.go reads the argument and passes it to ssr.Run:

package main

import (
    "os"

    "piko.sh/piko"
    "piko.sh/piko/wdk/logger"
)

func main() {
    logger.AddPrettyOutput()

    command := piko.RunModeDev
    if len(os.Args) > 1 {
        command = os.Args[1]
    }

    ssr := piko.New()
    if err := ssr.Run(command); err != nil {
        panic(err)
    }
}

The example above is the minimal shape. The wizard-generated scaffold goes one step further and constructs the SSR via internal.NewServer(command), a thin project-local wrapper under internal/piko.go that holds the shared option set. Both cmd/main and cmd/generator configure the framework the same way. If you scaffold with piko new, edit internal/piko.go to adjust framework options instead of scattering piko.With* calls across each binary.

Switch AddPrettyOutput() to AddJSONOutput() for structured logs in production.

Step 5: Expose the health probe

Piko runs two HTTP servers. One serves the application on port 8080 (default), the other serves the health probe on port 9090 (default). The default bind address for the health probe is 127.0.0.1, which keeps it off the public network. Expose it to the orchestrator by binding to all interfaces:

ssr := piko.New(
    piko.WithHealthEnabled(true),
    piko.WithHealthProbePort(9090),
    piko.WithHealthBindAddress("0.0.0.0"),
    piko.WithHealthLivePath("/live"),
    piko.WithHealthReadyPath("/ready"),
    piko.WithHealthMetricsPath("/metrics"),
    piko.WithHealthMetricsEnabled(true),
)

Step 6: Wire up a process manager

Piko does not supervise itself. Use a process manager that restarts the binary on crash.

systemd

[Unit]
Description=MyApp (Piko)
After=network.target

[Service]
ExecStart=/opt/myapp/bin/app prod
WorkingDirectory=/opt/myapp
Restart=on-failure
User=myapp
EnvironmentFile=/etc/myapp/env

[Install]
WantedBy=multi-user.target

Docker

A multi-stage Dockerfile:

# Build stage
FROM golang:1.26 AS build
WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

ENV CGO_ENABLED=0
ENV GOOS=linux
RUN go build -o bin/generator cmd/generator/main.go
RUN bin/generator all
RUN go build -ldflags="-s -w" -o bin/app cmd/main/main.go

# Runtime stage
FROM gcr.io/distroless/static:nonroot
WORKDIR /app

COPY --from=build /app/bin/app /app/app
COPY --from=build /app/dist /app/dist
COPY --from=build /app/.piko /app/.piko

CMD ["/app/app", "prod"]

The distroless/static:nonroot base image keeps the container small and runs as a non-root user by default.

Copy dist/ (the compiled manifest, pages, and collections that prod reads at startup) and .piko/ (content-addressed asset blobs plus registry metadata) into the runtime image. If your project serves raw static files from a top-level assets/ directory (favicons, images), copy that too. The .piko/cache, .piko/logs, and .piko/tmp subdirectories are regenerable runtime state — prune them in the build stage before the runtime COPY to slim the image, and mount a writable volume at /app/.piko if you run with a read-only root filesystem.

Kubernetes

A minimal deployment manifest:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: registry.example.com/myapp:latest
          ports:
            - containerPort: 8080
              name: http
            - containerPort: 9090
              name: health
          livenessProbe:
            httpGet:
              path: /live
              port: health
            initialDelaySeconds: 10
            periodSeconds: 30
          readinessProbe:
            httpGet:
              path: /ready
              port: health
            initialDelaySeconds: 5
            periodSeconds: 10
          envFrom:
            - secretRef:
                name: myapp-secrets

Production checklist

Before the first deploy:

  • HTTPS: terminate TLS directly with piko.WithTLS(...) (see TLS how-to) or front with a reverse proxy.
  • Security headers: on by default, tunable through piko.WithSecurityHeaders(...).
  • CSRF: on by default for actions.
  • Secrets: loaded into your func main from any source (env vars, secret manager, mounted files) and passed to the relevant With* option (see secrets how-to and secrets resolvers).
  • Rate limiting: configure piko.WithRateLimit(...) if exposed to the public internet.
  • Assets: run the generator before building the binary.
  • Logging: swap to JSON in production (logger.AddJSONOutput()).
  • Health probe: accessible to the orchestrator with piko.WithHealthBindAddress("0.0.0.0").
  • Metrics: /metrics on the health probe server when piko.WithHealthMetricsEnabled(true).
  • Process manager: configured to restart on failure.

Scaling

Piko applications are stateless when sessions, file uploads, caches, and rate-limit counters all live in external stores. For horizontal scaling:

  • Sessions: back by Redis, Valkey, or the database via the cache service.
  • Uploads: use storage_provider_s3, storage_provider_gcs, or another external provider.
  • Cache: use cache_provider_redis, cache_provider_valkey, or multilevel.
  • Rate limiter: use a Redis backend so counters survive instance restarts and replicas share the same state.

For vertical scaling inside one instance, tune GOMAXPROCS, database connection pools (piko.WithPostgresMaxConns, piko.WithPostgresMinConns), and the HTTP request timeout (piko.WithRequestTimeout).

See also