0

Bài 06: file build/package/Dockerfile và Makefile dành cho dự án Task Management

Dưới đây là toàn bộ mã nguồn hoàn chỉnh cho file build/package/Dockerfile và Makefile dành cho dự án Task Management (Bài 10), được thiết kế tối ưu kích thước, hỗ trợ build cả 2 binary (api và cli), bảo mật qua non-root user và tự động inject Git metadata qua -ldflags.

1. build/package/Dockerfile

File Dockerfile đa giai đoạn này sử dụng build argument TARGET_APP để tái sử dụng cùng một cấu hình đóng gói cho cả hai ứng dụng cmd/api hoặc cmd/cli.

Dockerfile

# syntax=docker/dockerfile:1

# ==========================================
# Stage 1: Build Environment
# ==========================================
FROM golang:1.23-alpine AS builder

# Cài đặt git và tzdata phục vụ build/time metadata nếu cần
RUN apk add --no-cache git ca-certificates tzdata

WORKDIR /src

# Tận dụng Docker layer cache cho dependencies
COPY go.mod go.sum* ./
RUN go mod download

# Copy toàn bộ source code
COPY . .

# Khai báo các argument nhận từ Docker build / Makefile
ARG TARGET_APP=api
ARG APP_VERSION=dev
ARG COMMIT_HASH=unknown
ARG BUILD_TIME=unknown

# Thiết lập cờ ldflags nhằm giảm kích thước binary và inject metadata phiên bản
ENV LDFLAGS="-w -s \
  -X 'task-master/internal/config.Version=${APP_VERSION}' \
  -X 'task-master/internal/config.Commit=${COMMIT_HASH}' \
  -X 'task-master/internal/config.BuildTime=${BUILD_TIME}'"

# Compile statically linked binary (tắt CGO)
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -trimpath -ldflags="${LDFLAGS}" \
    -o /bin/app ./cmd/${TARGET_APP}

# ==========================================
# Stage 2: Minimal Runtime (Distroless)
# ==========================================
FROM gcr.io/distroless/static-debian12:nonroot

WORKDIR /

# Copy timezone và SSL certificates
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# Copy binary từ builder stage
COPY --from=builder /bin/app /task-app

# Sử dụng non-root user có sẵn (uid 65532) của Distroless để đảm bảo an toàn
USER nonroot:nonroot

# Port mặc định của REST API
EXPOSE 8080

ENTRYPOINT ["/task-app"]

2. Makefile

File Makefile đặt ở thư mục gốc của repository, đóng vai trò là giao diện điều khiển trung tâm cho toàn bộ vòng đời phát triển: linting, unit test, build local, chạy container và dọn dẹp.

Makefile

# ==============================================================================
# Biến môi trường và metadata dự án
# ==============================================================================
SHELL       := /usr/bin/env bash
MODULE_NAME := task-master

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

BIN_DIR     := bin
DOCKER_IMG  ?= task-master

LDFLAGS := -w -s \
  -X '$(MODULE_NAME)/internal/config.Version=$(VERSION)' \
  -X '$(MODULE_NAME)/internal/config.Commit=$(COMMIT)' \
  -X '$(MODULE_NAME)/internal/config.BuildTime=$(BUILD_TIME)'

# ==============================================================================
# Mục tiêu chung
# ==============================================================================
.PHONY: help
help: ## Hiển thị danh sách các lệnh hỗ trợ
	@echo "Sử dụng: make [lệnh]"
	@echo ""
	@echo "Các lệnh khả dụng:"
	@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "  \033[36m%-18s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)

# ==============================================================================
# Phát triển & Kiểm thử chất lượng mã nguồn
# ==============================================================================
.PHONY: tidy lint test test-coverage
tidy: ## Tự động dọn dẹp và cập nhật go.mod / go.sum
	@echo "==> Đang dọn dẹp Go modules..."
	@go mod tidy

lint: ## Kiểm tra mã nguồn bằng golangci-lint
	@echo "==> Đang kiểm tra mã nguồn (lint)..."
	@if command -v golangci-lint >/dev/null 2>&1; then \
		golangci-lint run ./...; \
	else \
		echo "golangci-lint chưa được cài đặt. Đang chạy qua go run..."; \
		go run github.com/golangci/golangci-lint/cmd/golangci-lint@latest run ./...; \
	fi

test: ## Chạy toàn bộ Unit Tests
	@echo "==> Đang thực thi Unit Tests..."
	@go test -v -race -cover ./internal/...

test-coverage: ## Xuất báo cáo độ phủ mã nguồn ra HTML
	@echo "==> Đang phân tích Coverage..."
	@mkdir -p coverage
	@go test -race -coverprofile=coverage/coverage.out ./internal/...
	@go tool cover -html=coverage/coverage.out -o coverage/coverage.html
	@echo "Báo cáo coverage đã tạo tại: coverage/coverage.html"

# ==============================================================================
# Biên dịch Local Binaries
# ==============================================================================
.PHONY: build build-api build-cli clean
build: build-api build-cli ## Biên dịch tất cả binary (api + cli)

build-api: ## Biên dịch binary cho API Server
	@echo "==> Đang build API Server (phiên bản: $(VERSION))..."
	@mkdir -p $(BIN_DIR)
	@CGO_ENABLED=0 go build -trimpath -ldflags="$(LDFLAGS)" -o $(BIN_DIR)/api ./cmd/api

build-cli: ## Biên dịch binary cho CLI Client
	@echo "==> Đang build CLI Tool (phiên bản: $(VERSION))..."
	@mkdir -p $(BIN_DIR)
	@CGO_ENABLED=0 go build -trimpath -ldflags="$(LDFLAGS)" -o $(BIN_DIR)/cli ./cmd/cli

clean: ## Xóa binary và file build tạm
	@echo "==> Đang dọn dẹp thư mục bin/ và coverage/..."
	@rm -rf $(BIN_DIR) coverage

# ==============================================================================
# Thực thi trực tiếp
# ==============================================================================
.PHONY: run-api run-cli
run-api: ## Chạy API server ở môi trường local
	@go run ./cmd/api/main.go

run-cli-add: ## Chạy thử CLI tạo một task mẫu
	@go run ./cmd/cli/main.go -action=add -id=TASK-001 -title="Học Go Standard Layout"

# ==============================================================================
# Đóng gói Container Docker
# ==============================================================================
.PHONY: docker-build-api docker-build-cli docker-run-api
docker-build-api: ## Đóng gói Docker image cho API server
	@echo "==> Đang đóng gói Docker image cho API..."
	@docker build \
		-f build/package/Dockerfile \
		--build-arg TARGET_APP=api \
		--build-arg APP_VERSION=$(VERSION) \
		--build-arg COMMIT_HASH=$(COMMIT) \
		--build-arg BUILD_TIME=$(BUILD_TIME) \
		-t $(DOCKER_IMG)-api:$(VERSION) \
		-t $(DOCKER_IMG)-api:latest .

docker-build-cli: ## Đóng gói Docker image cho CLI Tool
	@echo "==> Đang đóng gói Docker image cho CLI..."
	@docker build \
		-f build/package/Dockerfile \
		--build-arg TARGET_APP=cli \
		--build-arg APP_VERSION=$(VERSION) \
		--build-arg COMMIT_HASH=$(COMMIT) \
		--build-arg BUILD_TIME=$(BUILD_TIME) \
		-t $(DOCKER_IMG)-cli:$(VERSION) \
		-t $(DOCKER_IMG)-cli:latest .

docker-run-api: ## Chạy container API server trên port 8080
	@docker run --rm -it -p 8080:8080 $(DOCKER_IMG)-api:latest

3. Cách tổ chức biến Version trong Go (internal/config/version.go)

Để các giá trị -ldflags từ Makefile/Dockerfile được nạp thành công khi khởi động, tạo file sau:

Go

package config

// Các biến này sẽ được ghi đè tự động lúc compile thông qua -ldflags
var (
	Version   = "dev"
	Commit    = "none"
	BuildTime = "unknown"
)

type AppInfo struct {
	Version   string `json:"version"`
	Commit    string `json:"commit"`
	BuildTime string `json:"build_time"`
}

func GetAppInfo() AppInfo {
	return AppInfo{
		Version:   Version,
		Commit:    Commit,
		BuildTime: BuildTime,
	}
}

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí