Why Your Go Project Begs for CI/CD Like a Dog Begs for a Walk
You've written a cool microservice in Go. It's fast, tests pass, the code is beautiful. But every morning you manually run go build, upload the binary to the server via SSH, and pray that nothing crashes. Sound familiar? If so, I have news for you: you're spending 15% of your life on something a robot can do.
CI/CD (Continuous Integration / Continuous Deployment) is not just a buzzword from DevOps job postings. It's your personal assistant that:
- runs tests on every push to Git;
- builds a binary for Linux/amd64, even if you're on a Mac;
- sends notifications to Telegram if the build fails;
- automatically deploys to the server when you merge a PR into main.
In this article, we'll set up a CI/CD pipeline for Go from scratch. No fluff, no "install Jenkins on 20 servers." Just GitHub Actions, Docker, and a bit of magic. Let's go!
What We'll Need
For the setup, we'll need:
- A GitHub repository (you can create a test one right now);
- A Go project with modules (we'll write a simple HTTP server);
- A Docker Hub account (or any other registry);
- 5 minutes of attention and a desire to automate the routine.
All the code we'll use is publicly available. You can copy it and adapt it to your project. Just remember to replace the tokens and repository names with your own.
Step 1: Write a Simple Go Application
For demonstration, let's take a minimal HTTP server. Create a file main.go:
package main
import ( "fmt" "net/http")
func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello from Go CI/CD pipeline!") })
http.ListenAndServe(":8080", nil)}And a file main_test.go to check functionality:
package main
import ( "net/http" "net/http/httptest" "testing")
func TestHelloHandler(t *testing.T) { req := httptest.NewRequest("GET", "/", nil) w := httptest.NewRecorder()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello from Go CI/CD pipeline!") })
http.DefaultServeMux.ServeHTTP(w, req)
if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) }}Initialize the module:
go mod init github.com/your-username/go-cicd-examplego mod tidyCheck that everything works:
go test ./... -vgo run main.goIf you see ok and a greeting on localhost:8080 — you're awesome. Let's move on.
Step 2: Create a Dockerfile for the Go Application
Containerization is a mandatory step in modern deployment. We'll build a static binary (without CGO) and package it into a minimal image based on scratch or alpine. This will give us an image size of about 10–15 MB instead of 300+ MB.
Create a Dockerfile in the project root:
FROM golang:1.22-alpine AS builder
WORKDIR /appCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 GOOS=linux go build -o main .
FROM alpine:latestRUN apk --no-cache add ca-certificatesWORKDIR /root/COPY --from=builder /app/main .EXPOSE 8080CMD ["./main"]Note: we use multi-stage builds. The first stage is compilation in a full Go environment. The second is a minimal Alpine image, where only the binary is copied. This is the industry standard for Go applications.
Build the image locally and check:
docker build -t go-cicd-example .docker run -p 8080:8080 go-cicd-exampleIf everything is ok — let's move on to