SSD Nodes Learn 🎉 VPS از $5.50/ماه
راهنماها Matt Connorتوسط Matt Connor

تفاوت build و image در Docker Compose روی VPS

اگر تغییرات Dockerfile شما در VPS اعمال نمی‌شود به دلیل استفاده از کلید image است. در این مطلب تفاوت این دو دستور و نحوه رفع مشکل عدم اعمال تغییرات در Docker Compose را بررسی می‌کنیم.

تفاوت Docker Compose build و image: پاسخ کوتاه

در یک فایل Docker Compose، عبارت image: نام تصویری (image) را مشخص می‌کند که باید از یک رجیستری دریافت (pull) شود و build: به Compose دستور می‌دهد که یک تصویر را روی همین ماشین از روی یک Dockerfile بسازد. اگر فقط image: را تنظیم کنید، Compose آن تگ را دریافت کرده و اجرا می‌کند. اگر فقط build: را تنظیم کنید، Compose تصویر را در همین‌جا می‌سازد و نامی بر اساس نام پروژه و نام سرویس به آن اختصاص می‌دهد. اگر هر دو را تنظیم کنید، Compose تصویر را به‌صورت محلی می‌سازد و سپس نتیجه را با نامی که در image: تعیین کرده‌اید تگ‌گذاری می‌کند؛ این روشی است که با آن می‌توانید یک تصویر بسازید و آن را با نام دلخواه خود push کنید.

این تمام تفاوت است. هر آنچه در ادامه می‌آید، به معنای عملیاتی آن روی سرور مربوط می‌شود. این متن فرض را بر این می‌گذارد که Docker Engine و افزونه Compose از قبل نصب شده‌اند؛ اجرای Docker روی VPS این بخش را پوشش می‌دهد.

سه شکل کامل

یک تگ منتشرشده را pull کرده و اجرا کنید. در هیچ مرحله‌ای از Dockerfile استفاده نمی‌شود.

services:
  web:
    image: nginx:1.27
    restart: unless-stopped
    ports:
      - "80:80"

از یک Dockerfile در دایرکتوری جاری build بگیرید. هیچ چیزی به جز image پایه که در FROM نام برده شده، pull نمی‌شود.

services:
  web:
    build: .
    restart: unless-stopped
    ports:
      - "80:80"

به‌صورت محلی build بگیرید و نتیجه را tag کنید. docker compose push سپس می‌تواند همان تگ دقیق را به یک registry ارسال کند.

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    image: registry.example.com/acme/web:1.4.2
    restart: unless-stopped
    ports:
      - "80:80"

context دایرکتوری است که به builder ارسال می‌شود. dockerfile نسبت به آن context حل می‌شود، بنابراین context: . با dockerfile: docker/prod.Dockerfile عادی و صحیح است. دستور docker compose images را اجرا کنید تا نام image و image ID پشت هر container سرویس را ببینید؛ این سریع‌ترین راه برای تأیید این است که کدام‌یک از این سه شکل را واقعاً نوشته‌اید.

چرا پس از تغییر Dockerfile، دستور docker compose up عملیات rebuild را انجام نمی‌دهد؟

زیرا up بررسی می‌کند که آیا image وجود دارد یا خیر، نه اینکه آیا به‌روز است یا نه.

هنگامی که Compose سرویسی را که دارای بخش build: است اجرا می‌کند، به دنبال image در مخزن محلی می‌گردد. اگر image با آن نام از قبل وجود داشته باشد، Compose از همان استفاده می‌کند. این ابزار Dockerfile شما را نمی‌خواند، فایل‌های منبع را مقایسه نمی‌کند و به هیچ timestamp توجهی ندارد. مشخصات Compose این قاعده را به عنوان ویژگی pull_policy تعریف کرده است و رفتار پیش‌فرض آن است که image را فقط در صورت نبودن آن می‌سازد. وجود داشتن image به منزله کافی بودن آن تلقی می‌شود.

بنابراین شما app.py را ویرایش می‌کنید، docker compose up -d را اجرا می‌کنید، می‌بینید که Compose وضعیت container را running گزارش می‌دهد و کد قدیمی را ارائه می‌کند. هیچ خطایی رخ نداده است، بنابراین هشداری هم دریافت نمی‌کنید. این رایج‌ترین گزارش «اعمال نشدن تغییرات» در Compose است. نشانه اصلی، کلمه‌ای است که Compose در کنار نام container چاپ می‌کند: containerای که Compose جایگزین کرده است به صورت recreated یا started گزارش می‌شود، اما containerای که Compose تصمیم گرفته به آن دست نزند، به صورت running گزارش می‌شود.

دو بررسی این موضوع را مشخص می‌کند. docker compose images شناسه image مورد استفاده هر container را چاپ می‌کند، پس آن را قبل از deploy یادداشت کرده و بعد از آن مقایسه کنید. docker image ls دارای ستونی به نام CREATED است؛ imageای که پیش از آخرین commit شما ایجاد شده باشد، یک image قدیمی (stale) است، صرف‌نظر از اینکه اسکریپت deploy چه چیزی را چاپ کرده باشد.

کدام فلگ‌ها بازسازی (rebuild) را اجباری می‌کنند

  • فلگ docker compose up -d --build ابتدا عملیات build را انجام می‌دهد و سپس هر کانتینری که ایمیج آن تغییر کرده باشد را بازسازی (recreate) می‌کند. این همان فلگی است که اکثر کاربران به دنبال آن هستند.
  • فلگ docker compose build web فقط یک سرویس را build می‌کند و هیچ چیزی را اجرا نمی‌کند. پس از آن از docker compose up --no-deps -d web استفاده کنید تا فقط همان کانتینر جایگزین شود و بقیه stack در حال اجرا باقی بماند.
  • فلگ docker compose build --no-cache web تمام لایه‌های کش‌شده را دور می‌ریزد و ساخت را از اولین دستور آغاز می‌کند.
  • فلگ docker compose build --pull تلاش می‌کند نسخه جدیدتری از base image را در FROM دریافت (pull) کند؛ بنابراین تگ‌های متغیر مانند node:22 محتوای فعلی خود را دریافت می‌کنند، نه نسخه‌ای که در ماه مارس دانلود کرده بودید.
  • فلگ docker compose up -d --force-recreate کانتینرها را از همان ایمیجی که در حال حاضر استفاده می‌کنند، بازسازی می‌کند. این فلگ هرگز عملیات build را انجام نمی‌دهد. استفاده از این فلگ زمانی که منظور شما --build بوده، یک اشتباه رایج است.

شما همچنین می‌توانید این تصمیم را به داخل فایل منتقل کنید. طبق مشخصات Compose، مقدار pull_policy: build به این معناست که Compose ایمیج را build می‌کند و اگر از قبل موجود باشد، آن را بازسازی می‌کند. در این حالت، هر up هزینه یک build را به همراه دارد؛ این رفتار برای لپ‌تاپ مناسب است اما در سرور به‌ندرت کاربرد دارد.

services:
  web:
    build: .
    image: registry.example.com/acme/web:dev
    pull_policy: build

یک تعامل دیگر نیز وجود دارد که دانستن آن مفید است. فلگ docker compose pull تلاش می‌کند برای سرویس‌هایی که بخش build دارند نیز ایمیج‌ها را pull کند، و اگر آن pull با شکست مواجه شود، به شما اعلام می‌کند که ایمیج باید build شود. برای نادیده گرفتن بی‌سروصدای آن سرویس‌ها، از --ignore-buildable استفاده کنید.

چگونه کش ساخت (build cache) زمان استقرار شما را تعیین می‌کند

هر دستور در Dockerfile یک لایه ایجاد می‌کند و سیستم ساخت (builder) زمانی که آن دستور و ورودی‌هایش تغییر نکرده باشند، از لایه کش‌شده استفاده مجدد می‌کند. برای COPY، ورودی‌ها محتوای فایل‌هایی هستند که کپی می‌شوند. به محض اینکه یک لایه در کش پیدا نشود (cache miss)، تمام لایه‌های بعدی دوباره ساخته می‌شوند، زیرا هر لایه بر اساس سیستم فایلی ساخته شده که لایه قبلی تولید کرده است.

همین یک قانون تعیین می‌کند که استقرار شما چند ثانیه طول بکشد یا چند دقیقه. Dockerfile را از مواردی که به‌ندرت تغییر می‌کنند تا مواردی که در هر commit تغییر می‌یابند، مرتب کنید.

FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]

npm ci بالاتر از COPY . . قرار دارد، بنابراین ویرایش یک فایل منبع باعث می‌شود لایه نصب (install layer) در کش باقی بماند و عملیات ساخت از مرحله کپی ادامه یابد. اگر این دو خط را جابه‌جا کنید، یک تغییر یک‌کاراکتری باعث نصب مجدد تمام وابستگی‌ها می‌شود، زیرا COPY . . لایه‌ای را که npm ci بر روی آن ساخته شده است، نامعتبر می‌کند. همین الگو برای pip install -r requirements.txt و go mod download نیز صدق می‌کند.

--no-cache ابزار مناسبی است زمانی که شک دارید یک لایه قدیمی (stale layer) مانع اعمال اصلاحات شما شده است. این یک پیش‌فرض نامناسب است، زیرا باعث هدر رفتن قابلیت استفاده مجدد می‌شود که هدف اصلی مرتب‌سازی Dockerfile است.

یک مورد که image تعیین می‌کند و Compose می‌تواند آن را بازنویسی (override) کند: CMD در Dockerfile همان چیزی است که image به‌صورت پیش‌فرض اجرا می‌کند و کلید command: در سرویس، جایگزین آن می‌شود. نحوه تعامل command و entrypoint در اینجا اهمیت دارد، زیرا یک override در Compose می‌تواند باعث شود یک image که به‌تازگی ساخته شده، دقیقاً مشابه نسخه قدیمی رفتار کند.

محیط ساخت (Build context) و .dockerignore

context: . به این معناست که Compose آن دایرکتوری را بسته‌بندی کرده و پیش از اجرای اولین دستور، به سازنده (builder) ارسال می‌کند. همه چیز در آن مسیر، از جمله .git و هر دایرکتوری داده‌ای که در کنار سورس‌کد خود نگه می‌دارید، منتقل می‌شود. اگر فرآیند ساخت در مرحله transferring-context برای پروژه‌ای که تغییری نکرده متوقف می‌شود، به این معناست که حجم محیط ساخت بیش از حد بزرگ است.

یک فایل .dockerignore در ریشه محیط ساخت، مسیرها را از این انتقال مستثنی می‌کند. سینتکس آن مشابه .gitignore است.

.git
node_modules
*.log
data/
.env

این کار دو مزیت دارد. حجم انتقال کاهش می‌یابد و در نتیجه هر فرآیند ساخت سریع‌تر آغاز می‌شود. همچنین COPY . . دیگر نمی‌تواند .env را به داخل ایمیج کپی کند؛ جایی که هر کسی با دریافت (pull) آن ایمیج، قادر به خواندن محتوای آن خواهد بود.

مورد رایج کند شدن فرآیند ساخت در طول زمان، استفاده از bind mount است. یک named volume خارج از دایرکتوری پروژه قرار دارد، اما یک bind mount مانند ./data:/var/lib/postgresql/data درون محیط ساخت قرار می‌گیرد؛ بنابراین با رشد دیتابیس، فرآیندهای ساخت شما هر هفته کندتر می‌شوند. یک خط در .dockerignore این مشکل را حل می‌کند. مقایسه Bind mounts با named volumes به بررسی دقیق‌تر این تفاوت‌ها می‌پردازد.

آرگومان‌های ساخت (Build arguments) نیز نسخه کوچک‌تری از همین ریسک را به همراه دارند. مقادیر ارسال‌شده از طریق args: در تاریخچه ایمیج برای هر کسی که ایمیج را در اختیار داشته باشد قابل مشاهده است؛ بنابراین فقط شماره نسخه را در آن قرار دهید و هرگز توکن‌های امنیتی را وارد نکنید. فایل‌های Env و اسرار در Compose توضیح می‌دهد که اعتبارنامه‌ها (credentials) باید در کجا قرار بگیرند.

آیا باید روی VPS بیلد (Build) کنید یا در جای دیگری بیلد کرده و ایمیج را دریافت کنید؟

بیلد کردن روی سروری که ترافیک شما را سرویس‌دهی می‌کند، به دلیل کوتاه‌ترین مسیر، حالت پیش‌فرض است: git pull، سپس docker compose up -d --build. این روش برای یک سرور کوچک که هنوز کسی به آن وابسته نیست، مناسب است. اما به دو دلیل قابل اندازه‌گیری و یک دلیل که فقط در شرایط بحرانی خود را نشان می‌دهد، دیگر مناسب نخواهد بود.

حافظه (Memory). فرآیند بیلد، کامپایلرها و باندلرها (Bundlers) را در کنار اپلیکیشن در حال اجرای شما اجرا می‌کند و این‌ها در اکثر پشته‌های نرم‌افزاری، پرمصرف‌ترین بخش از نظر حافظه هستند. روی یک VPS با 1 GB رم، یک باندلر JavaScript یا کامپایل Rust معمولاً بزرگ‌ترین پردازش روی سرور است. وقتی حافظه هسته (Kernel) تمام می‌شود، بزرگ‌ترین پردازش را می‌کشد: یا بیلد با Killed و وضعیت خروج 137 متوقف می‌شود، یا دیتابیس شما کشته شده و سایت در میانه عملیات استقرار (Deploy) از دسترس خارج می‌شود. دستور dmesg -T | grep -i oom خط مربوط به کشتن پردازش را به همراه نام آن چاپ می‌کند، بنابراین می‌توانید به جای حدس زدن، بفهمید کدام‌یک از این دو اتفاق افتاده است.

دیسک (Disk). هر بیلد لایه‌هایی از خود به جا می‌گذارد و ابزار بیلد، کش (Cache) مخصوص خود را جدا از ایمیج‌های شما نگه می‌دارد. دستور docker system df هر دو را نشان می‌دهد و ردیف کش بیلد فقط بزرگ‌تر می‌شود. برای پاکسازی ایمیج‌های معلق از docker image prune و برای لایه‌های کش شده از docker builder prune استفاده کنید. پر شدن دیسک فقط بیلد را متوقف نمی‌کند؛ دیتابیس نیز از نوشتن باز می‌ماند و هزینهٔ این خرابی بسیار بیشتر از یک استقرار کند است.

تکرارپذیری (Reproducibility). ایمیجی که روی سرور بیلد شده، فقط روی همان سرور وجود دارد. بازگشت به نسخه قبل (Rollback) به معنای چک‌اوت کردن کامیت قدیمی و بیلد مجدد است، و تضمینی وجود ندارد که آن بیلد دقیقاً همان خروجی قبلی را تولید کند، زیرا تگ پایه (Base tag) تغییر کرده و میرورهای پکیج نیز همراه با آن به‌روز شده‌اند. بیلد کردن در جای دیگر و Push کردن یک تگ، بازگشت به نسخه قبل را به یک ویرایش ساده تبدیل می‌کند: کافی است image: را روی تگ قبلی تنظیم کرده و docker compose up -d را اجرا کنید.

ترتیبی که پایداری را تضمین می‌کند ساده است. سیستم Continuous Integration شما بیلد را انجام داده و registry.example.com/acme/web:<git-sha> را Push می‌کند، و فایل Compose روی VPS شامل image: است و اصلاً کلید build: را ندارد. در این حالت، استقرار تنها شامل دو دستور است که تقریباً هیچ حافظه‌ای مصرف نمی‌کنند.

docker compose pull
docker compose up -d

دستور docker login registry.example.com را یک‌بار روی سرور اجرا کنید تا Compose بتواند از آن پس تگ‌های خصوصی را Pull کند.

بخش بیلد را برای توسعه نگه دارید و آن را حذف نکنید، اما آن را در فایلی با نام دلخواه خود قرار دهید.

# compose.dev.yaml
services:
  web:
    build:
      context: .
    pull_policy: build
docker compose -f compose.yaml -f compose.dev.yaml up -d --build

نام آن فایل را compose.dev.yaml بگذارید و نه compose.override.yaml. ابزار Compose هرگاه فایل override موجود باشد، آن را به‌طور خودکار بارگذاری می‌کند؛ بنابراین اگر یک فایل override سرگردان به سرور کپی شود، ممکن است به‌طور ناخواسته بیلد کردن روی سرور را دوباره فعال کند. لایه بندی چندین فایل Compose توضیح می‌دهد که چگونه ادغام (Merge) هر کلید را حل‌وفصل می‌کند.

تله معماری هنگام ساخت در محیط‌های دیگر

هر image حامل معماری CPU است که برای آن ساخته شده است. اگر روی یک لپ‌تاپ با تراشه Apple Silicon عملیات build را انجام دهید، آن را push کنید و سپس همان tag را روی یک VPS با معماری x86_64 pull کنید، Docker هشدار می‌دهد که پلتفرم image درخواستی با پلتفرم میزبان شناسایی‌شده مطابقت ندارد. در این حالت، فرآیند با خطای exec format error متوقف می‌شود که ممکن است شبیه به یک فایل باینری خراب به نظر برسد، در حالی که چنین نیست. برای هدف مورد نظر، build را به‌صورت صریح انجام دهید:

docker buildx build --platform linux/amd64 \
  -t registry.example.com/acme/web:1.4.2 --push .

همین عدم تطابق در حالت معکوس نیز رخ می‌دهد، اگر لپ‌تاپ شما x86 باشد و بخواهید یک VPS با معماری ARM به جای x86 را اجرا کنید. سپردن عملیات build در CI به همان معماری که قصد deploy روی آن را دارید، این مشکل را به‌طور کامل برطرف می‌کند.

بررسی‌های پس از استقرار

  • docker compose images تصویر و تگ مربوط به هر کانتینر در حال اجرا را چاپ می‌کند. تغییر ID تصویر، گواه شما بر این است که بیلد جدید در حال سرویس‌دهی است.
  • docker compose config فایل ادغام‌شده را پس از جایگزینی متغیرها چاپ می‌کند تا بتوانید نام نهایی تصویری که Compose استفاده خواهد کرد را پیش از اجرای هر دستوری مشاهده کنید.
  • docker compose logs -f web برای نیم دقیقه اول پس از جایگزینی. کانتینری که شروع می‌شود و بلافاصله خارج می‌گردد، به‌جای فعال ماندن در یک حلقه تکرار قرار می‌گیرد؛ این حلقه تا زمانی که آن را بررسی نکنید، بی‌صدا باقی می‌ماند.
  • docker image ls ستون CREATED را نمایش می‌دهد. تصویری که قدیمی‌تر از آخرین commit شما باشد، هرگز دوباره بیلد نشده است.

اگر هنوز در حال آماده‌سازی فایلی هستید که این بررسی‌ها روی آن اجرا می‌شوند، اصول فایل Compose روی یک VPS کلیدهای پیرامونی را پوشش می‌دهد و برگه تقلب دستورات Compose سایر زیردستورات را فهرست کرده است.

FAQ

آیا می‌توانم از build و image در یک سرویس استفاده کنم؟

بله، این پیکربندی استاندارد برای پروژه‌هایی است که خودتان می‌سازید. Compose بر اساس بخش build: عملیات ساخت را انجام داده و نتیجه را با مقدار image: برچسب‌گذاری (tag) می‌کند. این همان برچسبی است که docker compose push به یک رجیستری ارسال می‌کند و ماشین‌های دیگر آن را دریافت (pull) می‌کنند. بدون کلید image:، ابزار Compose همچنان عملیات ساخت را انجام می‌دهد، اما نام تصویر را بر اساس نام پروژه و سرویس انتخاب می‌کند و هشدار می‌دهد که نبود این ویژگی، مانع از ارسال (push) تصویر به رجیستری می‌شود.

چرا docker compose up تغییرات Dockerfile مرا اعمال نمی‌کند؟

زیرا up فقط بررسی می‌کند که آیا تصویری با آن نام وجود دارد یا خیر. وقتی تصویری موجود باشد، Compose آن را اجرا می‌کند و هرگز آن را با Dockerfile یا فایل‌های منبع شما مقایسه نمی‌کند. دستور docker compose up -d --build را اجرا کنید، یا از docker compose build web به همراه docker compose up --no-deps -d web برای جایگزینی یک سرویس خاص استفاده کنید. تنظیم pull_policy: build روی سرویس باعث می‌شود که هر بار اجرای up منجر به ساخت مجدد (rebuild) شود که برای ماشین‌های توسعه مناسب است.

تفاوت بین --build و --force-recreate چیست؟

--build تصویر را دوباره می‌سازد و سپس کانتینرهایی که تصویرشان تغییر کرده است را بازسازی می‌کند. --force-recreate کانتینرها را از همان تصویری که دارند بازسازی می‌کند، بنابراین هرگز تغییرات کد را اعمال نمی‌کند. اگر تغییرات شما در فایل‌های منبع یا Dockerfile است، --build پرچمی است که به آن نیاز دارید. --force-recreate برای بازنشانی خود کانتینر است؛ برای مثال جهت پاک‌سازی لایه قابل‌نوشتن (writable layer) در حالی که همان تصویر حفظ می‌شود.

آیا باید تصاویر Docker را روی VPS بسازم یا جای دیگری؟

بهتر است در جای دیگری بسازید و پس از آماده شدن، تصویر را دریافت (pull) کنید تا سرور فقط به ترافیک سرویس‌دهی کند. عملیات ساخت (build) برای حافظه با برنامه شما رقابت می‌کند و در یک VPS کوچک، این رقابت باعث می‌شود هسته سیستم‌عامل (kernel) بزرگ‌ترین پردازش را متوقف کند که ممکن است همان عملیات ساخت یا دیتابیس شما باشد. همچنین عملیات ساخت، کش (cache) روی دیسک باقی می‌گذارد که هیچ‌کس آن را برای شما پاک نمی‌کند. ساخت روی سرور برای پروژه‌های کوچک بدون کاربر مشکلی ندارد و اگر بخش build: را در یک فایل Compose مخصوص توسعه نگه دارید، انتقال آن در آینده هزینه کمی خواهد داشت.

چگونه از پر شدن دیسک توسط کش ساخت Docker جلوگیری کنم؟

دستور docker system df را اجرا کنید تا ببینید هر یک از تصاویر و کش ساخت چقدر فضا اشغال کرده‌اند. docker builder prune لایه‌های کش شده را حذف می‌کند و docker image prune تصاویر معلق (dangling) باقی‌مانده از ساخت‌های قبلی را پاک می‌کند. افزودن -a به هر یک از این دستورات، تهاجمی‌تر عمل کرده و باعث می‌شود ساخت بعدی شما از صفر (cold) شروع شود. دستور docker system prune -af --volumes را روی سرور زمان‌بندی نکنید، زیرا --volumes هر Volume که در حال حاضر توسط هیچ کانتینری استفاده نمی‌شود را حذف می‌کند؛ در حالی که استکی که برای نگهداری متوقف کرده‌اید، دیتابیس خود را دقیقاً در چنین Volumeای نگه می‌دارد.