Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions go/dockerfile/.gitignore
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions go/dockerfile/Dockerfile.vercel
Original file line number Diff line number Diff line change
@@ -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"]
56 changes: 56 additions & 0 deletions go/dockerfile/README.md
Original file line number Diff line number Diff line change
@@ -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%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://go-dockerfile.vercel.app/_

## 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).
3 changes: 3 additions & 0 deletions go/dockerfile/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module vercel-go-dockerfile

go 1.24
158 changes: 158 additions & 0 deletions go/dockerfile/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
71 changes: 71 additions & 0 deletions go/dockerfile/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Go on Vercel, from a Dockerfile</title>
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
background: #000;
color: #ededed;
font: 16px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
main { width: min(680px, 90vw); padding: 48px 0; }
h1 { font-size: clamp(28px, 5vw, 44px); letter-spacing: -0.02em; margin: 0 0 8px; }
p.lede { color: #a1a1a1; margin: 0 0 28px; }
code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
code { background: #1a1a1a; border: 1px solid #2a2a2a; border-radius: 6px; padding: 2px 6px; font-size: 0.9em; }
.card { background: #0a0a0a; border: 1px solid #2a2a2a; border-radius: 12px; padding: 20px 22px; }
.card h2 { font-size: 13px; text-transform: uppercase; letter-spacing: 0.08em; color: #888; margin: 0 0 12px; }
pre { margin: 0; white-space: pre-wrap; word-break: break-word; font-size: 13px; color: #d4d4d4; }
.routes { margin-top: 22px; display: flex; flex-wrap: wrap; gap: 10px; }
.routes a { color: #ededed; text-decoration: none; background: #1a1a1a; border: 1px solid #2a2a2a; border-radius: 8px; padding: 8px 12px; font-size: 14px; }
.routes a:hover { border-color: #555; }
footer { margin-top: 28px; color: #666; font-size: 13px; }
footer a { color: #888; }
</style>
</head>
<body>
<main>
<h1>Go on Vercel, from a Dockerfile 🐳</h1>
<p class="lede">
This is a plain Go HTTP server, deployed to Vercel with a
<code>Dockerfile.vercel</code>. Vercel built the image and is running it on
Fluid compute. It just listens on <code>$PORT</code>.
</p>

<div class="card">
<h2>Live container info — <code>GET /api/info</code></h2>
<pre id="info">loading…</pre>
</div>

<div class="routes">
<a href="/api/info">/api/info</a>
<a href="/api/echo?hello=world">/api/echo</a>
<a href="/healthz">/healthz</a>
</div>

<footer>
Built with the Go standard library. See the
<a href="https://github.com/vercel/examples/tree/main/go/dockerfile">source</a>.
</footer>
</main>

<script>
fetch("/api/info")
.then((r) => r.json())
.then((d) => {
document.getElementById("info").textContent = JSON.stringify(d, null, 2);
})
.catch((e) => {
document.getElementById("info").textContent = "Failed to load: " + e;
});
</script>
</body>
</html>
Loading