# 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.

- **Published:** 2026-08-28
- **Author:** Ownkube team
- **Category:** How-To
- **Tags:** go, golang, deploy, tutorial, ownkube-compute
- **Canonical URL:** https://ownkube.io/blog/deploy-go-app
- **Cover:** https://ownkube.io/blog/deploy-go-app.png

---
A [Go](https://go.dev/doc/) 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](/blog/manage-secrets-env-vars), an optional [Postgres connection](/blog/add-managed-postgres-database), and why Go's low idle footprint fits Ownkube's metered billing especially well.

**TL;DR**

- A [multi-stage Dockerfile](https://docs.docker.com/build/building/multi-stage/) 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:

```go
// 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:

```dockerfile
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

```bash
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

```bash
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.

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

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

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

Turn on [horizontal autoscaling](/blog/autoscale-cpu-memory) for uneven traffic:

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

```bash
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](/blog/railway-alternative-2026) 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](https://docs.docker.com/language/golang/) 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](https://app.ownkube.io/login).