From 99574da4a80c03c5a26b788ea1056d39bc0f6eea Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Mon, 29 Jun 2026 13:33:28 -0400 Subject: [PATCH 1/2] Add Go HTTP server example A small standard-library Go HTTP server with a landing page, health check, and a couple of JSON endpoints. Deploys to Vercel with zero configuration. Co-Authored-By: Claude Opus 4.8 (1M context) --- go/dockerfile/.gitignore | 41 +++++++++ go/dockerfile/Dockerfile.vercel | 17 ++++ go/dockerfile/README.md | 56 +++++++++++ go/dockerfile/go.mod | 3 + go/dockerfile/main.go | 158 ++++++++++++++++++++++++++++++++ go/dockerfile/public/index.html | 71 ++++++++++++++ go/dockerfile/server_test.go | 62 +++++++++++++ 7 files changed, 408 insertions(+) create mode 100644 go/dockerfile/.gitignore create mode 100644 go/dockerfile/Dockerfile.vercel create mode 100644 go/dockerfile/README.md create mode 100644 go/dockerfile/go.mod create mode 100644 go/dockerfile/main.go create mode 100644 go/dockerfile/public/index.html create mode 100644 go/dockerfile/server_test.go diff --git a/go/dockerfile/.gitignore b/go/dockerfile/.gitignore new file mode 100644 index 0000000000..38740e34dd --- /dev/null +++ b/go/dockerfile/.gitignore @@ -0,0 +1,41 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool +*.out + +# Go workspace file +go.work +go.work.sum + +# Dependency directories +vendor/ + +# Build output +server +main + +# Environment files +.env +.env*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Vercel +.vercel diff --git a/go/dockerfile/Dockerfile.vercel b/go/dockerfile/Dockerfile.vercel new file mode 100644 index 0000000000..3d6e8506f6 --- /dev/null +++ b/go/dockerfile/Dockerfile.vercel @@ -0,0 +1,17 @@ +# Dockerfile.vercel +# Vercel detects this file, builds the image, and runs it on Fluid compute. +# The server only needs to listen on $PORT. + +FROM golang:1.24-alpine AS build +WORKDIR /src +COPY . . +# Static, stripped binary so it runs on a minimal image. +RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /server . + +FROM alpine:3.20 +# Run as a non-root user. +RUN adduser -D -u 10001 app +USER app +COPY --from=build /server /server +EXPOSE 3000 +ENTRYPOINT ["/server"] diff --git a/go/dockerfile/README.md b/go/dockerfile/README.md new file mode 100644 index 0000000000..b17ba03f74 --- /dev/null +++ b/go/dockerfile/README.md @@ -0,0 +1,56 @@ +# Go Dockerfile Starter + +Deploy a Go HTTP server to Vercel using a `Dockerfile.vercel`, with zero configuration. Vercel builds the image, stores it, and runs it on [Fluid compute](https://vercel.com/blog/introducing-fluid-compute) — your server only needs to listen on `$PORT`. + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?demo-description=Deploy%20a%20Go%20HTTP%20server%20to%20Vercel%20from%20a%20Dockerfile.&demo-title=Go%20Dockerfile%20Starter&demo-url=https%3A%2F%2Fvercel-plus-go-dockerfile.labs.vercel.dev%2F&from=templates&project-name=Go%20Dockerfile%20Starter&repository-name=go-dockerfile-starter&repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fexamples%2Ftree%2Fmain%2Fgo%2Fdockerfile&skippable-integrations=1) + +_Live Example: https://vercel-plus-go-dockerfile.labs.vercel.dev/_ + +## How it works + +This example ships a `Dockerfile.vercel`. On deploy, Vercel detects it, builds the container image, and runs it on Fluid compute. The only requirement is that your server listens on the port from `$PORT`. + +The app uses only the Go standard library and exposes: + +- `GET /` — a landing page that renders the live container info +- `GET /healthz` — health check +- `GET /api/info` — live container/runtime info (hostname, region, Go version, CPUs, uptime) +- `GET /api/echo` — echoes your request + +It also shows a few production niceties: structured logging with `log/slog`, sensible `http.Server` timeouts, and graceful shutdown on `SIGTERM` (which Vercel sends when scaling an instance down). + +## Running Locally + +Make sure you have Go installed (from [go.dev](https://go.dev/dl/)). + +Start the server on http://localhost:3000: + +```bash +go run . +``` + +Run the tests: + +```bash +go test ./... +``` + +Or build and run the container exactly as Vercel does: + +```bash +docker build -f Dockerfile.vercel -t go-dockerfile . +docker run --rm -e PORT=3000 -p 3000:3000 go-dockerfile +``` + +## Deploying to Vercel + +Deploy your project to Vercel with the following command: + +```bash +npm install -g vercel +vercel --prod +``` + +Or `git push` to your repository with our [git integration](https://vercel.com/docs/deployments/git). + +To view the source code for this example, [visit the example repository](https://github.com/vercel/examples/tree/main/go/dockerfile). diff --git a/go/dockerfile/go.mod b/go/dockerfile/go.mod new file mode 100644 index 0000000000..d14f62068d --- /dev/null +++ b/go/dockerfile/go.mod @@ -0,0 +1,3 @@ +module vercel-go-dockerfile + +go 1.24 diff --git a/go/dockerfile/main.go b/go/dockerfile/main.go new file mode 100644 index 0000000000..bd86b42bbf --- /dev/null +++ b/go/dockerfile/main.go @@ -0,0 +1,158 @@ +// Command server is a small Go HTTP service that runs on Vercel from a Dockerfile.vercel. +// +// Vercel detects the Dockerfile.vercel, builds the image, and runs it on Fluid compute. +// The only requirement is that the server listens on the port given by $PORT. +package main + +import ( + "context" + "embed" + "encoding/json" + "errors" + "log/slog" + "net/http" + "os" + "os/signal" + "runtime" + "syscall" + "time" +) + +//go:embed public +var publicFS embed.FS + +var startTime = time.Now() + +func main() { + slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil))) + + if err := run(); err != nil { + slog.Error("server exited with error", "err", err) + os.Exit(1) + } +} + +func run() error { + // Vercel sets $PORT. Default to 3000 for local development. + port := os.Getenv("PORT") + if port == "" { + port = "3000" + } + + srv := &http.Server{ + Addr: ":" + port, + Handler: logRequests(routes()), + ReadHeaderTimeout: 5 * time.Second, + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + IdleTimeout: 60 * time.Second, + } + + // Vercel sends SIGTERM when scaling an instance down. Shut down gracefully. + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + go func() { + slog.Info("listening", "addr", srv.Addr) + if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + slog.Error("listen failed", "err", err) + stop() + } + }() + + <-ctx.Done() + slog.Info("shutting down") + + shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + return srv.Shutdown(shutdownCtx) +} + +func routes() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("GET /{$}", handleIndex) + mux.HandleFunc("GET /healthz", handleHealth) + mux.HandleFunc("GET /api/info", handleInfo) + mux.HandleFunc("GET /api/echo", handleEcho) + return mux +} + +func handleIndex(w http.ResponseWriter, r *http.Request) { + page, err := publicFS.ReadFile("public/index.html") + if err != nil { + http.Error(w, "not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _, _ = w.Write(page) +} + +func handleHealth(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) +} + +func handleInfo(w http.ResponseWriter, r *http.Request) { + host, _ := os.Hostname() + writeJSON(w, http.StatusOK, map[string]any{ + "message": "Hello from a Go container on Vercel 👋", + "hostname": host, + "region": envOr("VERCEL_REGION", "local"), + "env": envOr("VERCEL_ENV", "development"), + "goVersion": runtime.Version(), + "os": runtime.GOOS, + "arch": runtime.GOARCH, + "cpus": runtime.NumCPU(), + "uptime": time.Since(startTime).Round(time.Millisecond).String(), + "time": time.Now().UTC().Format(time.RFC3339), + }) +} + +func handleEcho(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, map[string]any{ + "method": r.Method, + "path": r.URL.Path, + "query": r.URL.Query(), + "host": r.Host, + "userAgent": r.UserAgent(), + }) +} + +func writeJSON(w http.ResponseWriter, status int, v any) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(status) + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + _ = enc.Encode(v) +} + +func envOr(key, fallback string) string { + if v := os.Getenv(key); v != "" { + return v + } + return fallback +} + +// logRequests is a small structured-logging middleware. +func logRequests(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + sw := &statusWriter{ResponseWriter: w, status: http.StatusOK} + next.ServeHTTP(sw, r) + slog.Info("request", + "method", r.Method, + "path", r.URL.Path, + "status", sw.status, + "duration", time.Since(start).String(), + ) + }) +} + +type statusWriter struct { + http.ResponseWriter + status int +} + +func (w *statusWriter) WriteHeader(code int) { + w.status = code + w.ResponseWriter.WriteHeader(code) +} diff --git a/go/dockerfile/public/index.html b/go/dockerfile/public/index.html new file mode 100644 index 0000000000..a193fae83f --- /dev/null +++ b/go/dockerfile/public/index.html @@ -0,0 +1,71 @@ + + + + + + Go on Vercel, from a Dockerfile + + + +
+

Go on Vercel, from a Dockerfile 🐳

+

+ This is a plain Go HTTP server, deployed to Vercel with a + Dockerfile.vercel. Vercel built the image and is running it on + Fluid compute. It just listens on $PORT. +

+ +
+

Live container info — GET /api/info

+
loading…
+
+ + + +
+ Built with the Go standard library. See the + source. +
+
+ + + + diff --git a/go/dockerfile/server_test.go b/go/dockerfile/server_test.go new file mode 100644 index 0000000000..13d49d086c --- /dev/null +++ b/go/dockerfile/server_test.go @@ -0,0 +1,62 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestHealthz(t *testing.T) { + rec := httptest.NewRecorder() + routes().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/healthz", nil)) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) + } + var body map[string]string + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + if body["status"] != "ok" { + t.Errorf("status = %q, want %q", body["status"], "ok") + } +} + +func TestInfo(t *testing.T) { + rec := httptest.NewRecorder() + routes().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/info", nil)) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) + } + var body map[string]any + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + if _, ok := body["goVersion"]; !ok { + t.Error("response is missing goVersion") + } +} + +func TestIndexServesHTML(t *testing.T) { + rec := httptest.NewRecorder() + routes().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil)) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) + } + if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/html") { + t.Errorf("Content-Type = %q, want text/html", ct) + } +} + +func TestUnknownRouteIs404(t *testing.T) { + rec := httptest.NewRecorder() + routes().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/does-not-exist", nil)) + + if rec.Code != http.StatusNotFound { + t.Errorf("status = %d, want %d", rec.Code, http.StatusNotFound) + } +} From 755f01784820435b8c2b44a65ac8fde9eacace40 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Mon, 29 Jun 2026 14:05:32 -0400 Subject: [PATCH 2/2] Update Live Example URL --- go/dockerfile/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go/dockerfile/README.md b/go/dockerfile/README.md index b17ba03f74..6ff706d79f 100644 --- a/go/dockerfile/README.md +++ b/go/dockerfile/README.md @@ -2,9 +2,9 @@ Deploy a Go HTTP server to Vercel using a `Dockerfile.vercel`, with zero configuration. Vercel builds the image, stores it, and runs it on [Fluid compute](https://vercel.com/blog/introducing-fluid-compute) — your server only needs to listen on `$PORT`. -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?demo-description=Deploy%20a%20Go%20HTTP%20server%20to%20Vercel%20from%20a%20Dockerfile.&demo-title=Go%20Dockerfile%20Starter&demo-url=https%3A%2F%2Fvercel-plus-go-dockerfile.labs.vercel.dev%2F&from=templates&project-name=Go%20Dockerfile%20Starter&repository-name=go-dockerfile-starter&repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fexamples%2Ftree%2Fmain%2Fgo%2Fdockerfile&skippable-integrations=1) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?demo-description=Deploy%20a%20Go%20HTTP%20server%20to%20Vercel%20from%20a%20Dockerfile.&demo-title=Go%20Dockerfile%20Starter&demo-url=https%3A%2F%2Fgo-dockerfile.vercel.app%2F&from=templates&project-name=Go%20Dockerfile%20Starter&repository-name=go-dockerfile-starter&repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fexamples%2Ftree%2Fmain%2Fgo%2Fdockerfile&skippable-integrations=1) -_Live Example: https://vercel-plus-go-dockerfile.labs.vercel.dev/_ +_Live Example: https://go-dockerfile.vercel.app/_ ## How it works