Go
Deploy Go services on Skiffly with Railpack — module detection, the Go version, static binaries and CGO, choosing the `cmd/` to build, `PORT`, Postgres and Redis, migrations and health checks.
What Railpack detects#
| Looks at | Effect |
|---|---|
go.mod, go.work or main.go in the root directory | the service is a Go app |
mise.toml/.tool-versions, then the go directive in go.mod, then RAILPACK_GO_VERSION | the toolchain version (default 1.23) |
main package in the root, or the first cmd/* with main.go | what gets compiled: go build -ldflags="-w -s" -o out |
RAILPACK_GO_BIN=api | build ./cmd/api instead of the first one found |
RAILPACK_GO_WORKSPACE_MODULE=services/api | with go.work, which module to build |
CGO_ENABLED=1 | dynamic binary with gcc/libc available at build and runtime (needed by mattn/go-sqlite3, some image libraries); the default is a static binary with CGO_ENABLED=0 |
The final image contains the binary and little else; the start command is the compiled binary. Dependencies are downloaded with the Go module cache reused between builds. go generate, sqlc generate or templ steps that must run before compiling go into the build command (sqlc generate && go build -o out ./cmd/api), which replaces Railpack's build step entirely — keep the -o out so the start command still finds the binary, or set a start command of your own.
Port#
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Fatal(http.ListenAndServe(":"+port, mux))":"+port binds to all interfaces; "localhost:"+port does not and the deployment fails its health check. Frameworks (Gin, Echo, Fiber, Chi) take the same address string. Skiffly sets PORT to the service port, 8080 when unset — so a Go service that defaults to 8080 needs no configuration at all.
Postgres and Redis#
skiffly deploy --template postgres
skiffly deploy --template redis
skiffly variables set 'DATABASE_URL=${{Postgres.DATABASE_URL}}' 'REDIS_URL=${{Redis.REDIS_URL}}'pgx (pgxpool.New(ctx, os.Getenv("DATABASE_URL"))), database/sql with lib/pq, GORM and sqlc all accept the URL. go-redis parses REDIS_URL with redis.ParseURL. Both hosts are on the private network only.
Migrations#
No release phase on Skiffly. Options that work:
- On boot, in code —
golang-migrateorgooseas a library, run beforeListenAndServe(both take an advisory lock so replicas do not race). Startup has five minutes before the health check gives up. - Start command — install the CLI at build time and chain: build command
go install github.com/pressly/goose/v3/cmd/goose@latest && go build -o out ./cmd/api, start commandsh -c "goose -dir migrations postgres \"$DATABASE_URL\" up && ./out". The image needssh; Railpack's runtime image has it. - One-off —
skiffly ssh -- ./out migrateif the binary has a subcommand, or run the migration tool from your machine throughskiffly connect postgres --print.
Health check#
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })Set /healthz as the healthcheck path. Handle SIGTERM with http.Server.Shutdown so rollouts and restarts finish in-flight requests (Skiffly waits for the container to exit after sending the signal).
Workers and cron#
A worker is a second service from the same repository with RAILPACK_GO_BIN=worker (or a build command targeting ./cmd/worker) and no domain. Scheduled jobs: a service with a cron schedule whose binary runs once and exits.
Private modules#
Railpack downloads modules with no credentials. For modules in private repositories either commit a vendor/ directory (go mod vendor; the build uses it automatically), or switch the service to a Dockerfile where you can mount a token as a BuildKit secret (RUN --mount=type=secret,id=GITHUB_TOKEN …, see Docker).
Common problems#
| Symptom | Fix |
|---|---|
[railpack] no main package found | main.go is not in the root or cmd/*: set RAILPACK_GO_BIN or a build command |
Deployment FAILED, log shows listening on localhost:8080 | bind to : + port |
go: go.mod requires go >= 1.24 | Railpack honours the go directive; make sure it is a released version, or pin RAILPACK_GO_VERSION |
undefined: sqlite3 / cgo: C compiler not found | CGO_ENABLED=1, or use a pure-Go driver (modernc.org/sqlite) |
x509: certificate signed by unknown authority at runtime | the runtime image has CA certificates; this usually means a corporate proxy or an HTTP URL, not a missing bundle |
Binary works locally, exec format error on Skiffly | cross-compiled for the wrong arch; let Railpack build it (nodes are linux/amd64) |
Example#
import { defineSkiffly, github, postgres, project, service } from "@skiffly/config";
export default defineSkiffly(() => {
const db = postgres("db");
const api = service("api", {
source: github("acme/api"),
healthcheck: "/healthz",
port: 8080,
env: { DATABASE_URL: db.env.DATABASE_URL, RAILPACK_GO_BIN: "api" },
domain: true,
});
return project("acme-api", { resources: [db, api] });
});Landing: /deploy/go