CI/CD for a Go Web Service with GitHub Actions
A Go service with a database runs go test against real Postgres, then builds a single static binary to ship.
This recipe sets up GitHub Actions for a Go web service. It vets, lints, runs tests against a Postgres service container, builds a binary, and deploys on main.
What the pipeline does
- go vet
- Lint with golangci-lint
- go test against Postgres
- go build static binary
- Deploy on main
The workflow
setup-go caches modules and build artifacts automatically.
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app_test
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/app_test?sslmode=disable
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true
- run: go vet ./...
- run: go test -race ./...
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true
- run: CGO_ENABLED=0 go build -o app ./cmd/server
- run: ./deploy.sh appTesting against a database
The postgres service is at localhost:5432; pass DATABASE_URL with sslmode=disable to your test setup. Apply migrations (golang-migrate, goose, or your own) before go test. Use -race to catch data races, which matters for concurrent HTTP handlers.
Deploying
CGO_ENABLED=0 go build produces a static binary with no runtime dependencies - ideal for a scratch or distroless container, or a straight copy to a VPS over SSH. Run it under systemd or in a container behind a load balancer.
Key takeaways
- Run go test -race to catch concurrency bugs in HTTP handlers.
- Build with CGO_ENABLED=0 for a portable static binary.
- Apply migrations against the postgres service before tests run.