#2 Bước 1 — Khởi tạo dự án & Hạ tầng Docker
Tài liệu thực hành. Làm theo từ trên xuống, mỗi mục đều có lệnh kiểm chứng để bạn biết chắc mình đang đi đúng trước khi sang mục tiếp theo.
Thời gian: 15–20 phút (lần đầu tải image có thể lâu hơn) Kiến trúc tổng thể: xem
ARCHITECTURE.md
Mục tiêu
Kết thúc bước này bạn sẽ có:
- Một Go module trống nhưng đúng chuẩn, sẵn sàng nhận code
- PostgreSQL 16 chạy trong Docker, có volume nên không mất dữ liệu khi restart
- Kafka 3.9 chạy chế độ KRaft (không cần Zookeeper), đã tạo sẵn các topic
- Kafka UI tại
localhost:8080để nhìn thấy event bằng mắt - Một chương trình Go nhỏ chứng minh code của bạn kết nối được cả hai
Chưa có ở bước này: bảng dữ liệu, migration, HTTP server. Đó là bước 2.
Mục lục
- Chuẩn bị
- Khởi tạo Go module
- Tạo
.gitignorevà.env - Viết
docker-compose.yml - Khởi động và kiểm chứng
- Tạo Kafka topic
- Smoke test bằng Go
- Lệnh tắt (Makefile)
- Dọn dẹp và reset
- Xử lý sự cố
- Checklist hoàn thành
1. Chuẩn bị
Cần cài sẵn
| Công cụ | Phiên bản tối thiểu | Kiểm tra |
|---|---|---|
| Go | 1.22 | go version |
| Docker Desktop | 4.x | docker version |
| Docker Compose | v2 | docker compose version |
| Git | bất kỳ | git --version |
Chạy cả bốn lệnh kiểm tra một lượt:
go version; docker version --format '{{.Server.Version}}'; docker compose version; git --version
Nếu lệnh nào báo "not recognized", cài công cụ đó trước rồi mở lại terminal (PATH chỉ được nạp khi terminal khởi động).
Riêng cho Windows
- Docker Desktop phải bật WSL 2 backend (Settings → General → Use the WSL 2 based engine). Hyper-V backend chạy được nhưng chậm hơn nhiều.
- Cấp cho Docker tối thiểu 4 GB RAM (Settings → Resources). Kafka một mình đã ăn ~1 GB.
- Tài liệu này dùng PowerShell. Nếu bạn dùng Git Bash hay WSL thì lệnh
docker/gogiống hệt, chỉ khác cách đặt biến môi trường — chỗ nào khác tôi sẽ ghi rõ.
Xác nhận Docker thực sự đang chạy
docker run --rm hello-world
Phải thấy dòng "Hello from Docker!". Nếu báo error during connect → Docker Desktop chưa khởi động xong, đợi biểu tượng cá voi ở khay hệ thống chuyển sang trạng thái Running.
2. Khởi tạo Go module
cd D:\project
go mod init github.com/yourname/community
Thay yourname bằng tên GitHub của bạn.
Chọn module path cho đúng ngay từ đầu
Module path là tiền tố của mọi import trong dự án. Với đường dẫn trên, package event bus sẽ được import là:
import "github.com/yourname/community/internal/platform/eventbus"Đổi module path về sau nghĩa là sửa dòng
importtrong mọi file — làm được nhưng phiền. Ba quy tắc:
- Dùng đúng đường dẫn repo thật (
github.com/<user>/<repo>), kể cả khi chưa push lên GitHub. Ngày đăng kýgo.modkhớp sẵn thì không phải sửa gì.- Không đặt tên chung chung như
myapphaybackend— khi dự án khác import vào sẽ đụng tên.- Chữ thường, dùng dấu gạch ngang, không dấu tiếng Việt.
Từ đây trở đi tài liệu dùng
github.com/yourname/community. Khi copy code, nhớ thay bằng path của bạn.
Kiểm chứng:
Get-Content go.mod
module github.com/yourname/community
go 1.22
Khởi tạo git luôn (bước 3 cần tới .gitignore):
git init
3. Tạo .gitignore và .env
.gitignore
# Binary
/bin/
*.exe
*.exe~
# Môi trường — KHÔNG BAO GIỜ commit
.env
.env.local
# Test
*.out
coverage.html
# IDE
.idea/
.vscode/
*.swp
# OS
.DS_Store
Thumbs.db
.env.example — commit file này
# PostgreSQL
DATABASE_URL=postgres://app:secret@localhost:5432/community?sslmode=disable
# Kafka
KAFKA_BROKERS=localhost:9092
# HTTP
HTTP_PORT=8000
# Log: debug | info | warn | error
LOG_LEVEL=debug
.env — file thật, không commit
Copy-Item .env.example .env
Vì sao tách hai file?
.env.examplelà tài liệu: nó cho người mới clone repo biết cần khai báo những biến gì..envlà bí mật: chứa giá trị thật và không bao giờ vào git. Ở môi trường dev hai file giống hệt nhau, nhưng thói quen này là thứ ngăn bạn đẩy mật khẩu production lên GitHub sau này.
Vì sao
HTTP_PORT=8000mà không phải 8080? Vì Kafka UI đã chiếm8080. Đây là loại xung đột cổng gây mất 20 phút debug một cách vô duyên — tránh sẵn từ đầu.
4. Viết docker-compose.yml
New-Item -ItemType Directory -Force deployments
Tạo deployments/docker-compose.yml:
name: community
services:
# ─────────────────────────────────────────────────────────────
# PostgreSQL — DB nghiệp vụ + bảng outbox + bảng consumed_events
# ─────────────────────────────────────────────────────────────
postgres:
image: postgres:16-alpine
container_name: community-postgres
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: community
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d community"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
# ─────────────────────────────────────────────────────────────
# Kafka — chế độ KRaft, một node, không cần Zookeeper
# ─────────────────────────────────────────────────────────────
kafka:
image: apache/kafka:3.9.0
container_name: community-kafka
ports:
- "9092:29092" # host 9092 → listener PLAINTEXT_HOST trong container
environment:
# --- KRaft: node này vừa là broker vừa là controller ---
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
# --- Listener: xem giải thích bên dưới, đây là chỗ hay sai nhất ---
KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093,PLAINTEXT_HOST://:29092
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:9092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
# --- Cụm 1 node thì mọi replication factor đều phải là 1 ---
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
# --- Thiết lập cho môi trường dev ---
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
KAFKA_NUM_PARTITIONS: 3
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "false"
healthcheck:
test: ["CMD-SHELL", "/opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092 >/dev/null 2>&1"]
interval: 10s
timeout: 10s
retries: 10
start_period: 20s
restart: unless-stopped
# ─────────────────────────────────────────────────────────────
# Kafka UI — xem topic, message, consumer lag bằng trình duyệt
# ─────────────────────────────────────────────────────────────
kafka-ui:
image: kafbat/kafka-ui:latest
container_name: community-kafka-ui
ports:
- "8080:8080"
environment:
KAFKA_CLUSTERS_0_NAME: local
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092 # ← tên service, KHÔNG phải localhost
DYNAMIC_CONFIG_ENABLED: "true"
depends_on:
kafka:
condition: service_healthy
restart: unless-stopped
volumes:
pgdata:
4.1 Vì sao Kafka cần hai listener
Đây là điểm gây nhầm nhiều nhất khi chạy Kafka trong Docker, và nó không báo lỗi rõ ràng — chỉ im lặng không kết nối được.
Kafka không hoạt động như một HTTP server thông thường. Khi client kết nối lần đầu, broker trả về địa chỉ mà client nên dùng cho các lần sau (đó là advertised.listeners). Client sẽ ngắt và kết nối lại theo đúng địa chỉ đó.
Vấn đề: "địa chỉ đúng" khác nhau tuỳ ai đang hỏi.
┌─ Máy bạn (host) ────────────────┐ ┌─ Docker network ──────────────┐
│ │ │ │
│ go run ./cmd/api │ │ kafka-ui │
│ → cần: localhost:9092 │ │ → cần: kafka:9092 │
│ │ │ │
└─────────────────────────────────┘ └───────────────────────────────┘
│ │
│ PLAINTEXT_HOST │ PLAINTEXT
│ advertise "localhost:9092" │ advertise "kafka:9092"
└───────────────┬───────────────────────┘
▼
┌─────────────┐
│ kafka │
└─────────────┘
- Từ máy bạn,
kafkakhông phải là hostname phân giải được → phải dùnglocalhost:9092 - Từ container khác,
localhosttrỏ về chính container đó → phải dùngkafka:9092
Nên ta khai báo hai listener riêng, mỗi cái advertise một địa chỉ phù hợp với đối tượng của nó.
Nếu chỉ có một listener advertise localhost:9092: Kafka UI khởi động bình thường, giao diện mở được, nhưng hiện "Cluster offline" mãi mãi — vì nó nhận địa chỉ localhost:9092 rồi kết nối vào chính bản thân nó. Không có dòng log lỗi nào giải thích chuyện đó.
Còn dòng ports: "9092:29092" thì đọc như sau: cổng 9092 trên máy bạn được nối tới cổng 29092 trong container (nơi listener PLAINTEXT_HOST đang lắng nghe). Nhờ vậy ứng dụng Go của bạn dùng localhost:9092 — một con số quen thuộc — dù bên trong container nó là cổng khác.
4.2 Vì sao KRaft mà không phải Zookeeper
KRaft là chế độ Kafka tự quản lý metadata, không cần cụm Zookeeper đi kèm. Cụ thể với bạn:
- Một container thay vì hai — khởi động nhanh hơn, tốn ít RAM hơn
- Zookeeper đã deprecated từ Kafka 3.5 và bị gỡ hoàn toàn ở Kafka 4.0
Nếu bạn đọc một hướng dẫn nào đó vẫn còn service zookeeper, đó là tài liệu viết trước 2023.
4.3 Vì sao tắt auto.create.topics.enable
Mặc định Kafka tự tạo topic khi có ai đó publish vào một topic chưa tồn tại. Nghe thì tiện, nhưng topic tạo tự động dùng số partition mặc định — và đó là cái bẫy.
ARCHITECTURE.md §5.2 dựa vào việc mọi event của cùng một entity rơi vào cùng partition để đảm bảo thứ tự xử lý. Nếu một topic bị tạo nhầm với số partition sai, thứ tự sẽ hỏng theo cách chỉ lộ ra dưới tải cao và gần như không tái hiện được trên máy dev.
Bắt buộc tạo topic tường minh nghĩa là số partition luôn là một quyết định có ý thức, không phải giá trị tình cờ.
4.4 Hai chi tiết nhỏ trong file compose
- Không có
version:ở đầu file. Khoá này đã lỗi thời; Docker Compose v2 sẽ in cảnh báo nếu bạn để nó. - Kafka không gắn volume. Có chủ đích: image
apache/kafkachạy dưới user không phải root, gắn named volume vào thư mục chưa tồn tại sẽ tạo ra thư mục thuộc quyền root và container crash vì không ghi được. Với môi trường dev điều này chấp nhận được — nguồn sự thật là bảngoutboxtrong PostgreSQL (đã có volume), nên nếu Kafka mất dữ liệu bạn chỉ cần cho relay phát lại.docker compose stop/startvẫn giữ nguyên dữ liệu Kafka; chỉdownmới xoá.
5. Khởi động và kiểm chứng
docker compose -f deployments/docker-compose.yml up -d
Lần đầu sẽ tải khoảng 500 MB image, mất vài phút.
5.1 Kiểm tra trạng thái container
docker compose -f deployments/docker-compose.yml ps
Đợi tới khi cả ba service đều running và hai service có healthcheck đều (healthy):
NAME STATUS PORTS
community-kafka Up 45 seconds (healthy) 0.0.0.0:9092->29092/tcp
community-kafka-ui Up 20 seconds 0.0.0.0:8080->8080/tcp
community-postgres Up 45 seconds (healthy) 0.0.0.0:5432->5432/tcp
Kafka mất khoảng 20–30 giây để chuyển sang healthy. Trạng thái starting là bình thường, cứ đợi. Nếu sau 2 phút vẫn chưa healthy → xem mục 10.
5.2 Kiểm chứng PostgreSQL
docker compose -f deployments/docker-compose.yml exec postgres psql -U app -d community -c "SELECT version();"
version
--------------------------------------------------------------------------------
PostgreSQL 16.x on x86_64-pc-linux-musl, compiled by gcc ...
(1 row)
Kiểm tra luôn từ phía máy host (đây mới là đường mà app Go sẽ đi):
Test-NetConnection localhost -Port 5432 | Select-Object TcpTestSucceeded
Phải trả True.
5.3 Kiểm chứng Kafka
docker compose -f deployments/docker-compose.yml exec kafka /opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092
Ra một danh sách API version dài — nghĩa là broker đã sẵn sàng.
5.4 Kiểm chứng Kafka UI
Mở trình duyệt: http://localhost:8080
Bạn phải thấy cluster tên local ở trạng thái online, 1 broker.
Nếu hiện "Cluster offline": gần như chắc chắn
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERSđang làlocalhost:9092thay vìkafka:9092. Đọc lại mục 4.1.
6. Tạo Kafka topic
Vì đã tắt auto-create, ta tạo tường minh các topic mà ARCHITECTURE.md cần:
$kt = "/opt/kafka/bin/kafka-topics.sh"
$dc = "docker compose -f deployments/docker-compose.yml"
foreach ($t in @("post", "social", "identity")) {
docker compose -f deployments/docker-compose.yml exec kafka $kt `
--bootstrap-server localhost:9092 --create --if-not-exists `
--topic $t --partitions 3 --replication-factor 1
}
foreach ($t in @("post.DLQ", "social.DLQ", "identity.DLQ")) {
docker compose -f deployments/docker-compose.yml exec kafka $kt `
--bootstrap-server localhost:9092 --create --if-not-exists `
--topic $t --partitions 1 --replication-factor 1
}
<details>
<summary>Bản cho Git Bash / WSL / macOS</summary>
DC="docker compose -f deployments/docker-compose.yml"
KT="/opt/kafka/bin/kafka-topics.sh"
for t in post social identity; do
$DC exec kafka $KT --bootstrap-server localhost:9092 \
--create --if-not-exists --topic "$t" --partitions 3 --replication-factor 1
done
for t in post.DLQ social.DLQ identity.DLQ; do
$DC exec kafka $KT --bootstrap-server localhost:9092 \
--create --if-not-exists --topic "$t" --partitions 1 --replication-factor 1
done
</details>
Kiểm chứng:
docker compose -f deployments/docker-compose.yml exec kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list
identity
identity.DLQ
post
post.DLQ
social
social.DLQ
Vì sao topic chính có 3 partition còn DLQ chỉ 1?
Partition là đơn vị song song hoá: 3 partition cho phép 3 consumer trong cùng group xử lý đồng thời. Với topic chính, đó là thứ bạn cần khi scale.
DLQ thì ngược lại — nó nên rỗng. Message vào đó là ngoại lệ cần người xem, không phải luồng cần thông lượng. Một partition giữ mọi message hỏng theo đúng thứ tự thời gian, tiện cho việc điều tra.
Lưu ý: số partition tăng được nhưng không giảm được, và tăng partition sẽ thay đổi cách phân bổ key — làm hỏng đảm bảo thứ tự với dữ liệu cũ. Cứ 3 cho môi trường dev là hợp lý.
Thử gửi/nhận một message
Mở terminal thứ nhất làm consumer:
docker compose -f deployments/docker-compose.yml exec kafka /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic post --from-beginning
Mở terminal thứ hai làm producer:
docker compose -f deployments/docker-compose.yml exec kafka /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic post
Gõ vào producer rồi Enter:
{"event_type":"post.post.created.v1","payload":{"title":"Xin chao"}}
Dòng đó phải hiện ngay ở terminal consumer. Thoát cả hai bằng Ctrl+C.
Vào Kafka UI → Topics → post → tab Messages, bạn cũng sẽ thấy chính message đó. Đây là công cụ bạn sẽ dùng liên tục ở các bước sau để kiểm tra event có thực sự được phát hay không.
7. Smoke test bằng Go
Hai mục trên chứng minh hạ tầng chạy. Mục này chứng minh code Go của bạn nối được vào hạ tầng đó — hai việc khác nhau, và cái sau mới là cái quan trọng.
Cài driver:
go get github.com/jackc/pgx/v5
go get github.com/twmb/franz-go
Tạo cmd/smoketest/main.go:
package main
import (
"context"
"log"
"os"
"strings"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/twmb/franz-go/pkg/kgo"
)
func env(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
// ── PostgreSQL ────────────────────────────────────────────
dsn := env("DATABASE_URL", "postgres://app:secret@localhost:5432/community?sslmode=disable")
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
log.Fatalf("❌ postgres: cấu hình sai: %v", err)
}
defer pool.Close()
if err := pool.Ping(ctx); err != nil {
log.Fatalf("❌ postgres: không kết nối được: %v", err)
}
var version string
if err := pool.QueryRow(ctx, "SELECT version()").Scan(&version); err != nil {
log.Fatalf("❌ postgres: query lỗi: %v", err)
}
log.Printf("✅ PostgreSQL: %s", strings.SplitN(version, " on ", 2)[0])
// ── Kafka ─────────────────────────────────────────────────
brokers := strings.Split(env("KAFKA_BROKERS", "localhost:9092"), ",")
cl, err := kgo.NewClient(kgo.SeedBrokers(brokers...))
if err != nil {
log.Fatalf("❌ kafka: cấu hình sai: %v", err)
}
defer cl.Close()
if err := cl.Ping(ctx); err != nil {
log.Fatalf("❌ kafka: không kết nối được: %v", err)
}
log.Printf("✅ Kafka: %s", strings.Join(brokers, ","))
log.Println("🎉 Hạ tầng sẵn sàng.")
}
Chạy:
go mod tidy
go run ./cmd/smoketest
Kết quả mong đợi:
2026/07/29 22:10:03 ✅ PostgreSQL: PostgreSQL 16.x
2026/07/29 22:10:03 ✅ Kafka: localhost:9092
2026/07/29 22:10:03 🎉 Hạ tầng sẵn sàng.
Vì sao có hàm
env()với giá trị mặc định? Đểgo runchạy được ngay mà không cần bạn set biến môi trường trước. Giá trị mặc định trùng với.env.example, nên cả hai cách đều cho kết quả giống nhau. Ở code thật (bước 2 trở đi),internal/platform/configsẽ bắt buộc phải có biến môi trường và báo lỗi nếu thiếu — dev thì tiện tay được, production thì không.
Giữ lại cmd/smoketest — nó rất hữu ích mỗi khi bạn nghi ngờ "lỗi này do code hay do hạ tầng?".
8. Lệnh tắt (Makefile)
Gõ docker compose -f deployments/docker-compose.yml ... hàng chục lần một ngày sẽ rất mệt.
Nếu bạn có make
winget install GnuWin32.Make
Sau đó mở lại terminal và tạo Makefile ở thư mục gốc:
COMPOSE := docker compose -f deployments/docker-compose.yml
KT := /opt/kafka/bin/kafka-topics.sh
BOOT := localhost:9092
.PHONY: up down stop logs ps topics smoke reset
up: ## Khởi động hạ tầng
$(COMPOSE) up -d
down: ## Dừng và xoá container (giữ dữ liệu)
$(COMPOSE) down
stop: ## Tạm dừng, giữ nguyên mọi thứ
$(COMPOSE) stop
logs: ## Xem log realtime
$(COMPOSE) logs -f
ps: ## Trạng thái container
$(COMPOSE) ps
topics: ## Tạo topic (chạy được nhiều lần)
@for t in post social identity; do \
$(COMPOSE) exec -T kafka $(KT) --bootstrap-server $(BOOT) \
--create --if-not-exists --topic $$t --partitions 3 --replication-factor 1; \
done
@for t in post.DLQ social.DLQ identity.DLQ; do \
$(COMPOSE) exec -T kafka $(KT) --bootstrap-server $(BOOT) \
--create --if-not-exists --topic $$t --partitions 1 --replication-factor 1; \
done
@$(COMPOSE) exec -T kafka $(KT) --bootstrap-server $(BOOT) --list
smoke: ## Kiểm tra kết nối từ Go
go run ./cmd/smoketest
reset: ## ⚠️ XOÁ SẠCH dữ liệu và dựng lại
$(COMPOSE) down -v
$(COMPOSE) up -d
Lưu ý:
makecầnshđể chạy vòng lặp trong targettopics. Git for Windows đã cài sẵnsh, nên nếu bạn có Git thì việc này chạy được. Nếu không, dùng cách bên dưới.
Nếu bạn không muốn cài make
Tạo dev.ps1 ở thư mục gốc:
param([Parameter(Position=0)][string]$cmd = "help")
$compose = "docker", "compose", "-f", "deployments/docker-compose.yml"
$kt = "/opt/kafka/bin/kafka-topics.sh"
function Invoke-Compose { & $compose[0] $compose[1..3] @args }
switch ($cmd) {
"up" { Invoke-Compose up -d }
"down" { Invoke-Compose down }
"stop" { Invoke-Compose stop }
"logs" { Invoke-Compose logs -f }
"ps" { Invoke-Compose ps }
"smoke" { go run ./cmd/smoketest }
"reset" { Invoke-Compose down -v; Invoke-Compose up -d }
"topics" {
foreach ($t in @("post","social","identity")) {
Invoke-Compose exec -T kafka $kt --bootstrap-server localhost:9092 `
--create --if-not-exists --topic $t --partitions 3 --replication-factor 1
}
foreach ($t in @("post.DLQ","social.DLQ","identity.DLQ")) {
Invoke-Compose exec -T kafka $kt --bootstrap-server localhost:9092 `
--create --if-not-exists --topic $t --partitions 1 --replication-factor 1
}
Invoke-Compose exec -T kafka $kt --bootstrap-server localhost:9092 --list
}
default { Write-Host "Dùng: .\dev.ps1 [up|down|stop|logs|ps|topics|smoke|reset]" }
}
Dùng:
.\dev.ps1 up
.\dev.ps1 topics
.\dev.ps1 smoke
Nếu PowerShell chặn script:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
9. Dọn dẹp và reset
| Lệnh | Tác dụng | Dữ liệu Postgres |
|---|---|---|
docker compose ... stop |
Tạm dừng container | Giữ |
docker compose ... down |
Dừng và xoá container | Giữ (nằm trong volume) |
docker compose ... down -v |
Xoá container và volume | MẤT SẠCH |
⚠️ Cẩn thận với
down -vCờ
-vxoá volumepgdata— toàn bộ dữ liệu PostgreSQL biến mất, không hoàn tác được. Không có bước xác nhận nào.Chỉ dùng khi bạn cố ý muốn làm lại từ đầu (ví dụ migration hỏng cần dựng lại schema sạch).
Muốn dừng máy đi ngủ thì dùng
stop.
Kiểm tra volume còn hay mất:
docker volume ls --filter name=community
10. Xử lý sự cố
Bảng tra nhanh
| Triệu chứng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
port is already allocated (5432) |
Máy đã cài PostgreSQL sẵn | Đổi sang "5433:5432" và sửa DATABASE_URL thành cổng 5433 |
port is already allocated (8080) |
Ứng dụng khác đang chiếm | Đổi Kafka UI sang "8081:8080" |
Kafka mãi không healthy |
Docker thiếu RAM | Settings → Resources → tăng lên ≥ 4 GB |
| Kafka UI báo "Cluster offline" | BOOTSTRAPSERVERS sai |
Phải là kafka:9092, không phải localhost:9092 |
Go báo connection refused (Kafka) |
Dùng sai cổng | Từ host phải là localhost:9092 |
Go báo password authentication failed |
Volume cũ giữ mật khẩu cũ | down -v rồi up -d |
unknown topic or partition |
Chưa tạo topic | Chạy lại mục 6 |
docker: command not found |
Docker Desktop chưa chạy | Khởi động Docker Desktop, đợi biểu tượng Running |
Tìm cổng nào đang bị chiếm
Get-NetTCPConnection -LocalPort 5432 -State Listen |
Select-Object LocalPort, OwningProcess |
ForEach-Object { [PSCustomObject]@{ Port = $_.LocalPort; Process = (Get-Process -Id $_.OwningProcess).ProcessName } }
Xem log khi container crash
docker compose -f deployments/docker-compose.yml logs kafka --tail 100
docker compose -f deployments/docker-compose.yml logs postgres --tail 50
Với Kafka, phần lớn lỗi cấu hình thật sự nằm ở những dòng đầu tiên của log, không phải dòng cuối:
docker compose -f deployments/docker-compose.yml logs kafka | Select-String -Pattern "ERROR|Exception" | Select-Object -First 10
password authentication failed — vì sao đổi mật khẩu không ăn
PostgreSQL chỉ đọc POSTGRES_PASSWORD đúng một lần, ở lần khởi tạo data directory đầu tiên. Sau đó biến này bị bỏ qua hoàn toàn.
Nghĩa là sửa mật khẩu trong docker-compose.yml rồi up -d lại sẽ không có tác dụng — container mới vẫn dùng mật khẩu cũ nằm trong volume. Muốn đổi thật thì phải xoá volume:
docker compose -f deployments/docker-compose.yml down -v
docker compose -f deployments/docker-compose.yml up -d
(Đọc lại cảnh báo ở mục 9 trước khi chạy.)
Làm lại hoàn toàn từ đầu
docker compose -f deployments/docker-compose.yml down -v
docker system prune -f
docker compose -f deployments/docker-compose.yml up -d
11. Checklist hoàn thành
Đánh dấu từng dòng — mỗi dòng đều kiểm chứng được bằng một lệnh cụ thể:
- [ ]
go.modtồn tại, module path là đường dẫn repo thật của bạn - [ ]
.gitignorecó dòng.env;git statuskhông liệt kê file.env - [ ]
.env.exampleđược commit,.envthì không - [ ]
docker compose ps— cả 3 servicerunning, 2 service(healthy) - [ ]
psql ... -c "SELECT version()"trả về PostgreSQL 16 - [ ]
kafka-topics.sh --listliệt kê đủ 6 topic - [ ] http://localhost:8080 hiện cluster
localonline - [ ] Gửi được message qua console producer và thấy nó ở consumer
- [ ]
go run ./cmd/smoketestin ra cả hai dấu ✅
Cấu trúc thư mục lúc này:
project/
├── cmd/
│ └── smoketest/
│ └── main.go
├── deployments/
│ └── docker-compose.yml
├── docs/
│ ├── ARCHITECTURE.md
│ └── 01-khoi-tao-du-an-va-ha-tang.md
├── .env ← không commit
├── .env.example
├── .gitignore
├── go.mod
├── go.sum
└── Makefile (hoặc dev.ps1)
Commit lại:
git add .
git commit -m "chore: khởi tạo go module và hạ tầng docker"
Bước tiếp theo
Bước 2 — Nền tảng dữ liệu: viết migration cho users, user_stats, posts, cộng thêm hai bảng hạ tầng outbox và consumed_events; cấu hình sqlc để sinh code truy vấn type-safe.
Trước khi sang bước 2, nên đọc lại hai mục này trong ARCHITECTURE.md:
- §7 Outbox Pattern — vì sao phải có bảng
outbox - §8 Idempotency — vì sao phải có bảng
consumed_events
Hai bảng này trông có vẻ thừa khi mới nhìn. Chúng là thứ giữ cho hệ thống không âm thầm sai số liệu, nên hiểu lý do tồn tại của chúng trước khi gõ migration sẽ đỡ cho bạn rất nhiều thời gian về sau.
All Rights Reserved