Deploy Go with AZIN

All guides
Deploy Guides·1 min read

Deploy Go with AZIN

deploygogolangpostgresql

Go compiles to a single static binary with no runtime dependencies. That makes deployment straightforward — if your build pipeline handles it correctly. AZIN detects your go.mod, compiles with CGO_ENABLED=0, and produces a minimal container image containing only the binary. No Go toolchain in production. No multi-stage Dockerfile to maintain. Push code, get a running service in your own GCP account.

#How AZIN detects Go

AZIN uses Railpack — a zero-config builder that auto-detects your language and framework. When it finds a go.mod or main.go in your repository, it identifies a Go project and configures the build automatically.

What Railpack does for Go projects:

  • Go version from the go directive in go.mod (defaults to 1.23 if unspecified)
  • Static binary compilation with CGO_ENABLED=0 — no libc dependency, no dynamic linking
  • Go workspace supportgo.work files are detected and handled for monorepo setups
  • Minimal final image — only the compiled binary is included. No Go toolchain, no source code, no module cache
  • Start command./out (the compiled binary)

The resulting container image is typically 5–20 MB. Compare that to a standard golang:1.26 base image at 800+ MB. Smaller images mean faster pulls, faster pod scheduling, and less storage on Artifact Registry.

Go 1.26 was released on February 10, 2026 and is the latest stable version. Go 1.25 remains in active support. Specify your version in go.mod for reproducible builds:

module github.com/yourorg/yourapp
 
go 1.26

Railpack respects whatever version you declare. If your go.mod says go 1.24, Railpack uses Go 1.24.

#Deployment config

Connect your GitHub repo from the AZIN Console, and AZIN deploys on every push. For a Go API with PostgreSQL:

name: my-go-api
cloud: gcp
region: us-central1
services:
  api:
    build:
      type: railpack
    env:
      PORT: "8080"
    scaling:
      min: 1
      max: 10
      target_cpu: 70
  db:
    type: postgres
    plan: production
  cache:
    type: redis

AZIN injects DATABASE_URL and REDIS_URL into your API service automatically. No manual connection string configuration.

API with background worker

For apps that process jobs from a queue, define separate services for the API and the worker. Both share the same codebase and the same environment variables:

name: my-go-app
cloud: gcp
region: us-central1
services:
  api:
    build:
      type: railpack
    env:
      PORT: "8080"
  worker:
    build:
      type: railpack
      buildCommand: "go build -o ./out ./cmd/worker"
    env:
      WORKER_CONCURRENCY: "10"
  db:
    type: postgres
    plan: production
  cache:
    type: redis

Both services compile from the same repository. The worker service uses a different build entry point to produce a separate binary. AZIN injects DATABASE_URL and REDIS_URL into both services.

#Go app structure for production

Three things matter for production Go apps on any container platform: PORT binding, graceful shutdown, and health checks.

PORT binding

AZIN injects PORT as an environment variable. Your HTTP server must listen on it:

package main
 
import (
	"log"
	"net/http"
	"os"
)
 
func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}
 
	mux := http.NewServeMux()
	mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	})
	mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("ok"))
	})
 
	log.Printf("listening on :%s", port)
	if err := http.ListenAndServe(":"+port, mux); err != nil {
		log.Fatal(err)
	}
}

Hard-coding a port will cause health checks to fail. Always read from os.Getenv("PORT").

Graceful shutdown

GKE Autopilot sends SIGTERM before terminating a pod. Your app should catch this signal, stop accepting new connections, drain in-flight requests, and exit cleanly:

package main
 
import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)
 
func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}
 
	mux := http.NewServeMux()
	mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	})
 
	srv := &http.Server{
		Addr:    ":" + port,
		Handler: mux,
	}
 
	go func() {
		log.Printf("listening on :%s", port)
		if err := srv.ListenAndServe(); err != http.ErrServerClosed {
			log.Fatalf("server error: %v", err)
		}
	}()
 
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
	<-quit
 
	log.Println("shutting down")
 
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
 
	if err := srv.Shutdown(ctx); err != nil {
		log.Fatalf("shutdown error: %v", err)
	}
}

http.Server.Shutdown closes listeners, stops accepting new connections, and waits for active requests to finish before returning. The 30-second timeout matches GKE Autopilot's default termination grace period.

Health checks

AZIN uses the /healthz endpoint to determine if your service is ready to receive traffic. A minimal health check returns 200 OK. For apps with database dependencies, check the connection pool:

mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
	if err := pool.Ping(r.Context()); err != nil {
		http.Error(w, "db unreachable", http.StatusServiceUnavailable)
		return
	}
	w.WriteHeader(http.StatusOK)
})

#Database access

AZIN provisions Cloud SQL (PostgreSQL) in your GCP account and injects the connection string as DATABASE_URL. Use pgx — the most widely used PostgreSQL driver for Go — with its connection pool:

package main
 
import (
	"context"
	"log"
	"os"
 
	"github.com/jackc/pgx/v5/pgxpool"
)
 
var pool *pgxpool.Pool
 
func initDB() {
	var err error
	pool, err = pgxpool.New(context.Background(), os.Getenv("DATABASE_URL"))
	if err != nil {
		log.Fatalf("unable to create connection pool: %v", err)
	}
}

pgxpool.New parses the DATABASE_URL and creates a connection pool with sensible defaults. For production tuning, use pgxpool.ParseConfig to control pool size and timeouts:

func initDB() {
	config, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
	if err != nil {
		log.Fatalf("unable to parse DATABASE_URL: %v", err)
	}
 
	config.MaxConns = 25
	config.MinConns = 5
	config.MaxConnLifetime = 30 * time.Minute
	config.MaxConnIdleTime = 5 * time.Minute
 
	pool, err = pgxpool.NewWithConfig(context.Background(), config)
	if err != nil {
		log.Fatalf("unable to create connection pool: %v", err)
	}
}

Add pgx to your module:

go get github.com/jackc/pgx/v5

Close the pool on shutdown to release connections back to Cloud SQL:

defer pool.Close()

Info

A CLI for setting environment variables is on our roadmap. Until then, manage all secrets through the AZIN Console.

#Worker processes

Go excels at background processing thanks to goroutines and channels. Define workers as separate services in azin.yaml alongside your API. Each worker compiles its own binary from the same repository.

A common pattern uses Redis (Memorystore) as a job queue with a library like asynq:

// cmd/worker/main.go
package main
 
import (
	"log"
	"os"
 
	"github.com/hibiken/asynq"
)
 
func main() {
	redisOpt, err := asynq.ParseRedisURI(os.Getenv("REDIS_URL"))
	if err != nil {
		log.Fatal(err)
	}
 
	srv := asynq.NewServer(
		redisOpt,
		asynq.Config{
			Concurrency: 10,
		},
	)
 
	mux := asynq.NewServeMux()
	mux.HandleFunc("email:send", handleSendEmail)
	mux.HandleFunc("report:generate", handleGenerateReport)
 
	if err := srv.Run(mux); err != nil {
		log.Fatal(err)
	}
}

Workers scale independently of the API. Set different scaling configurations per service if your worker needs different resource allocation.

Deploy Guides

Deploy Docker containers with AZIN

When you need CGO, system libraries, or full control over the build — use a custom Dockerfile.

#Why AZIN for Go hosting

Your cloud, your data. Your Go application, database, and Redis cache run in your own GCP account. You own the infrastructure, the data, and the billing relationship with Google. AWS and Azure are on our roadmap.

Static binary, minimal image. Railpack compiles Go with CGO_ENABLED=0, producing a fully static binary. The final container image contains only the binary — no Go toolchain, no source code, no module cache. Typical images are 5–20 MB. Fast pulls, fast pod scheduling, fast starts.

Managed PostgreSQL and Redis. Cloud SQL and Memorystore provisioned in your GCP account. Connection strings injected as DATABASE_URL and REDIS_URL. No manual database setup, no connection string copy-paste, no shared infrastructure.

No cold starts for production traffic. GKE Autopilot keeps your pods warm and scales horizontally based on CPU load. The first GKE cluster is free — you pay only for pod resources, not cluster overhead. This differs from platforms where a managed Kubernetes cluster can cost ~$225/month in underlying cloud fees before any workloads run (based on typical AWS EKS configurations, as of February 2026).

Scale-to-zero staging. Deploy staging environments on lttle.cloud (in early access). When your staging Go app receives no traffic, it scales to zero and costs nothing. Production stays warm on GKE Autopilot; staging idles without burning compute.

#Frequently asked questions

Deploy on private infrastructure

Managed AI environments with built-in isolation. Zero DevOps required.