Announcement

👇Official Account👇

Welcome to join the group & private message

Article first/tail QR code

Skip to content

Docker Go Project Deployment Practice Guide - Complete Containerization Tutorial

Introduction

Docker has become the de facto standard for containerizing applications, and Go's single binary output makes it an ideal candidate for containerization. This comprehensive guide will walk you through the entire process of containerizing a Go web application, from writing the code to deploying it in production.

By the end of this guide, you'll understand:

  • How to write a Go web application
  • How to create optimized Docker images using multi-stage builds
  • How to set up and use a private Docker registry
  • How to deploy containers in production environments
  • Best practices for Go containerization

Prerequisites

Before starting, ensure you have:

  • Go 1.16+ installed
  • Docker installed and running
  • Basic knowledge of Go and Docker
  • A Linux server (for production deployment)

Step 1: Prepare Go Web Application

Basic Go Web Server

Let's start with a simple Go web application that we'll containerize:

go
package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
)

func sayHello(w http.ResponseWriter, r *http.Request) {
	hostname, _ := os.Hostname()
	response := fmt.Sprintf("Hello from PFinalClub! Running on: %s", hostname)
	fmt.Fprintf(w, response)
}

func getConfig(w http.ResponseWriter, r *http.Request) {
	appId := r.FormValue("app_id")
	if appId == "" {
		http.Error(w, "Please provide app_id parameter", http.StatusBadRequest)
		return
	}
	response := fmt.Sprintf("Configuration for app_id: %s", appId)
	fmt.Fprintf(w, response)
}

func healthCheck(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
	fmt.Fprintf(w, "OK")
}

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8989"
	}

	http.HandleFunc("/", sayHello)
	http.HandleFunc("/get_config", getConfig)
	http.HandleFunc("/health", healthCheck)
	
	log.Printf("[Go Web Server] Starting on port %s", port)
	if err := http.ListenAndServe("0.0.0.0:"+port, nil); err != nil {
		log.Fatal("ListenAndServe failed: ", err)
	}
}

Project Structure

Create the following project structure:

go-web-app/
├── main.go
├── go.mod
├── Dockerfile
└── .dockerignore

Initialize Go Module

bash
go mod init go-web-app
go mod tidy

Create .dockerignore

dockerfile
# .dockerignore
*.md
.git
.gitignore
.env
*.log
.DS_Store

This prevents unnecessary files from being copied into the Docker image, reducing build time and image size.

Step 2: Create Optimized Dockerfile

Multi-Stage Build Strategy

Multi-stage builds are essential for Go applications because they allow us to:

  • Use a full Go environment for building
  • Create a minimal final image with only the binary
  • Reduce image size by 90%+ compared to using golang:alpine directly

Complete Dockerfile

dockerfile
# Stage 1: Build stage
FROM golang:1.21-alpine AS builder

# Install build dependencies
RUN apk add --no-cache git ca-certificates tzdata

# Set working directory
WORKDIR /build

# Copy go mod files
COPY go.mod go.sum ./

# Download dependencies (cached layer)
RUN go mod download

# Copy source code
COPY . .

# Set build arguments
ARG CGO_ENABLED=0
ARG GOOS=linux
ARG GOARCH=amd64

# Build the application
RUN CGO_ENABLED=${CGO_ENABLED} GOOS=${GOOS} GOARCH=${GOARCH} \
    go build -a -installsuffix cgo -ldflags="-w -s" -o app .

# Stage 2: Runtime stage
FROM scratch

# Copy CA certificates for HTTPS
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# Copy timezone data
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo

# Copy the binary
COPY --from=builder /build/app /app

# Expose port
EXPOSE 8989

# Set entrypoint
ENTRYPOINT ["/app"]

Dockerfile Explanation

Stage 1 (Builder):

  • Uses golang:1.21-alpine for building (smaller than full golang image)
  • Installs git and certificates needed for dependencies
  • Downloads dependencies separately (cached layer for faster rebuilds)
  • Builds with optimizations: -ldflags="-w -s" removes debug info

Stage 2 (Runtime):

  • Uses scratch (empty image, ~0MB)
  • Only copies the binary and essential files
  • Results in ~10-15MB final image (vs 300MB+ with golang:alpine)

Alternative: Using distroless

For better security while keeping small size:

dockerfile
# Stage 2 alternative
FROM gcr.io/distroless/static-debian11:nonroot

COPY --from=builder /build/app /app

EXPOSE 8989

ENTRYPOINT ["/app"]

Benefits of distroless:

  • No shell or package manager (reduces attack surface)
  • Still very small (~20MB)
  • Better security than scratch (has some base libraries)

Step 3: Build and Test Docker Image

Build the Image

bash
# Basic build
docker build -t go-web-app:latest .

# Build with tags
docker build -t go-web-app:v1.0.0 -t go-web-app:latest .

# Build with build arguments
docker build --build-arg GOARCH=arm64 -t go-web-app:arm64 .

Verify Image Size

bash
docker images go-web-app

You should see a very small image (10-20MB) compared to using golang:alpine directly (300MB+).

Run Container Locally

bash
# Run in foreground
docker run -p 8989:8989 go-web-app:latest

# Run in background (detached)
docker run -d -p 8989:8989 --name go-web-app go-web-app:latest

# Run with environment variables
docker run -d -p 8989:8989 -e PORT=8989 --name go-web-app go-web-app:latest

Test the Application

bash
# Test hello endpoint
curl http://localhost:8989

# Test config endpoint
curl "http://localhost:8989/get_config?app_id=test123"

# Test health check
curl http://localhost:8989/health

View Container Logs

bash
docker logs go-web-app
docker logs -f go-web-app  # Follow logs

Stop and Remove Container

bash
docker stop go-web-app
docker rm go-web-app

Tag the Image and Push to Private Registry

docker tag web:latest 172.31.1.40:5000/pfinalclub/web:v1

docker push 172.31.1.40:5000/pfinalclub/web:v1

172.31.1.40:5000 is the private registry address

Docker Private Registry Setup

Check if the Registry Has the Image

curl http://172.31.1.40:5000/v2/_catalog

Output:

{"repositories":["pfinalclub/web"]}

Pull and Run the Image on the Server

  • Pull the image
docker pull 172.31.1.40:5000/pfinalclub/web:v1
  • Run the image container
docker run -p 8981:8981 127.0.0.1:5000/pfinalclub/web

docker run -p 8981:8982 127.0.0.1:5000/pfinalclub/web

Last updated: