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