Go apps are detected from go.mod and compiled into a single binary. Read PORT, add a health route, and push.
Last updated
These are the defaults. Override any of them in Settings → Build.
We detect Go from go.mod, download modules, and build a binary named out:
main.go at the repo root is built with go build -o out.main.go, the package under cmd/<name> is built. If you have several, set the Build command, for example go build -o out ./cmd/api.CGO_ENABLED=0 is set, so the binary is static. Packages that need cgo (such as mattn/go-sqlite3) won't build; use a pure-Go alternative like modernc.org/sqlite, or set CGO_ENABLED=1 as an env var.package main
import (
"net/http"
"os"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})
http.ListenAndServe(":"+os.Getenv("PORT"), mux)
}If go.mod asks for a newer Go than the build image has, Go downloads that toolchain during the build. This needs the full version in go.mod (go 1.25.0, not go 1.25), which is what go mod init and go get go@latest write.
Files your binary reads at runtime (templates, static assets) are copied with the repo, but //go:embed keeps everything in the binary and avoids path surprises.
Every deploy requests your health check path (/ by default) and only goes live once it returns a 2xx or 3xx status. The platform keeps checking it after that. If / returns 404 or needs a login, add a route like /healthz that returns 200 and set it as the Health Check Path in Settings. Otherwise the deploy fails and the previous version keeps serving.
Add a Postgres addon and DATABASE_URL is injected automatically. pgx accepts it as-is:
pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))Run migrations (goose, golang-migrate, Atlas) when the binary starts, or in front of it in the Start command.
On each deploy, the old container is stopped once the new one is healthy. Catch SIGTERM and call Shutdown so requests in flight finish:
srv := &http.Server{Addr: ":" + os.Getenv("PORT"), Handler: mux}
go srv.ListenAndServe()
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt)
defer stop()
<-ctx.Done()
srv.Shutdown(context.Background())A Go program with no HTTP server (a queue consumer, a Telegram bot) runs as a Background Worker: set the service type in Settings. It skips the HTTP health check, and the old instance is stopped before the new one starts, so two copies never poll at once.
<project-slug>.ethiodeploy.com. Every push to the default branch redeploys, and the old version keeps serving until the new one is healthy.Build fails: go: download go1.25 for linux/amd64: toolchain not available
Cause: go.mod asks for a newer Go than the build image has, written without a patch number, so Go can't find a toolchain to download.
Fix: Write the full version in go.mod, for example go 1.25.0. Go then downloads that toolchain during the build.
Build fails: no Go files in the repo root
Cause: main lives in a subfolder that isn't under cmd/.
Fix: Set the Build command to go build -o out ./path/to/main.
cgo or sqlite3 errors at build or start
Cause: CGO_ENABLED=0 is set by default.
Fix: Use a pure-Go driver, or set CGO_ENABLED=1.
Health check times out
Cause: The server binds 127.0.0.1 or a fixed port.
Fix: Listen on ":"+os.Getenv("PORT").
Deploy fails: GET / returned 404
Cause: / doesn't return a 2xx or 3xx status, so the health check never passes.
Fix: Add /healthz and set it as the Health Check Path.
Still stuck? Check the deployment's build log in the dashboard, or see Troubleshooting.
Can I host a Go app for free?
Yes. The Free plan runs one project with a 512 MB Postgres or Redis. It sleeps after 30 minutes without traffic. A Go binary's small memory footprint fits the Free plan's 256 MB container well.
Do I need a Dockerfile?
No. go.mod is detected and the binary is built and started for you. If your repo has a Procfile, its web line is used as the start command.
Does it work with Gin, Echo, Fiber or Chi?
Yes. They all build the same way. Make the router listen on the PORT environment variable.
Ready to deploy? Sign in with GitHub and pick your repo. No Dockerfile, no YAML.
See the full docs for custom domains, env vars, addons, and billing.