Docker Production: Xử Lý Lỗi Khi Khởi Động

Bạn đã containerize app xong. Compose file trông clean. Chạy ngon trên máy local.
Rồi bạn deploy lên production và một trong hai thứ sau xảy ra:
- Image mới pull thất bại. Container crash. Version cũ đã bị xóa. Service down.
- App start trước khi database kịp sẵn sàng. Crash với lỗi kết nối. Không tự recover.
Đây không phải edge case. Chúng xảy ra với mọi team deploy Docker production, thường vào thời điểm tệ nhất. Bài này cho bạn các pattern cụ thể để xử lý cả hai.
Vấn Đề 1: Image Pull Thất Bại Khi Deploy
Tại Sao Xảy Ra
Khi bạn chạy docker compose up hoặc docker pull, Docker liên hệ registry để download image. Việc này có thể thất bại vì nhiều lý do:
- Registry down — Docker Hub có outages; private registry có vấn đề network
- Authentication hết hạn — credentials xoay vòng, token timeout
- Image tag không tồn tại — typo trong tag, image chưa được push trước khi deploy
- Network timeout — kết nối chậm, image lớn, proxy công ty chặn
- Rate limiting — Docker Hub free tier giới hạn unauthenticated pull
Scenario nguy hiểm là khi bạn đã stop container cũ trước khi image mới pull xong.
Cách Deploy Sai
# ❌ NGUY HIỂM — stop container cũ trước, rồi mới pull image mới
docker compose down
docker compose pull
docker compose up -dNếu docker compose pull thất bại ở đây, bạn không có service nào đang chạy. Bạn vừa biến một deploy failure thành outage.
Chiến Lược 1: Pull Trước Khi Stop
Luôn pull image mới trước, xác nhận nó tồn tại local, rồi mới swap:
# ✅ Pull image mới TRƯỚC khi stop container cũ
docker compose pull
# Chỉ stop và restart nếu pull thành công
if [ $? -eq 0 ]; then
docker compose up -d
else
echo "Pull thất bại — giữ nguyên containers đang chạy"
exit 1
fiChiến Lược 2: Ghim Version Image, Không Dùng :latest
# docker-compose.yml
services:
api:
image: myregistry.io/myapp:${APP_VERSION} # không bao giờ dùng :latest# deploy.sh
PREVIOUS_VERSION=$(docker inspect myapp:current --format='{{.Config.Image}}' 2>/dev/null)
NEW_VERSION="myregistry.io/myapp:${APP_VERSION}"
echo "Pulling $NEW_VERSION..."
docker pull "$NEW_VERSION"
if [ $? -ne 0 ]; then
echo "❌ Pull thất bại. Giữ nguyên $PREVIOUS_VERSION"
exit 1
fi
echo "✅ Pull thành công. Đang deploy..."
APP_VERSION=$APP_VERSION docker compose up -d
echo "Version trước để rollback: $PREVIOUS_VERSION"Chiến Lược 3: Retry với Exponential Backoff
# Retry pull tối đa 3 lần với exponential backoff
pull_with_retry() {
local image=$1
local max_attempts=3
local attempt=1
while [ $attempt -le $max_attempts ]; do
echo "Lần thử $attempt/$max_attempts: pulling $image"
docker pull "$image" && return 0
wait_time=$((attempt * 15))
echo "Pull thất bại. Chờ ${wait_time}s trước khi retry..."
sleep $wait_time
attempt=$((attempt + 1))
done
echo "❌ Tất cả lần thử đều thất bại cho $image"
return 1
}
pull_with_retry "myregistry.io/myapp:${APP_VERSION}"Chiến Lược 4: Local Registry Mirror
Với production server, chạy một local registry mirror để pull không phụ thuộc vào external network:
# registry-mirror/docker-compose.yml
services:
registry:
image: registry:2
ports:
- "5000:5000"
environment:
REGISTRY_PROXY_REMOTEURL: https://registry-1.docker.io
volumes:
- registry-data:/var/lib/registry
volumes:
registry-data:Cấu hình Docker daemon dùng mirror:
// /etc/docker/daemon.json
{
"registry-mirrors": ["http://localhost:5000"]
}Giờ docker pull nginx hit local mirror trước. Nếu mirror đã cache image, external registry outage không ảnh hưởng bạn.
Chiến Lược 5: Verify Image trong CI Trước Khi Deploy
Cách an toàn nhất là xác nhận image tồn tại và pull được trong CI pipeline, trước khi bước deploy bắt đầu:
# GitHub Actions
jobs:
verify-image:
runs-on: ubuntu-latest
steps:
- name: Pull và verify image
run: |
docker pull myregistry.io/myapp:${{ github.sha }}
echo "Image đã xác nhận ✅"
deploy:
needs: verify-image
runs-on: ubuntu-latest
steps:
- name: Deploy
run: |
ssh deploy@server "APP_VERSION=${{ github.sha }} ./deploy.sh"Nếu image thiếu hoặc registry down, pipeline fail trước khi bất kỳ container production nào bị động đến.
Vấn Đề 2: Mất Kết Nối Database Khi Khởi Động
Tại Sao Xảy Ra
Container start nhanh. Database start chậm. Khi bạn chạy docker compose up, container app có thể sẵn sàng nhận connection trong 1-2 giây — nhưng PostgreSQL, MySQL, hay MongoDB có thể mất 10-30 giây để khởi tạo xong.
Không có coordination rõ ràng, đây là những gì xảy ra:
t=0s → docker compose up
t=1s → api container start, app code chạy
t=1s → app thử kết nối db: "Connection refused"
t=1s → app crash (exit code 1)
t=2s → Docker restart api (restart: always)
t=3s → app thử kết nối lại: "Connection refused"
t=3s → app crash lại
...
t=15s → db cuối cùng sẵn sàng
t=20s → api cuối cùng kết nối được ở lần retry thứ NApp mất 20+ giây mới healthy, log đầy lỗi connection trong lúc startup. Trong production không có restart: always, app cứ nằm down.
Nguyên Nhân Gốc Rễ
depends_on trong Docker Compose chỉ chờ container start, không chờ service bên trong sẵn sàng:
# ❌ Cái này KHÔNG chờ postgres sẵn sàng
services:
api:
depends_on:
- db
db:
image: postgres:16Container api start ngay khi process db bắt đầu — dù Postgres chưa load xong.
Giải Pháp 1: Healthcheck + depends_on condition (Tốt nhất cho Compose)
Thêm healthcheck vào database service, rồi chờ nó với condition: service_healthy:
services:
api:
build: .
depends_on:
db:
condition: service_healthy # chờ cho đến khi db pass healthcheck
cache:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
interval: 5s # kiểm tra mỗi 5 giây
timeout: 5s # fail nếu không response trong 5s
retries: 10 # đánh dấu unhealthy sau 10 lần fail liên tiếp
start_period: 10s # không tính fail trong 10s đầu startup
cache:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5Với config này, Docker Compose sẽ không start api cho đến khi cả db và cache pass healthcheck. Không cần retry logic trong app cho startup race condition.
Lệnh healthcheck cho các database phổ biến:
# PostgreSQL
pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}
# MySQL / MariaDB
mysqladmin ping -h localhost -u root -p${MYSQL_ROOT_PASSWORD}
# MongoDB
mongosh --eval "db.adminCommand('ping')"
# Redis
redis-cli ping
# Elasticsearch
curl -f http://localhost:9200/_cluster/health?wait_for_status=yellowGiải Pháp 2: Retry Logic trong Application
Healthchecks xử lý startup race, nhưng bạn vẫn cần retry logic trong app vì hai lý do:
- Database có thể down trong khi app đang chạy (restart, failover, network blip)
- Healthchecks không phải lúc nào cũng available (Kubernetes, bare metal)
Node.js với Prisma:
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function connectWithRetry(maxAttempts = 10, delayMs = 3000): Promise<void> {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await prisma.$connect()
console.log('✅ Database đã kết nối')
return
} catch (error) {
console.error(`❌ Lần thử kết nối DB ${attempt}/${maxAttempts}:`, error.message)
if (attempt === maxAttempts) {
throw new Error(`Không thể kết nối database sau ${maxAttempts} lần thử`)
}
const backoff = Math.min(delayMs * attempt, 30000) // tối đa 30s
console.log(`Thử lại sau ${backoff / 1000}s...`)
await new Promise(resolve => setTimeout(resolve, backoff))
}
}
}
// Khi khởi động
await connectWithRetry()Python với SQLAlchemy:
import time
import logging
from sqlalchemy import create_engine, text
from sqlalchemy.exc import OperationalError
logger = logging.getLogger(__name__)
def connect_with_retry(database_url: str, max_attempts: int = 10, delay: float = 3.0):
engine = create_engine(database_url, pool_pre_ping=True)
for attempt in range(1, max_attempts + 1):
try:
with engine.connect() as conn:
conn.execute(text("SELECT 1"))
logger.info("✅ Database đã kết nối")
return engine
except OperationalError as e:
logger.error(f"❌ Lần thử DB {attempt}/{max_attempts}: {e}")
if attempt == max_attempts:
raise
backoff = min(delay * attempt, 30)
logger.info(f"Thử lại sau {backoff:.0f}s...")
time.sleep(backoff)
engine = connect_with_retry(os.environ["DATABASE_URL"])Go:
package db
import (
"database/sql"
"fmt"
"log"
"math"
"time"
_ "github.com/lib/pq"
)
func ConnectWithRetry(dsn string, maxAttempts int) (*sql.DB, error) {
var db *sql.DB
var err error
for attempt := 1; attempt <= maxAttempts; attempt++ {
db, err = sql.Open("postgres", dsn)
if err == nil {
err = db.Ping()
}
if err == nil {
log.Println("✅ Database đã kết nối")
return db, nil
}
log.Printf("❌ Lần thử DB %d/%d: %v", attempt, maxAttempts, err)
if attempt == maxAttempts {
break
}
backoff := math.Min(float64(attempt)*3, 30)
log.Printf("Thử lại sau %.0fs...", backoff)
time.Sleep(time.Duration(backoff) * time.Second)
}
return nil, fmt.Errorf("không thể kết nối sau %d lần thử: %w", maxAttempts, err)
}Giải Pháp 3: Dùng pool_pre_ping / Connection Validation
Cấu hình connection pool để test connection trước khi sử dụng. Xử lý không chỉ startup mà còn stale connections sau khi DB restart:
# SQLAlchemy — validate connection trước mỗi lần dùng
engine = create_engine(
DATABASE_URL,
pool_pre_ping=True, # test connection trước khi dùng
pool_recycle=3600, # recycle connections mỗi giờ
pool_size=10,
max_overflow=20,
)// Prisma — tự reconnect
const prisma = new PrismaClient({
datasources: {
db: { url: process.env.DATABASE_URL }
}
})
// Prisma xử lý reconnection nội bộGiải Pháp 4: Kubernetes Readiness Probes
Trong Kubernetes, bạn không dùng depends_on — bạn dùng readiness probes. Pod chỉ được thêm vào endpoint list của Service khi readiness probe pass:
spec:
containers:
- name: api
image: myapp:v1.0.0
readinessProbe:
httpGet:
path: /health/ready # trả về 200 chỉ khi DB đã kết nối
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 6
livenessProbe:
httpGet:
path: /health/live # trả về 200 nếu process còn sống
port: 8080
initialDelaySeconds: 15
periodSeconds: 10Endpoint /health/ready phải check kết nối database thực sự:
// Express health endpoints
app.get('/health/ready', async (req, res) => {
try {
await prisma.$queryRaw`SELECT 1`
res.json({ status: 'ready', db: 'connected' })
} catch (error) {
res.status(503).json({ status: 'not ready', db: 'disconnected' })
}
})
app.get('/health/live', (req, res) => {
res.json({ status: 'alive' })
})Liveness probe (/health/live) chỉ check process còn sống — luôn trả 200 khi process đang chạy. Readiness probe (/health/ready) check app có thể serve traffic không — trả 503 khi DB unavailable. Điều này ngăn Kubernetes route traffic đến pod khi DB down, mà không kill và restart pod không cần thiết.
Tổng Hợp Lại
Đây là docker-compose.yml production-grade xử lý cả hai failure mode:
services:
api:
image: myregistry.io/myapp:${APP_VERSION:?APP_VERSION là bắt buộc}
restart: unless-stopped
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
environment:
DATABASE_URL: postgresql://app:${DB_PASSWORD}@db:5432/myapp
REDIS_URL: redis://cache:6379
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
interval: 10s
timeout: 5s
retries: 3
start_period: 15s
ports:
- "127.0.0.1:8080:8080"
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD là bắt buộc}
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
cache:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redisdata:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
volumes:
pgdata:
redisdata:Và deploy script xử lý image pull failure:
#!/bin/bash
# deploy.sh
set -e
APP_VERSION=${1:?Cách dùng: ./deploy.sh <version>}
IMAGE="myregistry.io/myapp:${APP_VERSION}"
echo "🚀 Deploying $IMAGE"
# Bước 1: Pull image mới TRƯỚC khi động đến containers đang chạy
echo "📦 Đang pull image..."
MAX_RETRIES=3
for i in $(seq 1 $MAX_RETRIES); do
docker pull "$IMAGE" && break
if [ $i -eq $MAX_RETRIES ]; then
echo "❌ Pull thất bại sau $MAX_RETRIES lần. Hủy deploy."
exit 1
fi
echo "Retry $i/$MAX_RETRIES sau 15s..."
sleep 15
done
echo "✅ Pull thành công"
# Bước 2: Deploy (containers cũ vẫn chạy cho đến đây)
echo "🔄 Đang update service..."
APP_VERSION=$APP_VERSION docker compose up -d --no-deps api
echo "⏳ Chờ api trở nên healthy..."
timeout 60 bash -c 'until docker compose ps api | grep -q "healthy"; do sleep 2; done'
echo "✅ Deploy hoàn tất"Bảng Tóm Tắt Nhanh
Image pull failure:
| Scenario | Giải pháp |
|---|---|
| Registry down | Pull trước khi stop; local mirror |
| Auth hết hạn | Xoay credentials trong CI/CD secrets; verify trước deploy |
| Tag không tồn tại | Ghim version rõ ràng; verify tag trong CI trước deploy |
| Network timeout | Retry với backoff; local mirror; pre-pull trong CI |
| Rate limiting | Authenticate với Docker Hub; dùng private registry |
DB connection failure:
| Scenario | Giải pháp |
|---|---|
| DB chưa sẵn sàng khi startup | healthcheck + condition: service_healthy |
| DB restart khi app đang chạy | pool_pre_ping=True; retry logic trong app |
| Port DB chưa mở | Retry với exponential backoff khi startup |
| Kubernetes deployment | Readiness probe check /health/ready |
| DB failover | Connection pool với reconnect; circuit breaker |
Tóm Tắt và Điểm Chính
✅ Luôn pull image mới trước khi stop containers đang chạy — không bao giờ làm ngược lại
✅ Ghim version image rõ ràng trong production — :latest làm rollback bất khả thi
✅ Thêm retry với exponential backoff vào bước pull trong CI/CD pipeline
✅ Local registry mirror loại bỏ external registry outage như một deploy risk
✅ depends_on một mình KHÔNG chờ database sẵn sàng — phải thêm healthcheck
✅ condition: service_healthy trong Compose + pg_isready trong healthcheck là pattern đúng
✅ Thêm startup retry logic trong app như defense-in-depth — healthchecks không phải lúc nào cũng available
✅ Dùng pool_pre_ping=True (hoặc tương đương) để phát hiện stale connections trước khi dùng
✅ Trong Kubernetes: readiness probe trên /health/ready (check DB) + liveness trên /health/live (check process)
✅ Cú pháp APP_VERSION:? trong Compose biến missing env vars thành hard error trước khi deploy bắt đầu
Liên quan:
Docker Fundamentals — containers, images, concepts cốt lõi
Docker Compose & Multi-Container Apps — Compose deep dive
Docker Networking & Volumes — networking và storage
Lộ Trình Docker & Kubernetes — lộ trình học đầy đủ
Có câu chuyện Docker production failure? Đừng ngại liên hệ hoặc để lại comment!
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.