Deployment

Docker, Railway, and Cloudflare deployment for GOaT apps.

A GOaT app compiles to a single binary. Deployment is copying that binary somewhere and running it.

Dockerfile

# Build stage
FROM golang:1.22-alpine AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o server ./cmd/server

# Runtime stage
FROM scratch
COPY --from=build /app/server /server
COPY --from=build /app/templates /templates
COPY --from=build /app/static /static
EXPOSE 8080
CMD ["/server"]

The final image is ~15MB. No OS, no runtime, just your binary and assets.

Railway

Railway auto-detects Go projects. Add a Procfile or set the start command:

web: ./server

Set environment variables in the Railway dashboard:

DATABASE_URL=postgres://...
REDIS_URL=redis://...
PORT=8080

Railway provides managed PostgreSQL and Redis as add-ons.

Direct Deploy

For a VPS or bare metal:

GOOS=linux GOARCH=amd64 go build -o server ./cmd/server
scp server templates/ static/ yourserver:/app/
ssh yourserver "systemctl restart myapp"

Cloudflare (Static Sites)

For static sites (like this docs site), use Cloudflare Pages or Workers:

wrangler.jsonc:

{
  "name": "myapp",
  "compatibility_date": "2024-01-01",
  "site": {
    "bucket": "./public"
  }
}
hugo && npx wrangler pages deploy public

Environment Variables

Keep config in environment variables:

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

    dbURL := os.Getenv("DATABASE_URL")
    redisURL := os.Getenv("REDIS_URL") // Optional

    pool, _ := pgxpool.New(ctx, dbURL)
    sseHub := service.NewSSEHub(redisURL)

    log.Fatal(http.ListenAndServe(":"+port, mux))
}

Health Checks

Always include a health endpoint:

mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("OK"))
})

Gotchas

  • FROM scratch means no shell. You can’t exec into the container. Use FROM alpine if you need debugging.
  • Copy templates and static files. The binary doesn’t embed them by default. Use go:embed if you want a truly single-file deploy.
  • Set CGO_ENABLED=0 for static compilation. Without it, you’ll get dynamic linking errors on scratch.
  • Redis is optional. Your SSE hub works locally without it. Add Redis when you scale to multiple instances.