Deploying go/gin to render
how to deploy a go backend that lives inside a turborepo monorepo — build cmd, openapi path, gin mode, all the gotchas
service setup on render
when creating a new web service on render:
repo → select your github repo
branch → main
root dir → . (leave as repo root, never change this for monorepos)
runtime → Go (render defaults to Node because it sees package.json — change it)never set root dir to apps/backend — render needs to see the full monorepo to run commands across packages.
build command
depends on where your main.go lives:
# if main.go is at apps/backend/cmd/server/main.go
cd packages/openapi && bun install && bun run generate && cp openapi.json ../../apps/backend/openapi.json && cd ../../apps/backend && go build -o main ./cmd/server
# if main.go is at apps/backend/cmd/main.go
cd packages/openapi && bun install && bun run generate && cp openapi.json ../../apps/backend/openapi.json && cd ../../apps/backend && go build -o main ./cmd
# if main.go is at apps/backend/main.go
cd packages/openapi && bun install && bun run generate && cp openapi.json ../../apps/backend/openapi.json && cd ../../apps/backend && go build -o main .the cp openapi.json step is important — copies the generated file next to the binary so it can find it at runtime.
start command
# same pattern — match where your binary ends up
apps/backend/mainenv vars on render
GIN_MODE=release # not "production" — gin specifically uses "release"
PORT=5000 # or whatever port your server listens on
DATABASE_URL= # your neon/postgres connection string
JWT_ACCESS_SECRET= # your secrets
JWT_REFRESH_SECRET=
OPENAPI_PATH= # see belowsetting OPENAPI_PATH
render runs your binary from a specific absolute path. check your deploy logs for a line like:
Generated openapi.json at /opt/render/project/go/src/github.com/YourUsername/yourrepo/apps/backend/openapi.jsonor look for where your server starts:
source=/opt/render/project/go/src/github.com/YourUsername/yourrepo/apps/backend/cmd/server/main.goset OPENAPI_PATH to the absolute path of openapi.json in your backend:
OPENAPI_PATH=/opt/render/project/go/src/github.com/YourUsername/yourrepo/apps/backend/openapi.jsonin main.go the fallback looks like this:
openapiPath := os.Getenv("OPENAPI_PATH")
if openapiPath == "" {
openapiPath = filepath.Join("openapi.json") // relative — works locally, breaks on render
}the relative path works locally because you run the binary from the backend folder. on render the working directory is different — always use the absolute path via env var.
mistakes i made
render defaulted to Node runtime
render sees package.json in the monorepo root and assumes Node. manually change runtime to Go.
turbo not found during build
/usr/bin/bash: line 1: turbo: command not found
error: script "generate" exited with code 127render doesn't have turbo installed globally. don't use turbo run generate in the build cmd — call the script directly with bun:
cd packages/openapi && bun install && bun run generateopenapi.json returning 500
[GIN] | 500 | GET "/openapi.json"
Document could not be loadedthe file was generated in packages/openapi/ but the binary looked for it relative to its own working directory. fix — copy the file to apps/backend/ during build and set OPENAPI_PATH to the absolute path on render.
set root dir to apps/backend
build cmd failed because cd packages/openapi doesn't exist relative to apps/backend. always leave root dir as ..