Ship It (Go Edition)
Containerize your app and publish to GHCR with GitHub Actions
This guide adds a Dockerfile that bakes in the Git SHA, and a GitHub Actions workflow that builds and pushes to GitHub Container Registry (GHCR). Tags drive versions. The image carries the commit SHA in both runtime output and OCI labels.
What you'll get
- A multi-stage Dockerfile for Go, tiny final image, SHA baked in
via-ldflags
, and OCI labels for traceability. - A GitHub Action that:
- Builds on
main
and on tags likev1.2.3
. - Pushes to GHCR using
GITHUB_TOKEN
. - Tags images as
v1.2.3
+latest
on releases, ormain-<shortsha>
on branch builds.
- Builds on
Minimal Go service
Create a go service that will return the build infocmd/app/main.go
package main
import (
"encoding/json"
"log"
"net/http"
"os"
)
var (
version = "dev" // set via -ldflags
commitSHA = "unknown"// set via -ldflags
buildDate = "unknown"// set via -ldflags
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{
"ok": "true",
"version": version,
"commit": commitSHA,
"buildDate": buildDate,
})
})
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("listening on %s (commit %s)", port, commitSHA)
log.Fatal(http.ListenAndServe(":"+port, mux))
}
Add a go.mod
module github.com/OWNER/REPO
go 1.22
and a .dockerignore
file
.git
.github
**/tmp
**/*.test
Dockerfile (multi-stage, SHA baked in)
Create the following Dockerfile
# syntax=docker/dockerfile:1.6
########################################
# Builder
########################################
FROM golang:1.22-alpine AS builder
WORKDIR /src
# Build args for metadata
ARG GIT_SHA=unknown
ARG VERSION=dev
ARG BUILD_DATE=unknown
RUN apk add --no-cache ca-certificates upx
# Pre-cache modules
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
# Copy source
COPY . .
# Build static binary with build metadata
RUN --mount=type=cache,target=/go/pkg/mod CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "-s -w -X 'main.version=${VERSION}' -X 'main.commitSHA=${GIT_SHA}' -X 'main.buildDate=${BUILD_DATE}'" -o /out/app ./cmd/app
# Optional: compress for tiny binaries
RUN upx -q /out/app || true
########################################
# Runtime
########################################
FROM gcr.io/distroless/static:nonroot
# OCI labels for traceability
ARG GIT_SHA=unknown
ARG VERSION=dev
ARG BUILD_DATE=unknown
LABEL org.opencontainers.image.title="everyday-devops-go-app" org.opencontainers.image.revision="${GIT_SHA}" org.opencontainers.image.version="${VERSION}" org.opencontainers.image.created="${BUILD_DATE}"
WORKDIR /app
COPY --from=builder /out/app /app/app
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app/app"]
GitHub Actions workflow
Now create a github action to run this build each time there is a push to main or a new tag.github/workflows/docker.yaml
name: Build and Publish Docker image
on:
push:
branches: [ "main" ]
tags: [ "v*" ]
permissions:
contents: read
packages: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/everyday-devops-go-app
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Compute metadata
id: meta
run: |
SHORT_SHA=$(git rev-parse --short HEAD)
echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT
echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_OUTPUT
DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
echo "build_date=${DATE}" >> $GITHUB_OUTPUT
if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
echo "version=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
echo "tags=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${GITHUB_REF_NAME},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> $GITHUB_OUTPUT
else
BRANCH="${GITHUB_REF_NAME//\//-}"
echo "version=${BRANCH}-${SHORT_SHA}" >> $GITHUB_OUTPUT
echo "tags=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${BRANCH}-${SHORT_SHA}" >> $GITHUB_OUTPUT
fi
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
build-args: |
GIT_SHA=${{ github.sha }}
VERSION=${{ steps.meta.outputs.version }}
BUILD_DATE=${{ steps.meta.outputs.build_date }}
labels: |
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.version=${{ steps.meta.outputs.version }}
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.created=${{ steps.meta.outputs.build_date }}
- name: Show published tags
run: echo "Pushed: ${{ steps.meta.outputs.tags }}"
Repo layout
This gives us the following layout.
.
├── .dockerignore
├── .github
│ └── workflows
│ └── docker.yaml
├── .gitignore
├── Dockerfile
├── cmd
│ └── app
│ └── main.go
└── go.mod
Minimal Go service
Run git init and commit the code.
git init
git add .
git commit -m "first checkin"
Local build
export GIT_SHA=$(git rev-parse HEAD)
export VERSION=dev
export BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
docker build --build-arg GIT_SHA=$GIT_SHA --build-arg VERSION=$VERSION \
--build-arg BUILD_DATE=$BUILD_DATE \
-t ghcr.io/OWNER/ghcr-go-example:dev .
Look at the metadata for the built
docker inspect ghcr.io/OWNER/ghcr-go-example:dev \
--format '{{ index .Config.Labels "org.opencontainers.image.revision" }}'
If you want to push this to ghcr.io
you'll need to create a PAT with write:packages
and read:packages
permissions.
docker login ghcr.io -u USERNAME
Use your github username and the token as the password.
Running
Now run it and check with curl
docker run -p 8080:8080 ghcr.io/OWNER/ghcr-go-example:dev
curl localhost:8080
CI Build
First setup access to the github actions in the Github UI from: https://github.com/USERNAME/ghcr-go-example/pkgs/container/ghcr-go-example
Make sure you add your repository and make the access Write.

Now push the code and go to the Actions tab to see it complete.
git push
Now create a new tag and see that is created as well.
git tag v1.2.3
git push --tags
Conclusion
By adding a multi-stage Dockerfile and an automated GitHub Actions workflow, you’ve made your Go application build, ship, and traceable, without manual intervention. Every push or tag creates a reproducible container image in GHCR, tagged with meaningful versions and stamped with the commit SHA.
This setup makes deployments faster and safer:
- Faster, because builds run automatically with consistent steps.
- Safer, because you can roll back to any tagged image knowing exactly which commit it came from.
In everyday DevOps practice, these small, repeatable patterns are what keep teams shipping with confidence. You’ve now got a foundation that works for a weekend hobby project or a production-grade service, just keep tagging, and keep shipping.