Tutorial

A Go Web Service on a Free Container Tier: A 15 MB Image and Tens of Megabytes of RAM

SnapDeploy Team 2026-10-16 Updated 2026-10-16 6 min read
gogolangdockerfree-tiertutorial

If a free container tier was designed for one language, it was Go. A Go web service compiles to a single static binary, starts in milliseconds and idles in a few tens of megabytes, so 512 MB and a quarter of a CPU is not a constraint, it is a surplus. This guide builds a small HTTP service the way it should be built for a container: a two-stage Dockerfile, a tiny image, a health endpoint, graceful shutdown, and the port read from the environment.

The service

Standard library only. net/http is enough for most services and it keeps the binary small. Go 1.22 and later route by method and path pattern, so no router package is needed for this:

package main

import (
    "context"
    "encoding/json"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

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

    mux := http.NewServeMux()
    mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("ok"))
    })
    mux.HandleFunc("GET /api/time", func(w http.ResponseWriter, r *http.Request) {
        json.NewEncoder(w).Encode(map[string]string{"now": time.Now().UTC().Format(time.RFC3339)})
    })

    srv := &http.Server{
        Addr:              ":" + port,
        Handler:           mux,
        ReadHeaderTimeout: 5 * time.Second,
    }

    go func() {
        log.Printf("listening on :%s", port)
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()

    stop := make(chan os.Signal, 1)
    signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
    <-stop

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    log.Println("shutting down")
    srv.Shutdown(ctx)
}

Three things in there earn their keep on a container host. PORT from the environment, so the platform decides the port. ReadHeaderTimeout, so a slow client cannot hold a connection open forever on a small instance. And the SIGTERM handler: when a container is stopped, or a free container goes to sleep after 15 minutes without traffic, the runtime sends SIGTERM first; a server that shuts down cleanly finishes in-flight requests instead of dropping them.

The Dockerfile

FROM golang:1-alpine AS build
WORKDIR /src
COPY go.mod go.sum* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/server .

FROM alpine:3
RUN apk add --no-cache ca-certificates && adduser -D -u 10001 app
USER app
COPY --from=build /out/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
  • golang:1-alpine tracks the current Go 1.x release, so the tag does not go stale. Pin golang:1.27-alpine if you want reproducible builds.
  • CGO_ENABLED=0 produces a static binary; -s -w strips symbols and debug info. The service above builds to roughly 7 MB.
  • The final image is Alpine plus certificates plus the binary: about 15 MB. scratch would be smaller still, but Alpine gives you a shell for debugging and the CA bundle for outbound HTTPS.
  • Running as a non-root user costs nothing and is the first thing a security scan looks for.

If you deploy without a Dockerfile, SnapDeploy generates one when it sees go.mod: a two-stage build with CGO_ENABLED=0 onto an Alpine runtime, much like the above. The generated builder uses a Go 1.21 image; if your go.mod declares a newer Go, the toolchain directive downloads it during the build. For control over the version, keep your own Dockerfile.

What it uses at runtime

Measured on the service above: about 10 MB of resident memory idle, under 30 MB while handling a few hundred requests a second, and start-up in well under a second. Against a 512 MB container that leaves room for in-memory caches, a SQLite file for read-mostly data, or an embedded queue, none of which a Node or JVM service could afford in the same box. Wake-up on the free tier is about 60 seconds, but almost all of that is the platform starting the container; the Go process itself is ready as soon as it is scheduled.

If you want to see the numbers yourself, GODEBUG=gctrace=1 prints heap sizes at each collection, and /proc/self/status inside the container shows VmRSS.

Deploying

  1. Push the repository to GitHub with the Dockerfile.
  2. In SnapDeploy, Containers → Deploy from GitHub → choose the repository. Confirm the port (8080 here).
  3. Deploy. Go builds are quick; the first build is a few minutes because the base images are pulled and modules downloaded, later builds reuse the cached module layer.
  4. Hit /health on the containers.snapdeploy.app URL, then /api/time.

When Go still needs more than the free tier

  • It has to answer at any hour. Free containers sleep after 15 minutes without traffic. Always-On Small is $12/month for the same 512 MB; a Go service rarely needs the 2 GB Medium.
  • It needs a database that persists. The container filesystem is ephemeral, so the SQLite trick above is for caches and read-only data. PostgreSQL Mini is $29/month; a $1 DB Sprint Pack gives you 12 hours of it to test against.
  • It needs a custom domain. Always-On again; free containers live on containers.snapdeploy.app.

Adding a database without adding bloat

The one dependency most services need is a database driver. With pgx the binary grows by a few megabytes and the service still idles well under 30 MB. Read the connection string from DATABASE_URL, which a SnapDeploy add-on injects and every other provider gives you to paste, and keep the pool small: a free container will never need more than a handful of connections, and a PostgreSQL Mini allows 50 in total.

cfg, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
if err != nil {
    log.Fatal(err)
}
cfg.MaxConns = 4
pool, err := pgxpool.NewWithConfig(ctx, cfg)

Make the health handler run pool.Ping(ctx) with a one-second timeout. When the container wakes after sleeping, the first request will re-establish the pool; a ping with a timeout turns a hung database into a clear 503 instead of a slow page.

Static files inside the binary

If the service also serves a small front end, the embed package puts the files into the executable, so the image stays one binary and there is nothing to copy or mount:

//go:embed static/*
var static embed.FS

mux.Handle("GET /", http.FileServerFS(static))

Go 1.22 added http.FileServerFS; on older versions use http.FileServer(http.FS(static)). Either way the files are read from memory, which is the fastest static file server you will run on a quarter of a CPU.

What sleep looks like in the logs

Because the service handles SIGTERM, a container going to sleep after 15 quiet minutes leaves a clean trail: shutting down, then the process exits with code 0. A service that ignores the signal is killed after the platform's stop timeout and exits with code 137, which looks like a crash in the logs and drops any request that was in flight. Ten lines of shutdown code are the difference between "sleeps politely" and "keeps dying every night" when you read the runtime log a week later.

Free tier: 4 containers, 512 MB and 0.25 vCPU each, 100 container-hours a month, 10 deploys a day, no card. Details on the free tier docs page; the Docker hosting page covers what happens when you bring your own image instead of a repository.

Ready to Deploy?

Deploy free. 10 deploys a day, 100 hours a month, no credit card.

Run this yourself: Get a dedicated NVIDIA T4 (16 GB) for a flat $499/mo or A10G (24 GB) for $999/mo — never shared, never spot, no hourly metering. See dedicated GPU hosting →

One-click Ollama, vLLM, ComfyUI, Whisper, PyTorch & more — or deploy your own GitHub repo or Docker image. Compare plans.

FIRST OF ITS KIND

Sprint Pack — ₹99 for 24 hours

The first managed container PaaS to offer a one-time 24-hour Always-On pass under $5. Unlimited deploys + Always-On for one Small (512 MB) container — including WebSockets & real-time apps. No subscription, no auto-renewal. Stack two for 48 hours.

Need a managed database too? DB Sprint Pack — ₹99 / 12h for Postgres, MySQL, MariaDB, Mongo, Redis, or RabbitMQ.

Tip: Need 24 hours of Always-On for a demo, weekend, or quick test — or WebSockets & real-time apps? Sprint Pack is ₹99 one-time — no subscription, no auto-renewal.

Need a managed Postgres / MySQL / Mongo / Redis instead? DB Sprint Pack — ₹99 / 12h.

Take SnapDeploy with you

Deploy, monitor and wake your containers from your iPhone.

Download on the App Store

Get DevOps Tips & Updates

Container deployment guides, platform updates, and DevOps best practices. No spam.

Unsubscribe anytime. We respect your privacy.

More Articles