How to deploy a Go app on Ownkube

Deploy a Go app on Ownkube with a multi-stage Dockerfile for a small static binary, environment-based config, an optional managed Postgres, and metered billing.

Ownkube team | | How-To | 5 min

A Go binary is close to the ideal thing to deploy: no runtime to install, a small static executable, and a memory footprint that barely moves at idle. Ownkube runs it as a Dockerfile-based deploy, with automatic TLS and an optional managed Postgres if the app needs a database.

This walks through deploying a Go app on Ownkube Compute: a multi-stage Dockerfile that produces a small static binary, configuration through environment variables, an optional Postgres connection, and why Go’s low idle footprint fits Ownkube’s metered billing especially well.

TL;DR

  • A multi-stage Dockerfile compiles a static Go binary in one stage and copies just the binary into a minimal final image, often under 20 MB.
  • Configure the app entirely through environment variables read at startup, including PORT and DATABASE_URL.
  • Add a managed Postgres if the app needs one, and connect with database/sql and a driver like pgx or lib/pq.
  • Metered billing rewards Go’s footprint directly: a Go service idles at a few megabytes of RAM and near-zero CPU, so the meter barely moves between requests.
  • Autoscaling and zero-downtime rollouts work the same way as any other Ownkube app.

1. Read configuration from the environment

Idiomatic Go config is a handful of os.Getenv calls at startup, with PORT read the same way Ownkube injects it for any other language:

// main.go
package main

import (
	"log"
	"net/http"
	"os"
)

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte(`{"status":"ok"}`))
	})
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("hello from Ownkube"))
	})

	log.Printf("listening on %s", port)
	if err := http.ListenAndServe(":"+port, mux); err != nil {
		log.Fatal(err)
	}
}

2. Write a multi-stage Dockerfile

The build stage compiles a static binary with CGO_ENABLED=0, and the final stage copies only that binary into a minimal base, so the shipped image is small and starts fast:

FROM golang:1.23 AS build
WORKDIR /src

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

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server .

FROM gcr.io/distroless/static-debian12
COPY --from=build /app/server /server

EXPOSE 8080
ENTRYPOINT ["/server"]

distroless/static-debian12 has no shell and no package manager, which keeps the attack surface and the image size down. If you need a shell for debugging, swap it for alpine:3.20 and add RUN apk add --no-cache ca-certificates before the final COPY.

3. Push and create the app

okctl login
okctl apps create go-app --dockerfile Dockerfile
okctl deploy

Ownkube builds the image, allocates a *.ownkube.app hostname, and issues TLS automatically. Attach a custom domain from the app’s settings once you have one.

4. Set environment variables

okctl env set LOG_LEVEL=info --app go-app
okctl env set API_TOKEN="your-secret-token" --app go-app

Environment variables set this way are encrypted at rest and injected into the container at start, so secrets never need to live in the image or the repo.

5. Add a managed Postgres, if the app needs one

Create a Postgres box from New > Database > Postgres in the dashboard. It is a single-instance, private (in-region) box starting at $4 a month plus storage at $0.15 per GB, backups included. Multi-instance HA and public access are on the roadmap, not available yet.

okctl env set DATABASE_URL="postgres://user:password@db-host:5432/go_app" --app go-app

Connect with the standard library and a driver such as pgx:

import (
	"database/sql"
	"os"

	_ "github.com/jackc/pgx/v5/stdlib"
)

db, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
if err != nil {
	log.Fatal(err)
}
defer db.Close()

6. Wire up the health check and autoscaling

Point Ownkube’s health check at the /health route from step 1, so a rollout only completes once the new instance is actually serving:

okctl apps update go-app --health-check-path /health

Turn on horizontal autoscaling for uneven traffic:

okctl apps update go-app --autoscale-min 1 --autoscale-max 5 --autoscale-cpu-target 70

7. Why Go’s footprint fits metered billing

Ownkube’s metered pricing bills CPU and memory on actual per-minute usage, roughly $18 per vCPU per month and $4.50 per GiB per month. A Go binary is the shape that pricing model rewards most directly: no VM or interpreter warming up, a resident set often in the tens of megabytes, and CPU that drops to near zero the instant a request finishes. Set a small ceiling and let the meter do the rest:

okctl deploy --cpu-limit 250m --memory-limit 256Mi

For a low-traffic API or a webhook receiver, that low idle footprint means the wallet balance stretches a long way between top-ups, since a limit is a ceiling, not a reservation. You are billed for what the process actually uses up to that ceiling, not for the ceiling itself.

Moving to your own AWS later

If the app later needs to run in a named AWS account, your own or a customer’s, the same Dockerfile moves with no rewrite. Ownkube provisions the instance and keeps the same deploy flow, logs, metrics, and health checks attached to it. That is a path Railway does not offer, since it only runs on Railway’s own infrastructure. Our Railway alternative comparison covers what changes when you make that move.

FAQ

Does Ownkube support Go modules and private dependencies?

Yes. go mod download runs during the build stage as normal. For a private module in a private repo, add a GOPRIVATE build argument and the appropriate git credentials as build-time secrets.

Do I need CGO for database drivers?

No. Pure-Go drivers like pgx (Postgres) work with CGO_ENABLED=0, which keeps the static binary fully portable across the distroless base image. Drivers that wrap a C library, like some SQLite bindings, need CGO enabled and a base image with the right shared libraries.

How small does the final image get?

A typical Go HTTP service with a distroless base lands in the 10 to 20 MB range, mostly the binary itself plus CA certificates, which keeps build and deploy times short.

Can I run a background worker in the same repo?

Yes. Build a second binary from the same module (a second go build target or a build arg that selects cmd/worker versus cmd/server) and deploy it as a separate Ownkube app pointed at the same DATABASE_URL.

Is there a free tier for a small Go service?

No, Compute has no free tier. The Personal plan is $5 a month and loads $5 of wallet credit you then spend, and unused credit rolls over and never expires, so a low-traffic Go service that barely touches the meter carries balance forward month to month.

Where Ownkube fits

Go’s small static binary and near-zero idle footprint line up well with metered billing that charges for actual CPU and memory rather than instance size. Ship a multi-stage Dockerfile, read config from the environment, attach a managed Postgres if you need one, and the health check and autoscaling handle the rest. Deploy your first app.

More posts