Cài PostgreSQL local — Docker, psql và schema TaskFlow
Cài PostgreSQL 16 bằng Docker Compose, kết nối psql qua port 5433, nạp schema TaskFlow 5 bảng và thuộc 8 lệnh psql đủ dùng cho cả phần internals.
TL;DR: Cách nhanh nhất để có PostgreSQL chạy local là Docker Compose: một file YAML khai image postgres:16-alpine, map host port 5433 vào container port 5432, một volume giữ dữ liệu, rồi docker compose up -d postgres. Kết nối bằng docker compose exec postgres psql -U olhub -d olhub, tạo database taskflow, paste 5 câu CREATE TABLE của schema TaskFlow và seed 10 user. Postgres.app hoặc brew/apt chỉ là đường phụ khi bạn không muốn Docker. Cuối bài bạn phân biệt được cluster, database và role, và thuộc 8 lệnh psql đủ dùng cho toàn khoá.
Khoá SQL & Database đã dạy relational model, ER diagram và tour schema TaskFlow trên giấy. Khoá này mở PostgreSQL ra xem bên trong, nên từ bài sau trở đi mọi thứ đều cần một cluster thật để chạy EXPLAIN, pageinspect, VACUUM. Bài này lo đúng việc đó. Cuối bài bạn có psql sẵn sàng và 10 user trong bảng users.
1. Ba cách cài — chọn cách nào?
| Cách | Ưu điểm | Nhược điểm | Khi nào dùng |
|---|---|---|---|
| Docker Compose (khuyến nghị) | Reproducible, dọn sạch dễ, OLHub repo có sẵn config | Cần Docker Desktop đang chạy | Mọi dev mới, mọi OS |
| Postgres.app (macOS) | Binary native, cài dễ, có GUI nhỏ quản lý server | Chỉ macOS, không reproducible giữa máy | Dev macOS muốn nhanh, không cần Docker |
| brew / apt | Native, tự chọn version, không cần Docker | Phải tự tạo database, dễ đụng Postgres có sẵn của hệ thống | Power user quen CLI |
Cả ba đường đều đổ về một tiến trình postgres lắng nghe trên máy bạn, có role olhub và database olhub. Làm theo một cách, bỏ qua hai cách còn lại.

2. Cài bằng Docker Compose — đường chính
Yêu cầu: Docker Desktop đã cài và đang chạy.
Bước 1 — Chuẩn bị docker-compose.yml. Nếu bạn đã clone OLHub repo, file này có sẵn. Nếu chưa, tạo file với nội dung tối thiểu:
services:
postgres:
image: postgres:16-alpine
ports:
- "5433:5432"
environment:
POSTGRES_DB: olhub
POSTGRES_USER: olhub
POSTGRES_PASSWORD: olhub_secret_2026
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U olhub"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
Bốn khối, mỗi khối một việc: ports map host 5433 vào container 5432, environment tạo role + database lúc init, volumes giữ dữ liệu qua các lần restart, healthcheck để Docker biết Postgres đã sẵn sàng nhận kết nối chứ không chỉ "process đang chạy".
Bước 2 — Khởi động và verify.
docker compose up -d postgres
docker compose ps postgres
Output mong đợi:
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
olhub-postgres postgres:16-alpine "docker-entrypoint.s…" postgres 5 seconds ago Up 4 seconds (healthy) 0.0.0.0:5433->5432/tcp
Chữ (healthy) đến từ khối healthcheck: pg_isready đã trả lời được; 5–10 giây đầu còn thấy (health: starting) là bình thường.
Bước 3 — Kết nối bằng psql.
docker compose exec postgres psql -U olhub -d olhub
psql (16.x)
Type "help" for help.
olhub=#
Prompt olhub=# nghĩa là bạn đang ở database olhub với quyền superuser (dấu #; user thường thấy >). Gõ \q để thoát.
Connection string từ host (dùng cho GUI tool hoặc app chạy ngoài Docker):
postgresql://olhub:olhub_secret_2026@localhost:5433/olhub
Số 5433 chỉ có ở phía host; app chạy cùng compose network vẫn nối container qua postgres:5432.
Dọn sạch khi không cần:
docker compose down # stop container, giu data
docker compose down -v # stop container + xoa volume (mat data)
3. Đường phụ — Postgres.app và brew/apt
Chỉ đọc mục này nếu bạn không dùng Docker.
| Postgres.app (macOS) | brew (macOS) | apt (Ubuntu/Debian) | |
|---|---|---|---|
| Cài | Tải .dmg ở postgresapp.com, kéo vào Applications, bấm Initialize rồi Start | brew install postgresql@16 && brew services start postgresql@16 | sudo apt install -y postgresql-16 postgresql-contrib && sudo systemctl enable --now postgresql |
| Mặc định | Port 5432, trust mode, username = user macOS | Port 5432, trust mode, username = user macOS | Port 5432, peer auth, user hệ thống postgres |
| Kết nối | /Applications/Postgres.app/Contents/Versions/latest/bin/psql -d postgres | createdb taskflow && psql -d taskflow | sudo -u postgres psql |
command not found: psql sau khi cài Postgres.app: app không tự thêm binary vào PATH, thêm export PATH="/Applications/Postgres.app/Contents/Versions/latest/bin:$PATH" vào ~/.zshrc rồi source lại. database "alice" does not exist sau brew services start: brew tạo cluster nhưng không tạo database trùng tên user, chạy createdb taskflow trước rồi mới psql -d taskflow.
Thử ngẫmmáy bạn đã cài Postgres qua brew cho một dự án cũ, giờ thêm OLHub Docker. Hai bản Postgres này có tranh port hay tranh dữ liệu với nhau không?
4. Cluster, database và role khác nhau thế nào?
Hình dung một toà nhà văn phòng: toà nhà là cluster, mỗi tầng là một database, thẻ ra vào là role. Thẻ do ban quản lý toà nhà cấp nên lên được mọi tầng, nhưng mở được phòng nào trên tầng đó là chuyện của từng tầng.
| Toà nhà | PostgreSQL |
|---|---|
| Toà nhà, một ban quản lý | Cluster: một tiến trình postgres, một thư mục dữ liệu, một port |
| Tầng | Database: không gian tên riêng cho schema và bảng |
| Thẻ ra vào cấp cho cả toà | Role: tài khoản đăng nhập, dùng chung cho mọi database |
Container olhub-postgres là một cluster; bản brew trên máy bạn là một cluster khác. Bên trong có nhiều database (olhub, taskflow, postgres); một kết nối psql luôn gắn với đúng một database, muốn sang database khác phải \c. Role vừa là tài khoản đăng nhập vừa là chủ sở hữu object, và thuộc về cluster: olhub đăng nhập được vào cả olhub lẫn taskflow. POSTGRES_USER trong compose tạo role, POSTGRES_DB tạo database, cả hai chỉ chạy lần init đầu khi volume còn trống.
5. psql — 8 lệnh phải nhớ
| Lệnh | Mô tả |
|---|---|
\l | Liệt kê database trong cluster |
\c <db> | Connect sang database khác |
\dt | Liệt kê bảng trong schema hiện tại (public) |
\d <table> | Mô tả bảng: column, type, index, constraint |
\du | Liệt kê role trong cluster |
\timing on | Hiện thời gian thực thi mỗi query |
\x | Toggle expanded display, mỗi column một dòng |
\q | Thoát psql |
Trước khi gõ: database olhub vừa được Docker tạo lúc init, bạn đoán \dt in ra gì?
olhub=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype |
-----------+----------+----------+-------------+-------------+
olhub | olhub | UTF8 | en_US.utf8 | en_US.utf8 |
postgres | postgres | UTF8 | en_US.utf8 | en_US.utf8 |
olhub=# \dt
Did not find any relations.
\dt báo không có relation nào vì olhub là database trống, còn \l cho thấy cluster có hai database và hai owner khác nhau. Bước tiếp theo tạo database thứ ba và nạp bảng vào đó.
6. Nạp schema TaskFlow
Schema TaskFlow gồm 5 bảng; DDL đầy đủ và giải thích từng constraint ở Tour TaskFlow schema của khoá SQL & Database. Ở đây chỉ cần copy-paste để DB sẵn sàng cho module index.
Bước 1 — Tạo database taskflow:
docker compose exec postgres psql -U olhub -d olhub -c "CREATE DATABASE taskflow;"
Bước 2 — Apply DDL bằng heredoc (hoặc paste 5 CREATE TABLE vào psql):
docker compose exec -T postgres psql -U olhub -d taskflow <<'EOF'
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE projects (
id BIGSERIAL PRIMARY KEY,
owner_id BIGINT NOT NULL REFERENCES users(id),
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE project_members (
user_id BIGINT NOT NULL REFERENCES users(id),
project_id BIGINT NOT NULL REFERENCES projects(id),
role TEXT NOT NULL CHECK (role IN ('owner', 'member', 'viewer')),
joined_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, project_id)
);
CREATE TABLE tasks (
id BIGSERIAL PRIMARY KEY,
project_id BIGINT NOT NULL REFERENCES projects(id),
assignee_id BIGINT REFERENCES users(id),
title TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('todo', 'doing', 'done', 'archived')) DEFAULT 'todo',
due_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE comments (
id BIGSERIAL PRIMARY KEY,
task_id BIGINT NOT NULL REFERENCES tasks(id),
user_id BIGINT NOT NULL REFERENCES users(id),
body TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
EOF
Bước 3 — Seed nhỏ (10 user + 5 project):
docker compose exec -T postgres psql -U olhub -d taskflow <<'EOF'
INSERT INTO users (email, name) VALUES
('[email protected]', 'Nguyen Van An'),
('[email protected]', 'Tran Thi Binh'),
('[email protected]', 'Le Hoang Cuong'),
('[email protected]', 'Pham Tien Dung'),
('[email protected]', 'Hoang Thi Em'),
('[email protected]', 'Vo Minh Fan'),
('[email protected]', 'Bui Quoc Giang'),
('[email protected]', 'Do Thi Huong'),
('[email protected]', 'Mai Van Inh'),
('[email protected]', 'Ngo Hoang Khoa');
INSERT INTO projects (owner_id, name) VALUES
(1, 'OLHub Platform'),
(1, 'Marketing Site'),
(2, 'Mobile App'),
(3, 'Internal Dashboard'),
(4, 'Data Pipeline');
EOF
Verify:
docker compose exec postgres psql -U olhub -d taskflow -c "SELECT count(*) FROM users;"
count
-------
10
(1 row)
Các bảng project_members, tasks, comments còn rỗng: 1 triệu task sẽ được sinh bằng script ở mini-challenge dashboard tuning khi cần dataset đủ lớn để đo. SELECT count(*) FROM tasks trả 0 lúc này là điều đã lường trước.
Thử ngẫmseed chỉ có users và projects. Nếu bài sau JOIN tasks với users để đếm task mỗi người, kết quả lúc này là gì, và INNER JOIN với LEFT JOIN có cho cùng đáp án không?
7. GUI tool — chọn cái nào
Khuyến nghị cho khoá này: DBeaver Community (miễn phí, cross-platform, vẽ ER diagram từ FK). Kết nối với OLHub Docker: New Database Connection → PostgreSQL, Host localhost, Port 5433, Database olhub, Username olhub, Password olhub_secret_2026, bấm Test Connection. pgAdmin 4 đáng mở khi cần xem query plan dạng đồ hoạ; còn lại psql đủ dùng cho cả khoá.
8. Pitfall — 3 lỗi setup phổ biến
Lỗi 1 — Connection refused
psql: error: connection to server at "localhost" (127.0.0.1), port 5433 failed: Connection refused
Container chưa start hoặc đang khởi động. docker compose ps postgres chưa thấy Up (healthy) thì docker compose up -d postgres rồi đợi thêm 5–10 giây.
Lỗi 2 — password authentication failed
psql: error: connection to server failed: FATAL: password authentication failed for user "alice"
Sai role hoặc sai password. Postgres tạo sẵn superuser postgres khi init cluster, nhưng container OLHub không bật trust mode cho nó; role có password là olhub, khai trong docker-compose.yml. Luôn dùng -U olhub và kiểm tra lại connection string ở phần trên.
Lỗi 3 — port is already allocated
Error response from daemon: driver failed programming external connectivity:
bind for 0.0.0.0:5433 failed: port is already allocated
Process khác đang giữ 5433. lsof -i :5433 để xem là ai: nếu là container cũ thì docker compose down trước; nếu là process khác thì đổi host port trong docker-compose.yml sang 5434 hoặc port trống bất kỳ.
9. Workflow dev hàng ngày
Buổi sáng docker compose up -d postgres, rồi vào psql bằng docker compose exec postgres psql -U olhub -d taskflow. Gõ hai lệnh này mỗi ngày thì nên có alias:
alias psqltf='docker compose exec postgres psql -U olhub -d taskflow'
Trước khi nghịch ALTER TABLE hay DELETE hàng loạt, chụp snapshot để restore trong vài giây thay vì setup lại từ đầu:
# Snapshot
docker compose exec postgres pg_dump -U olhub -d taskflow > taskflow-snapshot.sql
# Restore
docker compose exec postgres psql -U olhub -d taskflow -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
docker compose exec -T postgres psql -U olhub -d taskflow < taskflow-snapshot.sql
Thử ngẫmbạn vừa restore bằng DROP SCHEMA CASCADE nhưng quên mình đang connect vào database olhub thay vì taskflow. Hậu quả là gì, và vì sao Postgres không cảnh báo?
10. Deep Dive — Postgres setup
- PostgreSQL Documentation — "Server Configuration" — reference cho mọi tham số trong
postgresql.conf; đọc khi cần biết một setting mặc định là gì. - PostgreSQL Documentation — "Client Authentication" — cơ chế
pg_hba.conf(trust, peer, scram-sha-256): vì sao brew đăng nhập không cần password còn container thì cần. - Docker Compose —
servicesreference — bốn khốihealthcheck,ports,volumes,environmentcủa file compose ở mục 2.
11. Liên hệ các bài khác
- Tour TaskFlow schema — DDL 5 bảng bạn vừa paste được giải thích từng constraint ở đó; đọc lại nếu chưa rõ vì sao
project_memberscó primary key ghép. - ER modeling — sơ đồ ER mà 5 bảng này map về, cần khi bài index nói "cardinality của assignee_id".
- B-tree internals — bài đầu tiên dùng cluster này để chạy
pageinspect; không có Postgres thật thì bài đó chỉ đọc được, không thử được. - Mini-challenge dashboard tuning — nơi bảng
tasksđược seed 1 triệu row; mọi con số EXPLAIN của module index đo trên dataset đó. - Publish port — DNAT và Kết nối container bằng veth — dòng
"5433:5432"là một luật DNAT của Docker; đọc khi muốn biết vì saolocalhost:5433tới được container.
12. Tóm tắt
- Checklist sau setup:
docker compose psthấy(healthy),SELECT count(*) FROM userstrêntaskflowtrả 10. - Đọc lỗi kết nối theo chữ:
Connection refusedlà container chưa lên,password authentication failedlà sai role (olhub, không phảipostgres),port is already allocatedthìlsof -i :5433. - Gặp
ECONNREFUSEDtừ app trong compose network thì kiểm tra app có đang trỏ nhầm vàolocalhost:5433thay vìpostgres:5432không. - Snapshot bằng
pg_dumptrước khi nghịch, restore bằngDROP SCHEMA public CASCADE+ psql < file; cách này cho môi trường học, production dùng migration tool. - Ba lệnh psql bạn sẽ gõ nhiều nhất ở module index:
\d <table>xem index và constraint,\timing ontrước khi đo,\xkhi output nhiều cột.
Bài tiếp theo mở B-tree ra xem page, page split và fill factor trên chính cluster bạn vừa dựng.
13. Tự kiểm tra
- Q1Vì sao OLHub map host port 5433 thay vì dùng default 5432? Nếu một dev khác trên cùng team cũng đang chạy Postgres local thì sao?
- Q2Phân biệt khi nào dùng psql CLI và khi nào dùng GUI tool. Nêu hai việc ví dụ cho mỗi loại.
- Q3Bạn chạy
CREATE ROLE reporter LOGIN PASSWORD 'x'khi đang connect vào database olhub. Role đó có đăng nhập được vào database taskflow không? Vì sao? - Q4Bạn chạy
\dttrong psql và thấy 'Did not find any relations' dù đã CREATE TABLE trước đó. Nguyên nhân có thể là gì? - Q5Bạn muốn rollback database về trạng thái trước khi thử một loạt ALTER TABLE và DELETE. Dùng lệnh nào? Tradeoff so với dùng migration tool như Flyway/Liquibase?
Bài tiếp theo: B-tree internals — page split và fill factor. Chưa quen schema TaskFlow? Đọc lại Tour TaskFlow schema ở khoá SQL & Database trước.
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Thảo luận & góp ý
Hỏi đáp về bài này