Skip to content

Container Builds

Create minimal, secure container images with multi-stage builds.


Standard Pattern

# Build stage
FROM golang:1.23-alpine AS builder

WORKDIR /app

# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download

# Build binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
    -ldflags="-w -s -X main.version=${VERSION:-dev}" \
    -o /myctl \
    ./main.go

# Runtime stage
FROM gcr.io/distroless/static-debian12:nonroot

COPY --from=builder /myctl /myctl

USER nonroot:nonroot

ENTRYPOINT ["/myctl"]

Distroless Benefits

Security First

Never use latest tags in production. Pin to specific versions and scan images for vulnerabilities in CI.

Distroless is preferred for security:

  • No shell (reduces attack surface)
  • No package manager
  • Non-root user by default
  • Automatic security updates via base image
# Use nonroot variant for security
FROM gcr.io/distroless/static-debian12:nonroot

# Or debug variant for troubleshooting (includes shell)
FROM gcr.io/distroless/static-debian12:debug-nonroot

Version Injection

Build-Time Variables

// main.go
package main

import (
    "myctl/cmd"
)

// Set via ldflags at build time
var (
    version = "dev"
    commit  = "unknown"
    date    = "unknown"
)

func main() {
    cmd.SetVersionInfo(version, commit, date)
    cmd.Execute()
}
// cmd/version.go
package cmd

import (
    "fmt"
    "runtime"

    "github.com/spf13/cobra"
)

var (
    Version string
    Commit  string
    Date    string
)

func SetVersionInfo(version, commit, date string) {
    Version = version
    Commit = commit
    Date = date
}

var versionCmd = &cobra.Command{
    Use:   "version",
    Short: "Print version information",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Printf("Version:    %s\n", Version)
        fmt.Printf("Commit:     %s\n", Commit)
        fmt.Printf("Built:      %s\n", Date)
        fmt.Printf("Go version: %s\n", runtime.Version())
        fmt.Printf("OS/Arch:    %s/%s\n", runtime.GOOS, runtime.GOARCH)
    },
}

func init() {
    rootCmd.AddCommand(versionCmd)
}

Build Script

#!/bin/bash
set -euo pipefail

VERSION="${VERSION:-$(git describe --tags --always --dirty 2>/dev/null || echo 'dev')}"
COMMIT="${COMMIT:-$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')}"
DATE="${DATE:-$(date -u +"%Y-%m-%dT%H:%M:%SZ")}"

go build \
    -ldflags="-w -s \
        -X main.version=${VERSION} \
        -X main.commit=${COMMIT} \
        -X main.date=${DATE}" \
    -o myctl \
    ./main.go

Multi-Architecture Builds

Dockerfile for Multi-Arch

FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS builder

ARG TARGETPLATFORM
ARG TARGETOS
ARG TARGETARCH
ARG VERSION=dev

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
    -ldflags="-w -s -X main.version=${VERSION}" \
    -o /myctl \
    ./main.go

FROM gcr.io/distroless/static-debian12:nonroot

COPY --from=builder /myctl /myctl

USER nonroot:nonroot

ENTRYPOINT ["/myctl"]

Security Context

When deploying, use a secure pod security context:

securityContext:
  runAsNonRoot: true
  runAsUser: 65532
  runAsGroup: 65532
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false
  capabilities:
    drop:
      - ALL

Small image + non-root + read-only = secure container.

Comments