Docker Compose không nhận .env và PostgreSQL báo password authentication failed
Phân biệt .env, env_file, environment; kiểm tra biến trong container và sửa mật khẩu PostgreSQL khi volume đã khởi tạo.
Bài viết bởi Phạm Minh Thiện
Front-end Developer, trực tiếp thực hành Next.js, NestJS, Docker, Nginx và VPS Ubuntu.
Bài viết đã được kiểm tra lại ngày 20/7/2026.
Mục lục bài viết
Một trong những lỗi khó chịu nhất khi deploy bằng Docker Compose là bạn đã sửa .env, nhưng container vẫn dùng giá trị cũ hoặc PostgreSQL tiếp tục báo:
password authentication failed for user "app_user"
Nguyên nhân thường không nằm ở một chỗ duy nhất. Docker Compose có nhiều cơ chế liên quan đến biến môi trường, trong khi image PostgreSQL chỉ dùng một số biến khởi tạo ở lần đầu tạo database. Ngoài ra, volume giữ dữ liệu độc lập với vòng đời container nên việc recreate container không đồng nghĩa với tạo lại mật khẩu database.

1. Ba khái niệm dễ nhầm
.env dùng cho nội suy Compose
Ví dụ .env:
POSTGRES_VERSION=18
API_PORT=3000
Compose:
services:
db:
image: postgres:${POSTGRES_VERSION}
api:
ports:
- "${API_PORT}:3000"
Compose đọc các giá trị để tạo cấu hình cuối cùng. Điều này không có nghĩa mọi biến trong .env tự động xuất hiện bên trong container.
env_file đưa biến vào container
services:
api:
env_file:
- ./apps/api/.env.docker
File:
NODE_ENV=production
DATABASE_HOST=db
DATABASE_PORT=5432
Các biến này được đưa vào môi trường của container api.
environment khai báo trực tiếp cho service
services:
api:
environment:
NODE_ENV: production
DATABASE_HOST: db
Khi nhiều nguồn cùng đặt một biến, thứ tự ưu tiên có thể làm giá trị bạn nghĩ đang dùng bị ghi đè. Vì vậy, luôn kiểm tra cấu hình thực tế thay vì đoán.
2. Kiểm tra Compose đang đọc file nào
Từ đúng thư mục project:
pwd
ls -la
ls -la apps/api/.env.docker
Sau đó render cấu hình:
docker compose config
Lệnh này rất hữu ích để phát hiện:
- Biến chưa được thay thế.
- Đường dẫn
env_filesai. - Port hoặc image tag không như mong đợi.
- Giá trị placeholder như
CHANGE_ME_DB_PASSWORDvẫn còn.
Lưu ý bảo mật: output có thể chứa secret. Không đăng toàn bộ lên bài viết, issue công khai hoặc ảnh chụp màn hình. Hãy che password, token và private URL.
Có thể kiểm tra một biến mà không in tất cả:
docker compose config | grep -n 'DATABASE_HOST\|DATABASE_PORT'
3. Kiểm tra biến thật sự bên trong container
Sau khi container chạy:
docker compose exec api printenv DATABASE_HOST
docker compose exec api printenv DATABASE_PORT
Hoặc:
docker compose exec api env | grep '^DATABASE_'
Không nên in DATABASE_PASSWORD trên terminal được ghi log hoặc quay màn hình. Với kiểm tra tự động, chỉ nên xác nhận biến có tồn tại:
docker compose exec api sh -lc 'test -n "$DATABASE_PASSWORD" && echo set || echo missing'

4. Đường dẫn env_file được tính từ đâu?
Trong cấu hình thông thường, đường dẫn tương đối của env_file được giải quyết theo vị trí Compose file/project. Lỗi phổ biến:
env file .../apps/api/.env.docker not found
Cấu trúc mẫu:
project/
├── compose.yaml
└── apps/
└── api/
└── .env.docker
Compose:
services:
api:
env_file:
- ./apps/api/.env.docker
Nếu chạy từ nơi khác hoặc dùng nhiều file -f, hãy xác nhận bằng:
docker compose -f /opt/app/compose.yaml config
Bạn cũng có thể chỉ định file nội suy:
docker compose --env-file .env.production config
.env dùng cho Compose interpolation và env_file của một service là hai cơ chế khác nhau.
5. localhost trong container không phải PostgreSQL container
Compose:
services:
api:
build: ./api
db:
image: postgres:18
Từ container api, hostname database nên là tên service:
DATABASE_HOST=db
DATABASE_PORT=5432
Không dùng:
DATABASE_HOST=localhost
Trong container api, localhost trỏ về chính container api. Nó không tự trỏ sang container db.
Kiểm tra DNS nội bộ:
docker compose exec api getent hosts db
Kiểm tra cổng:
docker compose exec api sh -lc 'nc -zv db 5432'
Image API có thể chưa cài nc; khi đó dùng healthcheck hoặc một container chẩn đoán phù hợp trong cùng network.
6. Vì sao sửa POSTGRES_PASSWORD nhưng mật khẩu cũ vẫn còn?
Ví dụ:
services:
db:
image: postgres:18
environment:
POSTGRES_USER: app_user
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: app_db
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Các biến POSTGRES_USER, POSTGRES_PASSWORD và POSTGRES_DB chủ yếu được image dùng khi thư mục dữ liệu còn trống và quá trình initdb diễn ra lần đầu.
Sau lần đầu, volume postgres_data đã chứa database. Bạn sửa .env rồi recreate container, volume vẫn giữ user và password cũ. Đây là lý do app báo sai mật khẩu dù docker compose config đã hiển thị password mới.
7. Sửa mật khẩu mà không xóa dữ liệu
Đăng nhập bằng tài khoản quản trị đang còn hoạt động. Ví dụ từ container database:
docker compose exec db psql -U postgres -d postgres
Trong psql:
ALTER USER app_user WITH PASSWORD 'mat_khau_moi';
Sau đó cập nhật secret của ứng dụng cho khớp và recreate API:
docker compose up -d --force-recreate api
Không đưa mật khẩu thật vào lịch sử shell hoặc bài viết. Trong production, ưu tiên secret manager, Docker secrets hoặc cơ chế quản lý secret của nền tảng.
Nếu bạn không còn thông tin đăng nhập quản trị, cần dùng quy trình phục hồi phù hợp với PostgreSQL và môi trường của mình. Không nên xóa volume production để “làm lại cho nhanh”.
8. Chỉ xóa volume khi dữ liệu có thể bỏ
Trong lab hoàn toàn mới, bạn có thể khởi tạo lại database:
docker compose down
docker volume ls
Xác định đúng volume rồi mới xóa:
docker volume rm project_postgres_data
Hoặc:
docker compose down -v
Cảnh báo: -v có thể xóa volume được khai báo trong Compose và làm mất toàn bộ database. Trước khi dùng với dữ liệu quan trọng, phải backup và kiểm tra khả năng restore.
Sau khi volume trống:
docker compose up -d db
docker compose logs -f db
Bạn sẽ thấy quá trình khởi tạo mới.
9. Kiểm tra DATABASE_URL
Dạng phổ biến:
postgresql://app_user:password@db:5432/app_db
Nếu password chứa ký tự đặc biệt như @, :, /, # hoặc %, chuỗi URL có thể bị phân tích sai. Cần URL-encode thành phần username/password hoặc dùng các biến tách riêng nếu thư viện hỗ trợ.
Ví dụ Node.js với biến tách riêng:
DATABASE_HOST=db
DATABASE_PORT=5432
DATABASE_USER=app_user
DATABASE_PASSWORD=your_secret
DATABASE_NAME=app_db
Cách này dễ debug hơn một chuỗi URL dài, nhưng ứng dụng phải cấu hình để đọc đúng các biến.
Không ghi secret trực tiếp trong compose.yaml đã commit vào Git.
10. Healthcheck và thứ tự khởi động
depends_on chỉ nói về quan hệ khởi động ở mức Compose; ứng dụng vẫn nên có cơ chế retry database.
Ví dụ healthcheck:
services:
db:
image: postgres:18
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app_user -d app_db"]
interval: 5s
timeout: 5s
retries: 10
api:
build: ./api
depends_on:
db:
condition: service_healthy
Sau đó:
docker compose ps
Healthcheck giúp phân biệt container “đang chạy process” với database “đã sẵn sàng nhận kết nối”.
11. Quy trình chẩn đoán theo thứ tự
# 1. Render cấu hình
docker compose config
# 2. Kiểm tra container
docker compose ps
# 3. Kiểm tra biến không nhạy cảm trong API
docker compose exec api env | grep '^DATABASE_HOST\|^DATABASE_PORT\|^DATABASE_USER\|^DATABASE_NAME'
# 4. Kiểm tra DNS service
docker compose exec api getent hosts db
# 5. Xem log
docker compose logs --tail=150 api
docker compose logs --tail=150 db
# 6. Kiểm tra volume
docker volume ls
Chỉ sau khi hiểu rõ volume và dữ liệu, bạn mới quyết định đổi password bằng SQL hay khởi tạo lại database lab.
12. Checklist xác minh
-
env_filetồn tại đúng đường dẫn. -
docker compose configkhông còn placeholder. - Biến cần thiết tồn tại trong container API.
- Host database là
db, không phảilocalhosttrong container. - User, database và password giữa API/PostgreSQL khớp nhau.
- Hiểu rằng
POSTGRES_PASSWORDkhông đổi password của volume đã khởi tạo. - Không xóa volume khi chưa backup.
- API có retry hoặc chờ database healthy.
13. Câu hỏi thường gặp
Recreate container có đọc .env mới không?
Có thể đọc cấu hình mới, nhưng database trong volume vẫn giữ trạng thái đã khởi tạo. Hai việc này độc lập.
docker compose config có an toàn để đăng công khai không?
Không nên. Nó có thể render cả secret. Chỉ trích phần không nhạy cảm và che thông tin trước khi chụp ảnh.
Tại sao kết nối từ máy host dùng localhost, còn từ API container phải dùng db?
Vì host và container có network namespace khác nhau. Tên service db được Docker DNS phân giải trong Compose network.
Nguồn tham khảo
Đọc tiếp
Bài viết liên quan
Docker Port Is Already Allocated: Cách tìm tiến trình chiếm cổng trên Windows và Linux
Hướng dẫn xử lý port is already allocated, address already in use và EADDRINUSE trong Docker Compose mà không tắt nhầm tiến trình.
Đọc bài →Docker bị Killed hoặc Exit Code 137 trên VPS: Cách xác định đúng nguyên nhân
Hướng dẫn kiểm tra Docker Exit Code 137, OOMKilled, RAM, swap và cách tối ưu quá trình build trên VPS cấu hình thấp.
Đọc bài →Docker Compose production: Node.js, PostgreSQL và backup
Cấu hình Docker Compose production cho Node.js và PostgreSQL với healthcheck, volume, network riêng, secret, log rotation, backup và rollback.
Đọc bài →