Building Docker Images

I this example we are building a multistage small image for go binary. This is usually a goto example on devops classes/tutorials etc.

#Keypoints

  1. Multistage builds are used.
  2. UPX used to reduce binary size
  3. Running Go Infrastructure within docker image.

#Read More

#Examples

//  - Makefile - 
VERSION ?= 1.16


# ~~~ Build ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

version:              ## Prints Go version we going to work with.
	echo $(VERSION)

bases-pull:           ## Downlaods Base Images
	@docker pull golang:$(VERSION)-alpine
	@docker pull golang:$(VERSION)-buster


bases-list:            ## List Base Images
	@docker images | grep golang

buster:  # Build Debian Buster Image
	docker build -t butuzov/goapp-debian -f debian.Dockerfile .

distroless: # Build Distroless Image
	docker build -t butuzov/goapp-distroless -f distroless.Dockerfile .

alpine:        ## Build Alpine Image
	docker build -t butuzov/goapp-alpine -f alpine.Dockerfile .


all: alpine buster distroless ## Build all images

cleanfailed:          ## Clean untagged images
	@docker images -q -f "dangling=true" | xargs -L1 docker rmi
	@docker ps -q -f "status=exited" | xargs -L1 docker rm


# ~~~ Docker Compose ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

up: # Startup Environment
	docker-compose up --no-recreate -d

down: # Shutdown Environment
	docker-compose down -v

logs: # See Logs
	docker-compose logs

ls:  # Show resulted images
	@docker images | grep "butuzov/goapp"

clean: down cleanfailed    ## Teardown All Bases/Failed/Resulted
	@ echo "Cleanup Docker Images"
	@ docker images | grep "butuzov/goapp" | awk '{print $$3}' | xargs -L1 docker rmi
	@ @docker images | grep "golang" | awk '{print $$3}' | xargs -L1 docker rmi
//  - main.go - 
package main

import (
	"log"
	"os"

	"github.com/gofiber/fiber/v3"
)

const AppPort = "8080"

func setupApp() *fiber.App {
	app := fiber.New()

	app.Get("/ping", func(c fiber.Ctx) error {
		return c.SendString("pong")
	})

	app.Get("/", func(c fiber.Ctx) error {
		return c.SendString("ok")
	})

	return app
}

func port() string {
	port := os.Getenv("APP_PORT")
	if port != "" {
		return port
	}
	return AppPort
}

func main() {
	app := setupApp()
	if err := app.Listen(":" + port()); err != nil {
		log.Fatal(err)
	}
}
//  - docker-compose.yaml - 
services:

  app_alpine:
    image: "butuzov/goapp-alpine:latest"
    build:
      context: .
      dockerfile: alpine.Dockerfile
    ports:
      - 8093:8091
    environment:
      APP_PORT: 8091 
    networks:
      - docker-network

  app_buster:
    image: "butuzov/goapp-debian:latest"
    build:
      context: .
      dockerfile: debian.Dockerfile
    ports:
      - 8092:8091
    environment:
      APP_PORT: 8091 
    networks:
      - docker-network

  app_distroless:
    image: "butuzov/goapp-distroless:latest"
    build:
      context: .
      dockerfile: distroless.Dockerfile
    ports:
      - 8091:8090
    environment:
      APP_PORT: 8090
    command: /main
    networks:
      - docker-network

networks:
  docker-network:
    driver: bridge
# - alpine.Dockerfile - 
ARG BUILDPLATFORM=linux/amd64

# Prepare Builder Platrform
FROM --platform=${BUILDPLATFORM} golang:1.26.1-alpine AS builder

ENV CGO_ENABLED=0 
ENV GOARCH=amd64 
ENV GOOS=linux
ENV GOPATH=/root/go

RUN apk add --no-cache curl git bash make tzdata ca-certificates
COPY --from=golangci/golangci-lint:v2.11-alpine /usr/bin/golangci-lint /usr/bin
RUN go install github.com/go-delve/delve/cmd/dlv@v1.26.1

# Build Process
FROM builder AS build

WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY  . /build
# Build & Lint & Test
RUN go build -ldflags "-w -s" -o /main
RUN go test ./...
RUN golangci-lint run ./...

# RUN apk update && \
#     apk add upx && \
#     upx --brute /main


# Final scratch image
FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo/
COPY --from=build /main /
COPY --from=build /root/go/bin/dlv /bin/

ENV APP_PORT=8090
EXPOSE $APP_PORT

WORKDIR /
# CMD [ "/main" ]
CMD [ "/bin/dlv", "--listen=:2345", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", "/main", "--continue"]
# - debian.Dockerfile - 
ARG BUILDPLATFORM=linux/amd64

# --- Builder ------------------------------------------------------------------
FROM --platform=${BUILDPLATFORM} golang:1.26.1-bookworm AS builder

ENV CGO_ENABLED=0 
ENV GOARCH=amd64 
ENV GOOS=linux
ENV GOPATH=/root/go

RUN go install github.com/go-delve/delve/cmd/dlv@v1.26.1
COPY --from=golangci/golangci-lint:v2.11 /usr/bin/golangci-lint /usr/bin

# --- Build --------------------------------------------------------------------
FROM builder AS build

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

# --- Code Quality -- Build & Lint & Test --------------------------------------
RUN go test ./...
RUN golangci-lint run ./...
RUN go build -o /main

# --- Packing it ---------------------------------------------------------------
RUN echo "deb http://deb.debian.org/debian bookworm-backports main" > /etc/apt/sources.list.d/backports.list && \
    apt-get update && apt-get install -y -t bookworm-backports upx-ucl
RUN upx --brute /main


# --- Prod Ready Image ---------------------------------------------------------
FROM gcr.io/distroless/base-debian13
COPY --from=build /main            /
COPY --from=build /root/go/bin/dlv /bin/

ENV APP_PORT=8090
EXPOSE $APP_PORT

WORKDIR /
# CMD [ "/main" ]
CMD [ "/bin/dlv", "--listen=:2345", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", "/main", "--continue"]
# - distroless.Dockerfile - 
ARG BUILDPLATFORM=linux/amd64

# --- Builder ------------------------------------------------------------------
FROM --platform=${BUILDPLATFORM} golang:1.26.1-bookworm AS builder

ENV CGO_ENABLED=0 
ENV GOARCH=amd64 
ENV GOOS=linux
ENV GOPATH=/root/go

RUN go install github.com/go-delve/delve/cmd/dlv@v1.26.1
COPY --from=golangci/golangci-lint:v2.11 /usr/bin/golangci-lint /usr/bin

# --- Build --------------------------------------------------------------------
FROM builder AS build

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

# --- Code Quality -- Build & Lint & Test --------------------------------------
RUN go test ./...
RUN golangci-lint run ./...
RUN go build -o /main

# --- Packing it ---------------------------------------------------------------
# RUN echo "deb http://deb.debian.org/debian bookworm-backports main" > /etc/apt/sources.list.d/backports.list && \
#     apt-get update && apt-get install -y -t bookworm-backports upx-ucl
# RUN upx --brute /main


# --- Prod Ready Image ---------------------------------------------------------
FROM gcr.io/distroless/static:nonroot
COPY --from=build /main /
COPY --from=build /root/go/bin/dlv /bin/

ENV APP_PORT=8090
EXPOSE $APP_PORT

WORKDIR /
CMD [ "/main" ]
CMD [ "/bin/dlv", "--listen=:2345", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", "/main", "--continue"]