অধ্যায় 5 · চালু করুন
Docker Compose: API, PostgreSQL ও Redis একসাথে
- পৃষ্ঠা 18 / 22
- 23 মিনিট পড়া
বাস্তব AI ব্যাকএন্ড খুব কমই একটা প্রসেস। আগের পাতার রিভিউ অ্যানালাইজার কী কী বিশ্লেষণ করেছে মনে রাখতে ডেটাবেস লাগে, আর কাউন্টার ও বারবার-আসা কাজের জন্য ক্যাশ লাগে। হাতে চালালে সেটা তিনটা docker run কমান্ড, সঙ্গে নেটওয়ার্ক, ভলিউম, পাসওয়ার্ড আর চালু করার ক্রম ঠিক রাখা: প্রতিবার, প্রতিটা ল্যাপটপে। Docker Compose এই সবকিছু একটা ফাইলে (compose.yaml) আর একটা কমান্ডে (docker compose up) নিয়ে আসে।
এই পাতায় আপনি তিনটা সার্ভিসের একটা স্ট্যাক বানাবেন: FastAPI API, PostgreSQL ও Redis, সঙ্গে একটা একবার-চলা মাইগ্রেশন জব। প্রতিটা ফাইল নিজে লিখবেন, আর Docker ছাড়া যে অংশগুলো যাচাই করা যায় সেগুলো চালিয়ে দেখবেন: সেটিংস কোড, রেডিনেস এন্ডপয়েন্ট, এবং compose.yaml পড়ে সত্যি কী লেখা আছে তার তালিকা দেখানো একটা স্ক্রিপ্ট। docker compose কমান্ডগুলো এখানে আউটপুট ছাড়া দেওয়া হয়েছে; নিজের মেশিনে চালালে ঠিক কী দেখবেন, লেখায় তা বর্ণনা করা আছে।
যা শিখবেন
compose.yamlকীভাবে সার্ভিস, নেটওয়ার্ক, ভলিউম ও সিক্রেট বর্ণনা করে, এবং সার্ভিসগুলো নাম ধরে একে অপরকে কীভাবে খুঁজে পায়।- হেলথ চেক এবং
condition: service_healthyদিয়েdepends_on, আর লাইভনেস ও রেডিনেসের পার্থক্য। - কনফিগারেশন:
.envবনামenv_file, এবং/run/secrets-এ মাউন্ট হওয়া Compose সিক্রেট, যা পাইথন থেকে পড়া হয়। - মাইগ্রেশনকে একবার-চলা সার্ভিস হিসেবে চালানো, এবং ডেভেলপমেন্ট বনাম প্রোডাকশন ওভাররাইড।
- প্রতিদিনের কমান্ড:
up,down,ps,logs,exec।
কেন Compose
হাতে চালালে শুধু ডেটাবেসের কমান্ডই লম্বা, আর সঙ্গে মনে রাখতে হয় API যাতে ডেটাবেসে পৌঁছাতে পারে সে জন্য একটা নেটওয়ার্ক, আর ডেটা যাতে টিকে থাকে সে জন্য একটা ভলিউম বানাতে:
docker network create review-net
docker volume create review-pgdata
docker run -d --name review-db --network review-net -v review-pgdata:/var/lib/postgresql \
-e POSTGRES_PASSWORD=change-me postgres:18-alpineএর সঙ্গে Redis, API, তার পোর্ট ও ভেরিয়েবল আর চালু করার ক্রম যোগ করুন। Compose এই সবকিছু এমন একটা ফাইলে রাখে যা আপনি পড়তে পারেন, পুল রিকোয়েস্টে রিভিউ করতে পারেন, আর Git-এ কমিট করতে পারেন। docker compose লিখুন মাঝে স্পেস দিয়ে: এটা Docker-এর নিজেরই অংশ (২০২৬-এ Compose v5; পুরোনো পাইথন docker-compose কমান্ড বাতিল হয়ে গেছে)। ফাইলের নাম compose.yaml, আর আগেকার শীর্ষস্তরের version: কি এখন উপেক্ষা করা হয়, তাই বাদ দিন।
| Compose-এর ধারণা | এটা কী |
|---|---|
| প্রজেক্ট | স্ট্যাকের একটা চলমান কপি। নাম আসে ফোল্ডার বা শীর্ষস্তরের name: থেকে; কন্টেইনার, নেটওয়ার্ক ও ভলিউমের নামের আগে বসে। |
| সার্ভিস | স্ট্যাকের এক ধরনের কন্টেইনার (api, db, cache)। এটা একাধিক কন্টেইনার হিসেবেও চলতে পারে। |
| নেটওয়ার্ক | একটা ব্যক্তিগত ভার্চুয়াল নেটওয়ার্ক। এতে থাকা সার্ভিস একে অপরকে সার্ভিসের নামে পায়। |
| ভলিউম | Docker-এর পরিচালিত স্টোরেজ, যা কন্টেইনারের চেয়ে বেশিদিন টিকে। |
| সিক্রেট | যে সার্ভিস চায় তার ভেতরে /run/secrets/<name>-এ শুধু-পড়া ফাইল হিসেবে মাউন্ট হওয়া মান। |
প্রজেক্ট
একটা ফোল্ডার ও ভার্চুয়াল এনভায়রনমেন্ট বানান। এনভায়রনমেন্টটা শুধু নিজের মেশিনে চালানো যাচাইগুলোর জন্য; কন্টেইনার পাবে তার নিজের পাইথন:
mkdir -p ~/projects/review-stack
cd ~/projects/review-stack
python3 -m venv .venv
source .venv/bin/activate
pip install -q "fastapi[standard]" "psycopg[binary]" redis pydantic-settings pyyaml httpx2পাইথনে সেটিংস ও সিক্রেট
API তার কনফিগারেশন পায় এনভায়রনমেন্ট থেকে: ডেটাবেস কোথায়, Redis কোথায়, ভার্সন কত। যে জিনিসটা সাধারণ এনভায়রনমেন্ট ভেরিয়েবলে যাওয়া উচিত নয়, সেটা ডেটাবেসের পাসওয়ার্ড, তাই কোড সেটা সিক্রেট ফাইল থাকলে সেখান থেকে পড়ে। Compose প্রতিটা সিক্রেট /run/secrets-এ সিক্রেটের নামের ফাইল হিসেবে মাউন্ট করে। যে প্ল্যাটফর্ম পুরো DATABASE_URL দেয়, সেটাও কাজ করে, এবং সেট থাকলে সেটাই প্রাধান্য পায়:
# file: settings.py
"""Settings: plain environment variables plus Docker/Compose secret files."""
import os
from pathlib import Path
from urllib.parse import quote
from pydantic_settings import BaseSettings
def read_secret(name: str) -> str | None:
"""Return /run/secrets/<name> (or $SECRETS_DIR/<name>), or None if there is no such file."""
path = Path(os.environ.get("SECRETS_DIR", "/run/secrets")) / name
return path.read_text().strip() if path.is_file() else None
class Settings(BaseSettings):
app_version: str = "dev"
database_url: str | None = None # a platform may hand you the whole URL
db_host: str = "localhost"
db_port: int = 5432
db_name: str = "reviews"
db_user: str = "app"
redis_url: str = "redis://localhost:6379/0"
@property
def postgres_dsn(self) -> str:
if self.database_url:
return self.database_url
password = read_secret("db_password") or os.environ.get("DB_PASSWORD", "")
return f"postgresql://{self.db_user}:{quote(password, safe='')}@{self.db_host}:{self.db_port}/{self.db_name}"নিজের ডিস্কে একটা সিক্রেট ফাইল দিয়ে পরীক্ষা করুন। প্রজেক্টের পাশে secrets নামের ফোল্ডারটাই পরে Compose মাউন্ট করবে, আর আপাতত SECRETS_DIR কোডকে সেদিকে দেখায়। পাসওয়ার্ডে এমন একটা অক্ষর আছে যা URL-এ বিশেষ, তাই quote কাজ করছে দেখতে পাবেন:
mkdir -p secrets
printf 'dev-only-pw!' > secrets/db_password
chmod 644 secrets/db_password
ls secretsdb_passwordimport os
os.environ.update(SECRETS_DIR="secrets", DB_HOST="db", APP_VERSION="1.0.0")
from settings import Settings
s = Settings()
print(s.app_version, s.db_host, s.db_port)
print(s.postgres_dsn)1.0.0 db 5432
postgresql://app:dev-only-pw%21@db:5432/reviewsহোস্ট db, localhost নয়; পরের অংশে কারণ আছে। (ওটা একটা ফেলনা ডেভেলপমেন্ট পাসওয়ার্ড। আসল কানেকশন স্ট্রিং কখনো প্রিন্ট বা লগ করবেন না।) লক্ষ করুন, chmod 644 ফাইলটাকে অন্য ব্যবহারকারীদের জন্যও পড়ার যোগ্য করে। কন্টেইনারের ব্যবহারকারী আপনি নন: API চলে UID 10001 হিসেবে, তাই শুধু আপনি পড়তে পারেন এমন ফাইল কন্টেইনারের ভেতরে পড়া নাও যেতে পারে। ডেভেলপমেন্টের পাসওয়ার্ডে এটা ঠিক আছে; প্রোডাকশনের সিক্রেট আসে আপনার প্ল্যাটফর্মের সিক্রেট স্টোর থেকে, প্রজেক্টের ফাইল থেকে নয়।
API
API-তে দুটো হেলথ এন্ডপয়েন্ট আছে, কাজ আলাদা। /health হলো লাইভনেস: "প্রসেস চলছে", কোনো ডিপেন্ডেন্সি পরীক্ষা করে না। /ready হলো রেডিনেস: "আমি সত্যিই রিকোয়েস্ট সামলাতে পারি", তাই এটা PostgreSQL ও Redis পরীক্ষা করে, আর কোনোটা বন্ধ থাকলে 503 দেয়। লোড ব্যালান্সারের উচিত জীবিত কিন্তু প্রস্তুত নয় এমন ইনস্ট্যান্সে ট্রাফিক পাঠানো বন্ধ করা, সেটাকে রিস্টার্ট না করে। বিশ্লেষণ এন্ডপয়েন্ট প্রতিটা রিভিউ PostgreSQL-এ রাখে আর Redis-এ গোনে। (সংক্ষেপের জন্য এটা প্রতি রিকোয়েস্টে একটা কানেকশন খোলে; আসল সার্ভিসের জন্য ইঞ্জিন ও সেশন-প্রতি-রিকোয়েস্ট প্যাটার্ন SQLAlchemy ও Alembic দিয়ে ডেটাবেস পাতায় দেখানো হয়েছে।)
# file: main.py
"""Review API for Compose: FastAPI + PostgreSQL + Redis."""
import logging
from typing import Annotated
import psycopg
import redis
from fastapi import FastAPI, Response
from pydantic import BaseModel, Field
from settings import Settings
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
log = logging.getLogger("review-api")
settings = Settings()
cache = redis.Redis.from_url(settings.redis_url, socket_connect_timeout=2, socket_timeout=2)
app = FastAPI(title="Review Analyser", version=settings.app_version)
POSITIVE = {"good", "great", "excellent", "clear", "helpful", "love"}
NEGATIVE = {"bad", "boring", "confusing", "slow", "hate", "broken"}
class Review(BaseModel):
text: Annotated[str, Field(min_length=3, max_length=2000)]
@app.get("/health")
def health() -> dict[str, str]:
"""Liveness: the process is up. Checks nothing else."""
return {"status": "ok", "version": settings.app_version}
@app.get("/ready")
def ready(response: Response) -> dict[str, str]:
"""Readiness: can this instance serve real requests right now?"""
checks = {}
try:
with psycopg.connect(settings.postgres_dsn, connect_timeout=2) as conn:
conn.execute("SELECT 1")
checks["postgres"] = "up"
except psycopg.Error:
checks["postgres"] = "down"
try:
cache.ping()
checks["redis"] = "up"
except redis.RedisError:
checks["redis"] = "down"
ok = all(state == "up" for state in checks.values())
if not ok:
response.status_code = 503
return {"status": "ready" if ok else "unavailable", **checks}
@app.post("/v1/analyse")
def analyse(review: Review) -> dict[str, str | float]:
words = [w.strip(".,!?").lower() for w in review.text.split()]
score = round((sum(w in POSITIVE for w in words) - sum(w in NEGATIVE for w in words)) / max(len(words), 1), 2)
sentiment = "positive" if score > 0 else "negative" if score < 0 else "neutral"
with psycopg.connect(settings.postgres_dsn) as conn:
conn.execute("INSERT INTO reviews (text, sentiment) VALUES (%s, %s)", (review.text, sentiment))
cache.incr("analysed_total")
log.info("stored a %s review", sentiment)
return {"sentiment": sentiment, "score": score}
@app.get("/v1/stats")
def stats() -> dict[str, int]:
with psycopg.connect(settings.postgres_dsn) as conn:
stored = conn.execute("SELECT count(*) FROM reviews").fetchone()[0]
return {"stored": stored, "analysed_total": int(cache.get("analysed_total") or 0)}আপনার ল্যাপটপে এখন দুটো ডেটাবেসই বন্ধ, যা রেডিনেস পরীক্ষার ভালো সুযোগ। সেটিংস একটা বন্ধ পোর্টে (পোর্ট ১-এ কেউ শোনে না) দেখান, তারপর FastAPI-র TestClient দিয়ে অ্যাপকে জিজ্ঞেস করুন:
import os
os.environ.update(DB_HOST="127.0.0.1", DB_PORT="1", REDIS_URL="redis://127.0.0.1:1/0")
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
print(client.get("/health").json())
r = client.get("/ready")
print(r.status_code, r.json()){'status': 'ok', 'version': 'dev'}
503 {'status': 'unavailable', 'postgres': 'down', 'redis': 'down'}লাইভনেস সবুজ থাকে, আর রেডিনেস ঠিক বলে দেয় কোন ডিপেন্ডেন্সি নেই। Compose স্ট্যাকের ভেতরে একই এন্ডপয়েন্ট জানাবে "postgres": "up" আর "redis": "up"।
মাইগ্রেশন স্ক্রিপ্ট
API কিছু রাখার আগে টেবিল থাকতে হবে। একটা ছোট idempotent স্ক্রিপ্ট সেগুলো বানায় (IF NOT EXISTS, তাই প্রতিবার চালুর সময় নিরাপদে চালানো যায়)। আসল প্রজেক্টে এই জায়গায় বসে alembic upgrade head, ঠিক SQLAlchemy ও Alembic দিয়ে ডেটাবেস পাতার মতো:
# file: migrate.py
"""Create the schema. Safe to run on every deploy."""
import psycopg
from settings import Settings
SCHEMA = """
CREATE TABLE IF NOT EXISTS reviews (
id SERIAL PRIMARY KEY,
text TEXT NOT NULL,
sentiment TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
"""
if __name__ == "__main__":
with psycopg.connect(Settings().postgres_dsn) as conn:
conn.execute(SCHEMA)
print("schema ready")ইমেজ
এটা Docker: আপনার API প্যাকেজ করা পাতার একক-স্টেজ pip Dockerfile, একটার বদলে তিনটা কোড ফাইলসহ। (ওই পাতার মাল্টি-স্টেজ uv Dockerfile শুধু COPY লাইন বদলে হুবহু কাজ করে; আপনার টিম যেটা পছন্দ করে সেটা নিন।) ডিপেন্ডেন্সি থাকে requirements.txt-এ; আসল প্রজেক্টে আগের পাতার মতো uv export দিয়ে সেটা পিন করুন:
cat > requirements.txt <<'EOF'
fastapi[standard]>=0.143.0
psycopg[binary]>=3.3
redis>=8.1
pydantic-settings>=2.15
EOF# file: Dockerfile
FROM python:3.14-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PORT=8000
RUN useradd --create-home --uid 10001 app
WORKDIR /app
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY main.py settings.py migrate.py ./
USER app
EXPOSE 8000
CMD ["fastapi", "run", "main.py"]# file: .dockerignore
.venv/
__pycache__/
.git/
.env
secrets/
*.logcompose.yaml লেখা
প্রথমে দুটো ছোট env ফাইল। app.env-এ কন্টেইনারের জন্য সিক্রেট-নয় এমন সেটিংস থাকে; এটা কমিট করা নিরাপদ। .env অন্য জিনিস (নিচে দেখুন) এবং Git-এর বাইরে থাকে:
# file: app.env
DB_HOST=db
DB_NAME=reviews
DB_USER=app
REDIS_URL=redis://cache:6379/0# file: .env
APP_VERSION=1.0.0# file: .gitignore
.venv/
__pycache__/
.env
secrets/এবার স্ট্যাক নিজে। ওপর থেকে নিচে পড়ুন; প্রতিটা অংশের পরেই তার ব্যাখ্যা আছে:
# file: compose.yaml
name: review-stack
# Settings shared by the API and the migration job (a YAML anchor).
x-app: &app
build: .
image: bitbyte-p18-api:${APP_VERSION:-dev}
env_file: [app.env]
environment:
APP_VERSION: ${APP_VERSION:-dev}
secrets: [db_password]
networks: [backend]
services:
api:
<<: *app
ports:
- "8000:8000"
networks: [public, backend]
depends_on:
db: {condition: service_healthy}
cache: {condition: service_healthy}
migrate: {condition: service_completed_successfully}
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=3)"]
interval: 10s
timeout: 5s
retries: 3
start_period: 15s
restart: unless-stopped
migrate:
<<: *app
command: ["python", "migrate.py"]
depends_on:
db: {condition: service_healthy}
restart: "no"
db:
image: postgres:18-alpine
environment:
POSTGRES_USER: app
POSTGRES_DB: reviews
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets: [db_password]
volumes:
- pgdata:/var/lib/postgresql
networks: [backend]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d reviews"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
restart: unless-stopped
cache:
image: redis:8-alpine
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redisdata:/data
networks: [backend]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
networks:
public: {}
backend:
internal: true
volumes:
pgdata:
redisdata:
secrets:
db_password:
file: ./secrets/db_passwordসার্ভিস, ইমেজ ও বিল্ড
services:-এর নিচের প্রতিটা কি একটা সার্ভিসের নাম। সার্ভিসের হয় একটা পুল করার image থাকে (postgres:18-alpine, redis:8-alpine), নয়তো Dockerfile-সহ একটা build কনটেক্সট (আমাদের API)। দুটোই থাকলে, যেমন api-তে, Compose ইমেজটা বিল্ড করে ওই নামে ট্যাগ দেয়। ${APP_VERSION:-dev} হলো ইন্টারপোলেশন: APP_VERSION এনভায়রনমেন্ট বা .env ফাইলে থাকলে সেটা নিন, না থাকলে dev। x-app ব্লক এবং &app / <<: *app জোড়া সাধারণ YAML অ্যাঙ্কর: x- দিয়ে শুরু হওয়া কি Compose উপেক্ষা করে, তাই api আর migrate-এর মধ্যে সেটিংস ভাগ করার বিনা খরচের জায়গা। <<: *app-এর পাশে লেখা কি, যেমন API-র নিজের networks, অ্যাঙ্করের মানের ওপর প্রাধান্য পায়।
নেটওয়ার্ক: সার্ভিস একে অপরকে কীভাবে খোঁজে
একই নেটওয়ার্কের সার্ভিস একে অপরকে সার্ভিসের নামে পায়। Compose একটা ছোট DNS সার্ভার চালায়, তাই API কানেক্ট করে db:5432 আর cache:6379-এ, কখনো IP ঠিকানায় নয় (প্রতিটা রিস্টার্টে ওগুলো বদলায়)। এ কারণেই app.env-এ DB_HOST=db। আর কন্টেইনারের ভেতরের localhost মানে ওই কন্টেইনারটাই, আপনার ল্যাপটপ বা অন্য সার্ভিস নয়: API-কে localhost:5432-এ দেখানো Compose-এর প্রথম ক্লাসিক ভুল।
| কোড কোথায় চলছে | PostgreSQL-এ পৌঁছাবেন যেভাবে |
|---|---|
api কন্টেইনারের ভেতরে | db:5432 (সার্ভিসের নাম আর কন্টেইনারের নিজের পোর্ট) |
আপনার ল্যাপটপে, যেমন psql বা টেস্ট রান | localhost:5432, কেবল db সার্ভিস ports পাবলিশ করলে |
ফাইলে ইচ্ছে করেই দুটো নেটওয়ার্ক আছে। public পাবলিশ করা পোর্ট বহন করে। backend-এ দেওয়া আছে internal: true: এর সদস্যরা নিজেদের মধ্যে কথা বলতে পারে কিন্তু বাইরের দুনিয়ার কোনো পথ নেই। ডেটাবেস ও ক্যাশ শুধু backend-এ, তাই ওখানে কোনো কন্টেইনার আক্রান্ত হলেও বাইরে যোগাযোগ করতে পারবে না। API দুটোতেই আছে, কারণ তাকে ইন্টারনেটেও পৌঁছাতে হয় (ধরুন একটা LLM প্রোভাইডার)। কী নেই তা লক্ষ করুন: db ও cache-এর কোনো ports নেই। পাবলিশ করা আছে শুধু API; ডেটাস্টোর কেবল অন্য সার্ভিস থেকে পৌঁছানো যায়। ডেটাবেসের পোর্ট পুরো ইন্টারনেটে খুলে দেওয়া একটা ক্লাসিক ব্রিচ।
ভলিউম
কন্টেইনার মুছলে তার নিজের ফাইলসিস্টেম ফেলে দেওয়া হয়। নামযুক্ত ভলিউম (pgdata, redisdata) Docker কোনো কন্টেইনারের বাইরে সংরক্ষণ করে, তাই docker compose down-এর পরে up করলেও আপনার রিভিউ থেকে যায়। মাউন্টের জায়গা গুরুত্বপূর্ণ: PostgreSQL 18 ইমেজ ডেটা রাখে /var/lib/postgresql-এর নিচে (পুরোনো ইমেজ ব্যবহার করত /var/lib/postgresql/data), তাই ভলিউম সেখানে যায়; Redis লেখে /data-তে, আর --appendonly yes লেখা টিকিয়ে রাখে। বাইন্ড মাউন্ট (./folder:/path) বরং আপনার মেশিনের একটা ফোল্ডার ম্যাপ করে; নিচে ডেভেলপমেন্টের জন্য একটা ব্যবহার করবেন।
হেলথ চেক ও চালু করার ক্রম
কন্টেইনার "চালু" হয় তার প্রসেস শুরু হওয়ার মুহূর্তেই; PostgreSQL-এর তারপর কানেকশন নেওয়ার মতো তৈরি হতে আরও কয়েক সেকেন্ড লাগে। healthcheck হলো এমন একটা কমান্ড যা Docker প্রতি interval-এ কন্টেইনারের ভেতরে চালায়; এক্সিট কোড ০ মানে সুস্থ। start_period ধীরে-চালু-হওয়া সার্ভিসকে একটা ছাড় দেয়, যে সময়ের ব্যর্থতা গোনা হয় না; পরপর retries বার ব্যর্থ হলে কন্টেইনার unhealthy চিহ্নিত হয়। এখানকার চেকগুলো প্রতিটা ইমেজে আগে থেকেই থাকা টুল ব্যবহার করে: PostgreSQL-এর জন্য pg_isready, Redis-এর জন্য redis-cli ping, আর API-র জন্য পাইথনের স্ট্যান্ডার্ড লাইব্রেরি (slim ইমেজে curl নেই)। Docker-এর চেক ব্যবহার করে /health, লাইভনেস এন্ডপয়েন্ট, কারণ রিস্টার্ট দিয়ে হারানো ডেটাবেস ফেরানো যায় না; /ready লোড ব্যালান্সার ও আপনার নিজের মনিটরিংয়ের জন্য।
condition-সহ depends_on এটাকে চালু করার ক্রমে বদলে দেয়:
| কন্ডিশন | ডিপেন্ডেন্সি কখন পর্যন্ত অপেক্ষা… |
|---|---|
service_started (ডিফল্ট) | চালু হওয়া পর্যন্ত। তৈরি হওয়া নিয়ে কিছু বলে না, তাই API প্রায়ই প্রথম কোয়েরিতেই ক্র্যাশ করবে। |
service_healthy | হেলথ চেক পাস করা পর্যন্ত। ওই সার্ভিসে healthcheck থাকতে হয়। |
service_completed_successfully | এক্সিট কোড ০ দিয়ে শেষ হওয়া পর্যন্ত। migrate-এর মতো একবার-চলা জবের জন্য। |
তাই ক্রমটা এমন: db ও cache সুস্থ হয়, migrate টেবিল বানিয়ে বেরিয়ে যায়, আর তখনই শুধু api চালু হয়। জবের restart: "no" (কোটেশন লাগবেই, নইলে YAML সেটাকে false পড়ে) সেটাকে অনন্তকাল ধরে আবার চলা থেকে আটকায়; দীর্ঘক্ষণ চলা সার্ভিসের unless-stopped ক্র্যাশ বা রিবুটের পর তাদের ফিরিয়ে আনে। মনে রাখবেন, চালু করার ক্রম পরের জন্য কোনো গ্যারান্টি নয়: API চলার সময়ও ডেটাবেস রিস্টার্ট হতে পারে। আসল কোডেও রিট্রাই থাকা উচিত, আর তখন /ready ডাউন জানানো উচিত।
নিজের লেখা ফাইল পড়া
compose.yaml তো শুধু YAML, তাই কয়েক লাইন পাইথন দিয়ে দেখে নিতে পারেন। এই স্ক্রিপ্ট রিভিউয়ের কাজে লাগে, আর নিশ্চিত করে ফাইলে ঠিক তা-ই লেখা আছে যা আপনি চেয়েছেন (অ্যাঙ্করসহ):
import yaml
cfg = yaml.safe_load(open("compose.yaml"))
for name, svc in cfg["services"].items():
deps = {d: c["condition"] for d, c in svc.get("depends_on", {}).items()}
print(f"{name:8} image={svc['image']}")
print(f" ports={svc.get('ports', [])} healthcheck={'healthcheck' in svc} networks={svc['networks']}")
print(f" depends_on={deps}")
print("volumes:", list(cfg["volumes"]), "| networks:", list(cfg["networks"]), "| secrets:", list(cfg["secrets"]))api image=bitbyte-p18-api:${APP_VERSION:-dev}
ports=['8000:8000'] healthcheck=True networks=['public', 'backend']
depends_on={'db': 'service_healthy', 'cache': 'service_healthy', 'migrate': 'service_completed_successfully'}
migrate image=bitbyte-p18-api:${APP_VERSION:-dev}
ports=[] healthcheck=False networks=['backend']
depends_on={'db': 'service_healthy'}
db image=postgres:18-alpine
ports=[] healthcheck=True networks=['backend']
depends_on={}
cache image=redis:8-alpine
ports=[] healthcheck=True networks=['backend']
depends_on={}
volumes: ['pgdata', 'redisdata'] | networks: ['public', 'backend'] | secrets: ['db_password']শুধু api-র পোর্ট আছে; db ও cache-এর নেই। migrate-এর হেলথ চেক নেই, কারণ ওটা সার্ভার নয়, একটা জব।
এনভায়রনমেন্ট ফাইল ও সিক্রেট
Compose-এ দুটো আলাদা env-ফাইল সুবিধা আছে, আর এদের গুলিয়ে ফেলা থেকে অনেক বাগ হয়:
.env ফাইল | env_file: কি | |
|---|---|---|
| কে পড়ে | Compose নিজে, প্রজেক্ট ফোল্ডার থেকে, স্বয়ংক্রিয়ভাবে | Compose, যে সার্ভিসগুলো এটা তালিকাভুক্ত করে তাদের জন্য |
| কীসের জন্য | compose.yaml-এর ভেতরের ${VARIABLES} ভরা | কন্টেইনারের ভেতরে এনভায়রনমেন্ট ভেরিয়েবল সেট করা |
| এই প্রজেক্টে | APP_VERSION (ইমেজ ট্যাগ ও ভার্সন) | app.env (DB_HOST, REDIS_URL…) |
.env-এর ভেরিয়েবল কন্টেইনারে দেখা যায় না, যদি না compose.yaml সেটা এগিয়ে দেয়, যেমন environment: APP_VERSION: ${APP_VERSION:-dev} দিচ্ছে। একই ভেরিয়েবলের অগ্রাধিকার, সবচেয়ে বেশি থেকে: আপনার শেলের এনভায়রনমেন্ট, তারপর .env, তারপর :--এর পরের ডিফল্ট। Compose যেভাবে পড়ে সেভাবে env ফাইল পড়া যায় python-dotenv দিয়ে, যা বিগিনার টিউটোরিয়ালের এনভায়রনমেন্ট ভেরিয়েবল, .env ফাইল ও ভার্চুয়াল এনভায়রনমেন্ট পাতায় ছিল; এখানে স্ট্যান্ডার্ড লাইব্রেরিই যথেষ্ট:
def read_env_file(path):
pairs = (line.split("=", 1) for line in open(path).read().splitlines() if "=" in line and not line.startswith("#"))
return {key: value for key, value in pairs}
print(read_env_file(".env"))
print(read_env_file("app.env")){'APP_VERSION': '1.0.0'}
{'DB_HOST': 'db', 'DB_NAME': 'reviews', 'DB_USER': 'app', 'REDIS_URL': 'redis://cache:6379/0'}সিক্রেট
environment:-এ পাসওয়ার্ড সহজেই ফাঁস হয়: docker inspect-এ, ক্র্যাশ রিপোর্টে আর চাইল্ড প্রসেসে দেখা যায়। Compose সিক্রেট আলাদা। শীর্ষস্তরের secrets: সেকশন বলে মানটা হোস্টে কোথায় আছে (file: ./secrets/db_password); প্রতিটা সার্ভিস তালিকা করে কোন সিক্রেট সে দেখতে পাবে; আর ওই কন্টেইনারের ভেতরে মানটা ফাইল হিসেবে আসে, /run/secrets/db_password। আমাদের স্ট্যাকের দুটো সার্ভিস সিক্রেটটা দুইভাবে নেয়:
- PostgreSQL
_FILEরীতি সমর্থন করে:POSTGRES_PASSWORD_FILE=/run/secrets/db_passwordইমেজকেPOSTGRES_PASSWORD-এর বদলে ফাইল থেকে পাসওয়ার্ড পড়ায়। অনেক অফিসিয়াল ইমেজ তাদের ভেরিয়েবলের জন্য একই কাজ করে। - আমাদের API ডাকে
read_secret("db_password"), যা আপনি ওপরেSECRETS_DIRদিয়ে পরীক্ষা করেছেন। কন্টেইনারে কোনো ভেরিয়েবল লাগে না, কারণ/run/secrets-ই ডিফল্ট।
এটা আপনাকে কী দেয়, সে ব্যাপারে সৎ থাকা যাক। Compose ফাইল সিক্রেট এনক্রিপ্ট করা নয়: এটা আপনার হোস্টের একটা ফাইলের শুধু-পড়া মাউন্ট, যা মানটাকে এনভায়রনমেন্ট তালিকা, ইমেজ লেয়ার ও খোদ compose.yaml থেকে দূরে রাখে। secrets/ .gitignore ও .dockerignore দুটোতেই রাখুন (ওপরে করা হয়েছে)। প্রোডাকশনে একই ফাইল-পদ্ধতিতে সিক্রেট জোগান দিন আপনার প্ল্যাটফর্মের সিক্রেট ম্যানেজার থেকে। আরও লক্ষ করুন, PostgreSQL POSTGRES_PASSWORD_FILE প্রয়োগ করে শুধু ফাঁকা ভলিউম প্রথমবার তৈরির সময়; পরে ফাইল বদলালে ডেটাবেসের পাসওয়ার্ড বদলায় না, তাই SQL দিয়ে বদলান, নয়তো ভলিউম মুছুন (তাতে ডেটাও যায়)।
স্ট্যাক চালানো
ফাইলগুলো জায়গামতো থাকলে সবকিছু ব্যাকগ্রাউন্ডে (-d) বিল্ড করে চালু করুন। --build আগে API ইমেজ আবার বিল্ড করে:
docker compose up -d --build
docker compose psপ্রথমবারে postgres ও redis পুল হয় আর API বিল্ড হয়, তাই এক-দুই মিনিট লাগে। Compose নেটওয়ার্ক ও ভলিউম বানায়, db ও cache চালু করে, তাদের জন্য অপেক্ষা করে, migrate চালায়, তারপর api চালু করে। docker compose ps একটা টেবিল ছাপে, কলাম NAME, IMAGE, COMMAND, SERVICE, CREATED, STATUS ও PORTS। আপনি দেখবেন তিনটা চলমান কন্টেইনার, নাম review-stack-api-1, review-stack-db-1, review-stack-cache-1-এর মতো, প্রত্যেকটার STATUS-এ (healthy), এবং শুধু API-র PORTS-এ 0.0.0.0:8000->8000/tcp। শেষ হয়ে যাওয়া migrate কন্টেইনার তালিকায় আসে না, কারণ ps চলমান কন্টেইনার দেখায়; docker compose ps -a সেটাকে Exited (0) হিসেবে দেখায়। এবার হোস্ট থেকে API ব্যবহার করুন। up ফিরে আসার পরও API প্রসেসের শোনা শুরু করতে এক-দুই সেকেন্ড লাগে, এ কারণেই প্রথম curl রিট্রাই করে:
curl -s --retry 30 --retry-all-errors --retry-delay 1 http://localhost:8000/ready
echo
curl -s -X POST http://localhost:8000/v1/analyse -H "Content-Type: application/json" -d '{"text": "Great course, very clear and helpful!"}'
echo
curl -s http://localhost:8000/v1/statsপ্রথম কলে ফেরে {"status":"ready","postgres":"up","redis":"up"}: আগের রেডিনেস চেক, এবার পেছনে আসল সার্ভিসসহ। দ্বিতীয়টা ফেরায় {"sentiment":"positive","score":0.5}, আর তৃতীয়টা {"stored":1,"analysed_total":1}: PostgreSQL-এ একটা সারি, Redis-এ একটা গণনা। ইন্টারেক্টিভ ডকের জন্য ব্রাউজারে http://localhost:8000/docs খুলুন।
| কমান্ড | কী করে |
|---|---|
docker compose up -d | সবকিছু বানিয়ে ব্যাকগ্রাউন্ডে চালু করে। ইমেজ আবার বিল্ড করতে --build যোগ করুন। |
docker compose ps / ps -a | সার্ভিস, তাদের অবস্থা ও হেলথ দেখায় (-a থেমে-যাওয়াগুলোও আনে)। |
docker compose logs -f api | একটা সার্ভিসের লগ অনুসরণ করে (সব দেখতে নাম বাদ দিন)। --tail 50 সীমা দেয়। |
docker compose exec db psql -U app reviews | চলমান কন্টেইনারের ভেতরে কমান্ড চালায়, এখানে একটা ইন্টারেক্টিভ SQL শেল। |
docker compose run --rm migrate | সার্ভিসের নতুন একটা একবারের কন্টেইনার চালায়, তারপর মুছে দেয়। |
docker compose restart api | একটা সার্ভিস রিস্টার্ট করে। |
docker compose stop / start | কন্টেইনার না মুছে থামায় বা চালু করে। |
docker compose down | থামায় এবং কন্টেইনার ও নেটওয়ার্ক মুছে দেয়। ভলিউম (আপনার ডেটা) থেকে যায়। |
docker compose down -v | ভলিউমও মুছে দেয়: এতে ডেটাবেস চলে যায়। |
docker compose config | ইন্টারপোলেশন ও মার্জের পর চূড়ান্ত ফাইল ছাপে: কিছু ঠিক না লাগলে প্রথমে এটা চালান। |
ভলিউম যে কাজ করছে তা প্রমাণ করতে এটা চালিয়ে আবার /v1/stats ডাকুন। এখনো সংরক্ষিত রিভিউ দেখাবে, কারণ কন্টেইনার বদলেছে কিন্তু pgdata বদলায়নি:
docker compose down
docker compose up -d
curl -s --retry 30 --retry-all-errors --retry-delay 1 http://localhost:8000/ready > /dev/null
curl -s http://localhost:8000/v1/statsএখানে প্রতিটা up-এ মাইগ্রেশন চলে, কারণ জবটা idempotent। স্কিমা বদলালে নতুন API ভার্সন ট্রাফিক নেওয়ার আগে জবটা স্পষ্টভাবে আবার চালান (docker compose run --rm migrate); আসল প্ল্যাটফর্মে একই জব রিলিজ বা প্রি-ডিপ্লয় ধাপ হিসেবে চলে। ডিপ্লয় নিরাপদ রাখার নিয়ম: প্রতিটা মাইগ্রেশন আগের API ভার্সনের সঙ্গেও সামঞ্জস্যপূর্ণ রাখুন, যাতে পুরোনো ও নতুন কন্টেইনার কিছুক্ষণ পাশাপাশি চলতে পারে।
ডেভেলপমেন্ট ও প্রোডাকশন ওভাররাইড
একই স্ট্যাক আপনার ল্যাপটপে আর সার্ভারে আলাদা আচরণ করা উচিত। ফাইলের দুটো কপি না রেখে একটা বেস compose.yaml রাখুন আর পার্থক্যগুলো ছোট ওভাররাইড ফাইলে। সাধারণ docker compose up চালালে Compose স্বয়ংক্রিয়ভাবে compose.yaml-এর ওপর compose.override.yaml মার্জ করে। ডেভেলপমেন্টের সুবিধার জায়গা এটাই: আপনার সোর্স কোড কন্টেইনারে মাউন্ট করে অটো-রিলোড, আর ল্যাপটপের টুল থেকে পৌঁছানো যায় এমন একটা ডেটাবেস। backend বাইরের কোনো রুট-ছাড়া ইন্টারনাল নেটওয়ার্ক, তাই শুধু সেখানে থাকা সার্ভিসের পাবলিশ করা পোর্ট আপনার ল্যাপটপ থেকে পৌঁছানো যাবে না; সে কারণে ওভাররাইড db-কে public-এও রাখে:
# file: compose.override.yaml
services:
api:
# fastapi dev reloads on every save; --host 0.0.0.0 is needed inside a container
command: ["fastapi", "dev", "main.py", "--host", "0.0.0.0"]
volumes:
- .:/app
db:
networks: [public, backend]
ports:
- "127.0.0.1:5432:5432"প্রোডাকশন বেছে নিতে হয় স্পষ্টভাবে -f দিয়ে। -f দিলে স্বয়ংক্রিয় ওভাররাইড বন্ধ হয়ে যায়, তাই প্রোডাকশনের কমান্ডে ভুলবশত ডেভেলপমেন্টের সেটিংস ঢোকে না:
# file: compose.prod.yaml
services:
api:
build: !reset null
image: ghcr.io/nadia-rahman/review-api:${APP_VERSION:?set APP_VERSION}
pull_policy: always
ports: !override
- "127.0.0.1:8000:8000"
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
logging:
driver: json-file
options: {max-size: "10m", max-file: "3"}
migrate:
build: !reset null
image: ghcr.io/nadia-rahman/review-api:${APP_VERSION:?set APP_VERSION}মার্জ কীভাবে কাজ করে, সংক্ষেপে: ম্যাপিং কি ধরে ধরে মার্জ হয় এবং একক মানের (command, image) ক্ষেত্রে পরের ফাইল জেতে; ports ও volumes-এর মতো লিস্টে এন্ট্রি বদলানো হয় না, যোগ হয়। এ কারণেই প্রোডাকশন ফাইলে লিস্ট পুরোপুরি বদলাতে !override ট্যাগ, আর build সেকশন সরাতে !reset null, কারণ প্রোডাকশনে চলে CI-র আগে-বানিয়ে-পুশ-করা ইমেজ (পরের পাতায় সেটা করা হয়)। ভেরিয়েবল সেট না থাকলে ${APP_VERSION:?message} ওই বার্তাসহ থেমে যায়, যা চুপচাপ :dev ডিপ্লয় করার চেয়ে অনেক ভালো। ports-এর 127.0.0.1 মানে "শুধু এই মেশিন": সামনে থাকে একটা রিভার্স প্রক্সি বা প্ল্যাটফর্মের রাউটার, API সরাসরি উন্মুক্ত হয় না। deploy.resources.limits কন্টেইনারের CPU ও মেমরির সীমা বেঁধে দেয়, যাতে একটা বেপরোয়া মডেল কল ডেটাবেসকে না খেয়ে ফেলে। দুটো কমান্ড, আর মার্জ করা ফলাফল দেখানোরটা:
docker compose up -d --build
APP_VERSION=1.0.0 docker compose -f compose.yaml -f compose.prod.yaml configপ্রথমটা ডেভেলপমেন্ট ওভাররাইড নিজে থেকে ব্যবহার করে। দ্বিতীয়টা শুধু ছাপে: docker compose config ইন্টারপোলেশন ও মার্জের পর চূড়ান্ত YAML দেখায়, প্রোডাকশনের ইমেজ নাম ও একটামাত্র লুপব্যাক পোর্ট ম্যাপিংসহ; প্রতিটা প্রোডাকশন ডিপ্লয়ের আগে এটা দেখে নিন। দুটো ফাইলই এখনো সাধারণ YAML, তাই অন্তত পার্স হচ্ছে কি না আর প্রতিটা কোন কি ছুঁয়েছে তা দেখতে পারেন। ! ট্যাগগুলো Compose-নির্দিষ্ট, তাই এই স্ক্রিপ্ট PyYAML-কে সেগুলো সাধারণ মান হিসেবে পড়তে বলে:
import yaml
def any_tag(loader, suffix, node):
if isinstance(node, yaml.SequenceNode):
return loader.construct_sequence(node)
return loader.construct_scalar(node)
yaml.add_multi_constructor("!", any_tag, Loader=yaml.SafeLoader)
for filename in ("compose.override.yaml", "compose.prod.yaml"):
for service, body in yaml.safe_load(open(filename))["services"].items():
print(f"{filename:22} {service:8} changes {sorted(body)}")compose.override.yaml api changes ['command', 'volumes']
compose.override.yaml db changes ['networks', 'ports']
compose.prod.yaml api changes ['build', 'deploy', 'image', 'logging', 'ports', 'pull_policy']
compose.prod.yaml migrate changes ['build', 'image']কিছু ভুল হলে
শুরু করুন docker compose ps -a দিয়ে: কোন কন্টেইনার বেরিয়ে গেছে, রিস্টার্ট হচ্ছে বা unhealthy তা দেখায়। তারপর docker compose logs --tail 50 <service> দিয়ে সেই সার্ভিসের লগ পড়ুন। সাধারণ কারণ: migrate ১ দিয়ে বেরিয়েছে (কানেক্ট করতে পারেনি বা SQL ফেল করেছে; লগে কারণ থাকে); API লুপে রিস্টার্ট হচ্ছে (ইমপোর্টের সময় একটা অনুপস্থিত সেটিং এক্সেপশন তুলেছে, লগে দেখা যায়); db unhealthy (চেকে ব্যবহারকারী বা ডেটাবেসের নাম ভুল, অথবা মেজর ভার্সন আপগ্রেডের পর নষ্ট ভলিউম)। ডেভেলপমেন্টে একদম শূন্য থেকে শুরু করতে docker compose down -v চালান, যা কন্টেইনার, নেটওয়ার্ক এবং ভলিউম মুছে দেয়, তারপর আবার up।
বাস্তবে কোথায় কাজে লাগে
- AI সার্ভিসের লোকাল ডেভেলপমেন্ট। একটা কমান্ডে প্রতিটা টিমমেট একই PostgreSQL, Redis (বা Valkey) ও API পায়, সিড করা ডেটাসহ, ল্যাপটপে কিছু ইনস্টল না করেই। ভেক্টর ডেটাবেসও (PostgreSQL-এর ভেতরে pgvector, Qdrant, Chroma) একই ফাইলের আরেকটা সার্ভিস মাত্র।
- CI। পরের পাতার পাইপলাইন ইন্টিগ্রেশন টেস্টের জন্য একই ডেটাবেস চালু করতে পারে, ফলে CI-তে টেস্ট ফেল করলে আপনার মেশিনে তা পুনরুৎপাদন করা যায়।
- ছোট প্রোডাকশন ডিপ্লয়মেন্ট। রিভার্স প্রক্সির পেছনে একটা VPS-এ
docker composeছোট প্রোডাক্টের জন্য যুক্তিসংগত ও সস্তা প্রোডাকশন সেটআপ। একাধিক মেশিন, স্বয়ংক্রিয় ফেইলওভার বা রোলিং ডিপ্লয় লাগলে Compose-এর কাজ একটা প্ল্যাটফর্মে যায় (Kubernetes, Cloud Run, ECS), আর আপনার বানানো ইমেজ একই থাকে।
সাধারণ ভুল
- অন্য সার্ভিসে পৌঁছাতে
localhostব্যবহার। কন্টেইনারের ভেতরে এটা কন্টেইনারটাকেই বোঝায়, তাই কানেকশন প্রত্যাখ্যাত হয়। সার্ভিসের নাম (db,cache) আর কন্টেইনারের নিজের পোর্ট ব্যবহার করুন। - কন্ডিশন ছাড়া সাধারণ
depends_on। এটা শুধু প্রসেস চালু হওয়া পর্যন্ত অপেক্ষা করে, তৈরি হওয়া পর্যন্ত নয়, ফলে API ডেটাবেসের সঙ্গে দৌড়ে হেরে মাঝে মাঝে ক্র্যাশ করে।healthcheckআরcondition: service_healthyযোগ করুন। - অভ্যাসবশত
docker compose down -vচালানো।-vআপনার নামযুক্ত ভলিউম মুছে দেয়, মানে আপনার ডেটাবেস। সাধারণdownডেটা রাখে;-vরাখুন ইচ্ছাকৃত রিসেটের জন্য। environment:-এ বা কমিট করা.env-এ পাসওয়ার্ড।docker inspect, লগ আর Git ইতিহাস দিয়ে ফাঁস হয়। সিক্রেট ব্যবহার করুন,.envওsecrets/.gitignore-এ রাখুন, আর নকল মানসহ একটা উদাহরণ ফাইল দিন।.envকন্টেইনারে পৌঁছাবে, বা বদলানো পাসওয়ার্ড বিদ্যমান ডেটাবেসে পৌঁছাবে ভাবা।.envশুধু YAML-এর${…}ভরে, আর ডেটাবেস ইমেজ পাসওয়ার্ড ভেরিয়েবল পড়ে শুধু ফাঁকা ভলিউম প্রথমবার তৈরির সময়।- ডেটাবেসের পোর্ট সবার জন্য খুলে দেওয়া।
"5432:5432"প্রতিটা নেটওয়ার্ক ইন্টারফেসে শোনে। প্রোডাকশনে ডেটাস্টোর পাবলিশ করবেন না; ডেভেলপমেন্টে127.0.0.1-এ বাঁধুন।
নিজে চেষ্টা করুন
- সহজ: পাইথন ও PyYAML দিয়ে
compose.yaml-এর যে সার্ভিসগুলোরhealthcheckনেই তাদের নাম ছাপুন। - মাঝারি: Redis-সামঞ্জস্যপূর্ণ Valkey (Linux Foundation-এর BSD-লাইসেন্সের ফর্ক) একই পাইথন ক্লায়েন্টে কাজ করে। এমন একটা স্ক্রিপ্ট লিখুন যা
compose.yaml-এর একটা কপিcompose.valkey.yamlনামে বানায়, যাতেvalkey/valkey:9-alpineইমেজ,valkey-serverওvalkey-cliব্যবহার হয়, তারপর কপি থেকে cache সার্ভিসের ইমেজ ও হেলথ চেক ছাপুন। - কঠিন:
lint_compose.pyলিখুন: একটা ফাংশনproblems(cfg), যা জানায় (ক) এমন সার্ভিস যাservice_healthyদিয়ে এমন ডিপেন্ডেন্সির জন্য অপেক্ষা করে যার হেলথ চেক নেই, এবং (খ)apiছাড়া যেকোনো সার্ভিস যা পোর্ট পাবলিশ করে। দেখান যে আপনার ফাইলে এটা কিছুই পায় না, আর একটা ভাঙা কপিতে দুটো সমস্যাই ধরে।
উত্তর
১. ফাইল পড়ে কি (key) ধরে ফিল্টার করুন:
import yaml
services = yaml.safe_load(open("compose.yaml"))["services"]
print([name for name, svc in services.items() if "healthcheck" not in svc])['migrate']২. টেক্সট প্রতিস্থাপনে ফাইলের বাকিটা অক্ষত থাকে; ভলিউমের নাম থাকে, REDIS_URL-ও থাকে, কারণ Valkey Redis প্রোটোকলে কথা বলে। তারপর কপি পার্স করে যাচাই করুন:
import yaml
text = open("compose.yaml").read()
text = text.replace("redis:8-alpine", "valkey/valkey:9-alpine")
text = text.replace("redis-server", "valkey-server").replace("redis-cli", "valkey-cli")
open("compose.valkey.yaml", "w").write(text)
cache = yaml.safe_load(text)["services"]["cache"]
print(cache["image"], cache["healthcheck"]["test"])valkey/valkey:9-alpine ['CMD', 'valkey-cli', 'ping']৩. লিন্টার পার্স করা ডিকশনারির ওপর একটা সাধারণ ফাংশন, তাই এটা CI-তে মার্জের আগের পরীক্ষা হিসেবে চালানো যায় (এবং যেকোনো কোডের মতো টেস্ট করা যায়):
# file: lint_compose.py
import copy
import yaml
def problems(cfg: dict) -> list[str]:
services = cfg["services"]
found = []
for name, svc in services.items():
for dep, rule in svc.get("depends_on", {}).items():
if rule["condition"] == "service_healthy" and "healthcheck" not in services[dep]:
found.append(f"{name} waits for {dep} to be healthy, but {dep} has no healthcheck")
if name != "api" and svc.get("ports"):
found.append(f"{name} publishes ports")
return found
if __name__ == "__main__":
cfg = yaml.safe_load(open("compose.yaml"))
print(problems(cfg))
broken = copy.deepcopy(cfg)
del broken["services"]["db"]["healthcheck"]
broken["services"]["cache"]["ports"] = ["6379:6379"]
for line in problems(broken):
print(line)python lint_compose.py[]
api waits for db to be healthy, but db has no healthcheck
migrate waits for db to be healthy, but db has no healthcheck
cache publishes ports
সবশেষে, পরীক্ষা-নিরীক্ষা শেষে স্ট্যাক পরিষ্কার করুন। কন্টেইনার ও নেটওয়ার্ক যাবে; ডেটাও মুছতে চাইলে তবেই -v যোগ করুন:
docker compose downসারসংক্ষেপ
- একটা
compose.yamlসার্ভিস, নেটওয়ার্ক, ভলিউম ও সিক্রেট বর্ণনা করে;docker compose up -dপুরো স্ট্যাক বিল্ড করে চালু করে,downসেটা সরিয়ে দেয় (আরdown -vডেটাও মোছে)। - সার্ভিসগুলো একই নেটওয়ার্কে সার্ভিসের নামে একে অপরকে পায় (
db:5432,cache:6379);localhostমানে কন্টেইনার নিজে। শুধু API পাবলিশ করুন, আর ডেটাস্টোর রাখুন ইন্টারনাল নেটওয়ার্কে। - প্রতিটা ডিপেন্ডেন্সিতে
healthcheckদিন এবংservice_healthy-সহdepends_onব্যবহার করুন; মাইগ্রেশন চালানservice_completed_successfully-সহ একবার-চলা সার্ভিস হিসেবে। লাইভনেস (/health) আর রেডিনেস (/ready) আলাদা রাখুন। .envYAML-এর${…}ভরে,env_fileকন্টেইনারের ভেতরের ভেরিয়েবল সেট করে, আর Compose সিক্রেট ফাইল হিসেবে/run/secrets/<name>-এ আসে, যা পাইথন থেকে বা_FILEভেরিয়েবল দিয়ে পড়া হয়।- একটা বেস ফাইল রাখুন, ডেভেলপমেন্টের জন্য
compose.override.yamlযোগ করুন আর প্রোডাকশনের জন্য-f compose.prod.yaml; ফলাফলdocker compose configদিয়ে যাচাই করুন।
এরপর: GitHub Actions দিয়ে CI/CD প্রতিটা পুশে আপনার টেস্ট চালায়, সেগুলোর জন্য একটা PostgreSQL সার্ভিস চালু করে, এবং ওপরের প্রোডাকশন ফাইল যে ইমেজ পুল করে সেটা বিল্ড করে পাবলিশ করে।