کدنامهمرجع‌های مهندسی نرم‌افزار، به فارسی
Docker · فصل ۰۶Docker · Chapter 6

داده: volume، bind mount و پشتیبان‌گیری

Data: volumes, bind mounts, and backups

کانتینر را می‌توان دوباره ساخت؛ داده‌ای که داخلش جا مانده، همیشه قابل ساختن دوباره نیست. این فصل نشان می‌دهد فایل دقیقاً کجا می‌نشیند و چه چیزی آن را نگه می‌دارد.

A container can be rebuilt. The data left inside it cannot always be recreated. This chapter makes the storage location visible and shows what actually keeps data alive.

۱۲۰دقیقهٔ مطالعه و اجراminutes
۱۸تمرین با پاسخsolved exercises
۴نمودار مفهومیdiagrams
۱آزمایشگاه کاملfull lab

اول یک فایل بساز؛ بعد کانتینر را دور بیندازCreate one file, then throw the container away

فرض کن برنامه‌ات یک فایل گزارش ساخته یا کاربر عکسی آپلود کرده. container را stop می‌کنی، دوباره start می‌کنی و فایل هنوز سر جایش است. خیالت راحت می‌شود. چند روز بعد نسخهٔ جدید برنامه را می‌آوری، container قدیمی را حذف می‌کنی و یکی تازه می‌سازی... فایل ناپدید شده. اینجا تازه معلوم می‌شود «با stop از بین نرفت» اصلاً به معنی «داده ماندگار است» نبوده.

Imagine creating a job report, an uploaded file, or a temporary record inside a container. Stop and start that same container and everything appears fine. The surprise comes when you remove the old container for a new version and create another one from the same image.

هر container یک لایهٔ قابل‌نوشتن مخصوص خودش دارد. هر تغییری که داخل فایل‌سیستم همان container بدهی، آنجا می‌نشیند. تا وقتی خود container را نگه داشته‌ای، آن لایه هم هست؛ اما وقتی container را حذف می‌کنی، خانهٔ آن تغییرها را هم دور انداخته‌ای. بیایید این را با یک آزمایش درست ببینیم، نه با تعریف حفظی.

The reason is simple: an image is only a starting template, and every container has its own writable layer. Filesystem changes go there, but that layer belongs to that container's lifetime. Let us see it directly.

Experiment A · writable layer
$ docker run -d --name data-story alpine sh -c 'sleep infinity'
...

$ docker exec data-story sh -c 'echo "draft-1" > /tmp/report.txt'
$ docker exec data-story cat /tmp/report.txt
draft-1

$ docker stop data-story
data-story

$ docker start data-story
data-story

$ docker exec data-story cat /tmp/report.txt
draft-1

$ docker rm -f data-story
data-story

$ docker run --rm alpine sh -c 'test -e /tmp/report.txt && cat /tmp/report.txt || echo "file is gone"'
file is gone

حالا آزمایش تمیز است: فایل را یک بار ساختیم، بعد همان container را stop/start کردیم و دیدیم فایل باقی ماند. وقتی خود container را حذف کردیم و یکی تازه از همان image ساختیم، فایل دیگر نبود. پس ماندگاریِ قبلی از image نیامده بود؛ از همان لایهٔ قابل‌نوشتنِ container قبلی آمده بود.

The important distinction is this: stop stops the process while the writable layer still belongs to the container; rm removes the container and that layer. The last command created a new alpine container, not a continuation of the old one.

قاعدهٔ این فصلThis chapter's rule

اگر داده بعد از حذف و ساخت دوبارهٔ کانتینر باید بماند، آن را در writable layer رها نکن. محل ماندن داده را آگاهانه با volume یا bind mount انتخاب کن.

If data must survive container removal and recreation, do not leave it in the writable layer. Choose its durable home deliberately with a volume or a bind mount.

writable layer دقیقاً چه عمری دارد؟What is the lifetime of the writable layer?

فصل اول یک سرنخ به ما داده بود: image لایه‌های خواندنی دارد و container هنگام اجرا یک لایهٔ قابل‌نوشتن روی آن‌ها می‌گذارد. این لایه برای فایل موقت و تغییرات کوتاه‌مدت کاملاً طبیعی است. دردسر از جایی شروع می‌شود که چیزی مثل پایگاه‌داده، upload یا فایل مهم را هم همان‌جا بگذاریم و انتظار داشته باشیم بعد از حذف container زنده بماند.

In Chapter 1 we saw that an image is made of read-only layers and a running container adds a read-write layer on top. When Docker changes a file, the changed version lives in that layer; the original image is untouched. This is useful for cache, temporary files, and short experiments, not for data that should live independently of a container.

A writable layer survives stop and start but disappears when the container is removed imageread-only base containerwritable layer stop / startsame file remains same containersame layer rm → layer destroyed new container → empty start fromsame lifecyclestill attached destroyrecreate

فلش‌های ردیف بالا ادامهٔ همان کانتینر را نشان می‌دهند: stop/start فایل را نگه می‌دارد. فلش‌های پایین دو رویداد جدا را نشان می‌دهند: rm layer را از بین می‌برد و کانتینر تازه از image دوباره شروع می‌شود.

The top arrows show one container continuing: stop/start keeps its file. The lower arrows show two separate events: rm destroys the layer, and a new container starts again from the image.

رویدادچه چیزی می‌ماند؟نتیجه برای فایل داخل writable layer
docker stopهمان container و layerفایل می‌ماند
docker startهمان container و layerفایل دوباره دیده می‌شود
docker rmcontainer حذف می‌شودفایل هم دیگر محل مستقلی ندارد
ساخت container تازهimage و layer تازهفایل قبلی وجود ندارد

The short version is: the writable layer is attached to a container identity, not to the image name. Reusing the image does not bring back the old layer.

named volume: خانه‌ای که Docker جدا نگه می‌داردNamed volumes: a home Docker manages separately

اینجاست که named volume وارد می‌شود. به‌جای اینکه داده را داخل عمر یک container زندانی کنیم، یک فضای جدا با اسم خودش می‌سازیم؛ مثلاً demo-data. container فقط این فضا را در یک مسیر می‌بیند. خود volume می‌تواند بماند، حتی اگر container امروز حذف و فردا از نو ساخته شود.

یک تشبیه سادهA simple analogy

container را مثل اتاق هتل ببین و volume را مثل صندوق امانات بیرون اتاق. اگر از اتاق بیرون بروی و اتاق را تحویل بدهی، وسایلی که داخل اتاق جا گذاشته‌ای می‌روند؛ اما چیزی که در صندوق امانات گذاشته‌ای به شمارهٔ اتاق وابسته نیست و می‌توانی از اتاق بعدی دوباره به آن دسترسی بگیری.

Think of the container as a hotel room and the volume as a safe outside the room. If the room is discarded, things left inside it go away, while the safe has its own identity and can be attached to the next room.

For data generated or consumed by containers, a named volume is usually the natural choice. A volume has its own name, such as demo-data, and its lifetime is separate from a container. The container sees it at a path; Docker manages where it is stored.

A container writes through a mounted data path into a named volume that another container can reuse container A/data/report.txtread + write named volumedemo-dataDocker-managedoutside container layer container B/data/report.txtsame bytes mount at /datareuse same volume

فلش اول می‌گوید مسیر /data داخل container به volume وصل شده و نوشتن از آن مسیر در demo-data ذخیره می‌شود. فلش دوم نشان می‌دهد container دیگری می‌تواند همان volume را دوباره mount کند.

The first arrow says that the container path /data is connected to the volume, so writes there land in demo-data. The second arrow shows another container mounting the same volume and seeing the same bytes.

در نحوهٔ نوشتن پیشنهادی Docker، اول --mount را بخوان: نوع mount، منبع و مقصد را با نام صریح می‌نویسی. این شکل برای آموزش و پیدا کردن اشتباه‌ها خواناتر است.

Read Docker's preferred syntax first: --mount states the mount type, source, and destination explicitly. It is easier to teach and easier to debug.

create · attach · inspect
$ docker volume create demo-data
demo-data

$ docker run --rm --mount type=volume,src=demo-data,dst=/data alpine sh -c 'echo "kept" > /data/note.txt; cat /data/note.txt'
kept

$ docker run --rm --mount type=volume,src=demo-data,dst=/data alpine cat /data/note.txt
kept

$ docker volume ls
DRIVER    VOLUME NAME
local     demo-data

$ docker volume inspect demo-data
[
  {
    "Name": "demo-data",
    "Driver": "local",
    "Mountpoint": ".../volumes/demo-data/_data"
  }
]

در docker volume inspect مسیر Mountpoint کمک می‌کند بفهمی Docker جایی خارج از لایهٔ container این داده را نگه می‌دارد. اما برنامه را به آن مسیر داخلیِ host گره نزن؛ برنامه باید همان مسیری را ببیند که داخل container mount کرده‌ای، مثل /data.

The Mountpoint in inspect helps explain Docker's model, but do not make your application depend on that internal host path. Read and write through the mounted path, /data. The exact path differs across Docker Desktop, Linux, and a remote daemon.

دو شکل نوشتن برای یک mountTwo syntaxes, one mount

اول --mount را یاد بگیر چون مبدأ، مقصد و نوع mount را خوانا و جدا می‌نویسد. بعد شکل کوتاه -v را بشناس؛ چون در مثال‌ها و پروژه‌های زیادی می‌بینیش. مفهوم یکی است، فقط شکل نوشتن فرق دارد.

Once the explicit form is clear, the shorter -v form is still worth recognizing because it appears in compose files, scripts, and older answers. Both commands below mount the same named volume at /data.

مدلsyntax پیشنهادیshorthandمعنی source
named volume--mount type=volume,src=demo-data,dst=/data-v demo-data:/dataنام volume در Docker
bind mount--mount type=bind,src=/host/site,dst=/site-v /host/site:/siteمسیر واقعی روی host
read-only...,readonly:roکانتینر اجازهٔ نوشتن ندارد

یک تفاوت را از همین‌جا محکم نگه دار: در volume، مبدأ یک نام Docker است؛ در bind mount، مبدأ یک مسیر واقعی روی host. پس demo-data:/data به معنی «پوشهٔ demo-داده کنار پروژه» نیست؛ این اسم یک volume است.

For a volume, Docker can create the named volume. For a bind mount, the source is a host path. That difference causes a common mistake: demo-data:/data means a volume, not a folder called demo-data beside your project.

mount روی مسیر موجود، فایل‌های زیرش را پنهان می‌کندA mount hides what was already under its target

mount را مثل «کپی‌کردن فایل داخل پوشه» تصور نکن. بیشتر شبیه این است که یک کشو را دقیقاً جلوی یک قفسه بگذاری: تا وقتی کشو آنجاست، قفسهٔ پشتش را نمی‌بینی. فایل‌های image پاک نشده‌اند؛ فقط مسیر mount شده روی آن‌ها قرار گرفته است.

Mounting is not the same as copying a few files into a directory. Another filesystem is placed over the target, so the old contents are hidden until the mount is gone. We use volume-nocopy for an unambiguous volume demonstration because Docker can otherwise copy existing target contents into a new empty volume on first use.

Experiment B · hidden target
$ docker volume create hide-demo
hide-demo

$ docker run --rm --mount type=volume,src=hide-demo,dst=/etc,volume-nocopy alpine sh -c 'ls -la /etc; test ! -e /etc/alpine-release && echo "base file is hidden by the mount"'
total 0
drwxr-xr-x    2 root     root           ... .
drwxr-xr-x    1 root     root           ... ..
base file is hidden by the mount

$ docker volume rm hide-demo
hide-demo

در این آزمایش /etc/alpine-release هنوز داخل image وجود دارد. volume خالی فقط روی /etc نشسته و آن را از دید container پنهان کرده. اگر container را بدون آن mount اجرا کنی، فایل دوباره دیده می‌شود. این رفتار وقتی روی مسیرهای مهمی مثل /app یا پوشهٔ دادهٔ پایگاه‌داده mount می‌کنی، می‌تواند حسابی گیج‌کننده باشد.

The file /etc/alpine-release comes from the image. In the second run, the empty mount hides it; the file was not deleted and the image was not changed. Run a fresh container without the mount and the image file is visible again. This matters for paths such as /app, /var/lib/postgresql/data, and build directories.

خطای واقعیA realistic failure

اگر بعد از mount می‌گویی «فایل‌های image ناپدید شدند»، اول مقصد را بررسی کن. ممکن است فقط روی آن‌ها پوشانده باشی. container را بدون mount اجرا کن یا mount را به مسیر خالی و مخصوص داده منتقل کن؛ دنبال کپی‌کردن کورکورانهٔ فایل‌ها نباش.

If files from the image seem to disappear after a mount, inspect the target first. They may simply be covered. Run without the mount or use a dedicated empty data path; do not blindly copy files until the model is clear.

bind mount: وقتی پوشهٔ host باید زنده دیده شودBind mounts: when a host folder should stay visible

گاهی برعکس volume، دقیقاً می‌خواهی همان فایلی که روی سیستم خودت ویرایش می‌کنی داخل container هم فوراً دیده شود. اینجا bind mount مناسب است: Docker یک مسیر واقعی از host را داخل container نشان می‌دهد. فایل هنوز مال host است؛ Docker فقط راه دسترسی را وصل می‌کند.

With a bind mount, the source is a real path on the host. This is useful for source code, a local config file, output that must appear immediately on your machine, and everyday VS Code work. Docker does not own the data; it makes the host path visible inside the container.

A host directory is bind-mounted into a container workspace and writes can flow back to the host host directory./siteindex.htmlowned by host bind mounthost path → /workspacerw by default container/workspaceVS Code viewcan write read filessave changes visiblewrites host

فلش رفت‌وبرگشت اول نشان می‌دهد فایل از ./site خوانده می‌شود و ذخیرهٔ داخل کانتینر به host برمی‌گردد. فلش‌های دوم فقط قابل‌دیدن بودن همین مسیر را در /workspace نشان می‌دهند؛ Docker مالک پوشهٔ host نیست.

The first pair of arrows shows files read from ./site and writes from the container returning to the host. The second pair shows the same path visible at /workspace; Docker does not own the host directory.

VS Code style · bind mount
$ mkdir -p site
$ printf '<h1>hello from host</h1>\n' > site/index.html

$ docker run --rm --mount type=bind,src="$PWD/site",dst=/workspace alpine \
  sh -c 'cat /workspace/index.html; echo "<p>written in container</p>" > /workspace/generated.html'
<h1>hello from host</h1>

$ ls site
generated.html  index.html

روی Windows و PowerShell حواست به شکل مسیرها باشد. مهم نیست نمونهٔ دستور روی اینترنت POSIX نوشته شده یا PowerShell؛ چیزی که Docker لازم دارد یک مسیر واقعی و درست از host است. مزیت --mount اینجاست که اگر مبدأ پیدا نشود، خطا واضح‌تر است و اشتباه کمتر پنهان می‌ماند.

In Windows PowerShell, use an absolute path or the PowerShell form for the host source instead of the POSIX form $PWD/site; the important part is that the host path exists. With --mount, a missing source is an error. With -v, Docker may create a source directory in some cases, which can hide a typo.

bind mount به‌طور پیش‌فرض writable است. اگر container فقط باید فایل‌ها را بخواند، readonly یا :ro بگذار: docker run --rm --mount type=bind,src="$PWD/site",dst=/site,readonly alpine cat /site/index.html. یادت باشد daemon روی کدام host اجرا می‌شود؛ در راه‌دور Docker، مسیر host، سیستم راه‌دور است نه لپ‌تاپ client.

A bind mount is writable by default. If the container should only read files, use readonly or :ro. Also remember which host runs the daemon: with a remote Docker daemon, the path belongs to the remote host, not the laptop running the CLI.

کدام جا برای کدام داده؟Which storage fits which data?

اگر بین این سه انتخاب گیر کردی، سؤال را خیلی ساده کن: «این داده قرار است با همین container بمیرد؟ باید مستقل از container بماند؟ یا خودم روی host باید مستقیم فایلش را ببینم و ویرایشش کنم؟» جواب همین سه سؤال معمولاً تو را به writable layer، volume یا bind mount می‌رساند.

Separate the three options with one question: should the data end with this container, should Docker keep it separately, or do I need to see and manage the files directly on the host?

محلمالک و چرخهٔ عمرنمونهٔ خوبریسک رایج
writable layerمتعلق به همان containercache یا آزمایش چنددقیقه‌ایبا rm از بین می‌رود
named volumeDocker مدیریت می‌کند و مستقل می‌ماندdatabase، uploads، دادهٔ سرویسvolume اشتباه را mount یا حذف می‌کنی
bind mounthost مالک مسیر و فایل‌هاستsource code، config محلی، output توسعهpermission و تغییر ناخواستهٔ host

anonymous volume هم وجود دارد: اگر به جای نام، فقط مقصدی مثل -v /data بدهی، Docker یک volume بی‌نام می‌سازد. برای یک آزمایش کوتاه بد نیست، اما در کار آموزشی و سرویس واقعی named volume بهتر قابل ردیابی است؛ اینجا وارد جزئیاتش نمی‌شویم.

Anonymous volumes also exist: a destination such as -v /data creates a volume without a human-chosen name. They can be handy for a short experiment, but named volumes are easier to trace in a real service. We will not go deeper into them here.

مجوز دسترسی: وصل‌کردن volume مشکل مالکیت را جادویی حل نمی‌کندPermissions: a mount does not solve ownership

وصل‌شدن مسیر فقط می‌گوید داده کجاست؛ نمی‌گوید چه کسی اجازهٔ نوشتن دارد. پردازش داخل container هنوز با UID/GID خودش اجرا می‌شود. اگر مالک پوشه و کاربری که برنامه با آن اجرا می‌شود با هم جور نباشند، Permission denied کاملاً طبیعی است. قبل از chmod 777 اول ببین دقیقاً چه کسی می‌نویسد و مالک مسیر کیست.

After mounting a volume or bind path, the process still runs with its own UID/GID. If the directory belongs to another UID, writes can fail with Permission denied. Inspect the process identity and file mode before choosing a fix.

small permission check
$ docker run --rm --mount type=volume,src=demo-data,dst=/data alpine sh -c 'id; ls -ld /data; touch /data/check.txt; ls -l /data/check.txt'
uid=0(root) gid=0(root)
drwxr-xr-x    2 root     root          ... /data
-rw-r--r--    1 root     root            0 ... /data/check.txt

راه‌حل درست به image و محیط بستگی دارد: شاید لازم باشد مالکیت را از قبل تنظیم کنی، UID/GID را هماهنگ کنی یا مسیر مناسبی برای داده انتخاب کنی. chmod 777 معمولاً فقط صدای خطا را ساکت می‌کند؛ علت را توضیح نمی‌دهد و دسترسی را هم بیش از حد باز می‌کند.

A real image may run as a non-root user. A fix can be preparing ownership, matching the container UID/GID to the host, adjusting permissions deliberately, or choosing a volume. chmod 777 is not a model for the problem; it only hides the permission boundary.

برای تنظیمات یا مبدأ که container نباید تغییرش دهد، read-only را هم به مجوز model اضافه کن. read-only هم جلوی خواندن را نمی‌گیرد و هم به‌تنهایی جایگزین ownership درست نیست؛ فقط مسیر نوشتن را می‌بندد.

For config or source that the container must not change, add read-only to the permission model. Read-only still allows reading and is not a replacement for correct ownership; it simply closes the write path.

از فایل کوچک به دادهٔ واقعی: PostgreSQLFrom a tiny file to real data: PostgreSQL

تا اینجا با فایل کوچک بازی کردیم؛ حالا همان مفهوم را جایی ببینیم که واقعاً درد دارد: پایگاه‌داده. imageِ PostgreSQL برنامه را می‌آورد و volume خانهٔ داده را فراهم می‌کند. اگر این دو را جدا بفهمی، حذف container دیگر مساوی حذف اطلاعات نیست.

Now apply the same idea to a database. Treat the image as the program and the volume as the data directory. To keep this lesson stable, we deliberately pin postgres:17 and mount the volume at /var/lib/postgresql/data. Data paths can differ in newer major versions, so do not leave the version floating without a reason.

رمز فقط برای آزمایشA lab-only password

POSTGRES_PASSWORD=dev-only را فقط برای همین محیط محلی ببین. مدیریت secret، environment و روش امن اجرای سرویس در فصل ۰۹ می‌آید؛ اینجا موضوع ما داده مسیر و عمر volume است.

Treat POSTGRES_PASSWORD=dev-only as local-lab material only. Secret handling, environment, and safer service configuration belong to Chapter 9; this chapter is about the data path and volume lifetime.

database with a named volume
$ docker volume create pg-data
pg-data

$ docker run -d --name pg-data \
  -e POSTGRES_PASSWORD=dev-only \
  --mount type=volume,src=pg-data,dst=/var/lib/postgresql/data \
  postgres:17
...

$ docker exec -i pg-data psql -U postgres -c "CREATE TABLE notes (id serial PRIMARY KEY, body text); INSERT INTO notes(body) VALUES ('kept in volume');"
CREATE TABLE
INSERT 0 1

$ docker exec -i pg-data psql -U postgres -c "SELECT id, body FROM notes;"
 id |      body
----+----------------
  1 | kept in volume
(1 row)

دستورهای psql اینجا فقط وسیله‌اند تا یک رکورد واقعی بسازیم و بعد پیدایش کنیم. موضوع فصل PostgreSQL نیست. چیزی که باید دنبال کنی محل نوشتن داده است: table داخل مسیری نوشته می‌شود که volume روی آن mount شده. حالا می‌توانیم container را دور بیندازیم و با یک container تازه همان داده را دوباره ببینیم.

Here docker exec is only a way to query the database; this is not a PostgreSQL command course. The important part is that the table is written into a data directory separate from the container. Remove the container and create another one with the same volume.

recreate the database container
$ docker rm -f pg-data
pg-data

$ docker run -d --name pg-data \
  -e POSTGRES_PASSWORD=dev-only \
  --mount type=volume,src=pg-data,dst=/var/lib/postgresql/data \
  postgres:17
...

$ docker exec -i pg-data psql -U postgres -c "SELECT id, body FROM notes;"
 id |      body
----+----------------
  1 | kept in volume
(1 row)

اگر این پرس‌وجو شکست خورد، فوراً نتیجه نگیر که volume خراب است: ممکن است container هنوز آماده نشده باشد، image اصلی با داده جور نباشد، مقصد مسیر اشتباه باشد یا volume دیگری را mount کرده باشی. docker inspect pg-data و docker volume inspect pg-data محل شروع بررسی‌اند.

If the query fails, do not immediately blame the volume. The container may not be ready, the image major may not match the data, the target path may be wrong, or a different volume may be mounted. Start with docker inspect pg-data and docker volume inspect pg-data.

پشتیبان یعنی کپی از volume، نه عکس از کانتینرA backup copies the volume, not the container

اینجا یک دام مهم هست: «داده از container جدا شد» به معنی «پس پشتیبان داریم» نیست. اگر خود volume اشتباهی پاک شود یا دیسک خراب شود، اسم volume معجزه نمی‌کند. بنابراین باید یک نسخهٔ مستقل بیرون از همان volume بسازیم و مهم‌تر از آن، یک بار هم بازیابی‌اش را امتحان کنیم.

Having a volume is not the same as having a backup. If you remove the wrong volume, Docker cannot guess the previous version. We use a temporary helper container: mount the data volume read-only, bind a host backup folder, and write a tar archive there.

A helper container reads a volume into a host archive, then restores it into a fresh volume pg-datasource volume helpertar reads /volumetemporary container host archivebackups/pg-data.tgz fresh volumepg-restored restore helper tar extracts read-onlytar czfrestore tar xzfinto volume

فلش بالایی مسیر پشتیبان را می‌خواند: volume → کمکی → فایل پشتیبان روی host → volume تازه. فلش پایینی مسیر بازیابی را برمی‌گرداند: فایل پشتیبان توسط کمکی باز می‌شود و فایل‌ها داخل volume مقصد بازکردن می‌شوند.

The top path is backup: volume → helper → archive on the host → fresh volume. The bottom path is restore: a helper opens the archive and extracts its files into the destination volume.

برای این آزمایش، قبل از گرفتن tar، PostgreSQL را متوقف می‌کنیم تا وسط نوشتن از فایل‌های خام کپی نگیریم. این کار برای فهم مسیر پشتیبان خوب است، اما نسخهٔ نهاییِ سیاست پشتیبان‌گیری یک پایگاه‌دادهٔ واقعی نیست؛ هر پایگاه‌داده ابزار و قواعد سازگاری خودش را دارد.

For a raw PostgreSQL data directory, stop the service before tar so the files are not copied while they are changing. This is a teaching workflow, not a universal production backup policy; production also needs database-specific consistency and native backup considerations.

backup and restore with a helper
$ mkdir -p backups
$ docker stop pg-data
pg-data

$ docker run --rm \
  --mount type=volume,src=pg-data,dst=/volume,readonly \
  --mount type=bind,src="$PWD/backups",dst=/backup \
  alpine sh -c 'tar czf /backup/pg-data.tgz -C /volume .'

$ ls -lh backups/pg-data.tgz
-rw-r--r--  1 user  user  18K ... backups/pg-data.tgz

$ docker volume create pg-restored
pg-restored

$ docker run --rm \
  --mount type=volume,src=pg-restored,dst=/volume \
  --mount type=bind,src="$PWD/backups",dst=/backup \
  alpine sh -c 'tar xzf /backup/pg-data.tgz -C /volume'

$ docker run -d --name pg-restored \
  -e POSTGRES_PASSWORD=dev-only \
  --mount type=volume,src=pg-restored,dst=/var/lib/postgresql/data \
  postgres:17
...

$ docker exec -i pg-restored psql -U postgres -c "SELECT id, body FROM notes;"
 id |      body
----+----------------
  1 | kept in volume
(1 row)

دو مسیر را از هم جدا نگه دار: /volume مسیر داخل کمکی برای volume است و /backup مسیر فایل پشتیبان روی host. اگر در tar اشتباهاً /data یا مسیر دیگری بزنی، فایل پشتیبان شاید ساخته شود اما بازیابی به داده پوشه واقعی چیزی اضافه نمی‌کند. بعد از بازیابی هم همان اصلی version و مقصد مسیر را نگه دار.

Keep the two paths distinct: /volume is the helper's view of the data volume, while /backup is the host archive path. If the tar command uses the wrong path, an archive may still be created but restore will not put files into the real data directory. Keep the same major version and target path after restore.

پاک‌سازی: قبل از حذف volume مطمئن شو چه چیزی را دور می‌اندازیCleanup: remove a volume deliberately

حذف کانتینر با حذف volume یکی نیست؛ این جدایی همان چیزی است که persistence را ممکن می‌کند. اما همین جدایی یعنی volumeهای آزمایشی اگر تمیز نشوند روی disk می‌مانند. اول consumerها را پیدا کن، سپس volume را با نام دقیقش حذف کن.

Removing a container is not the same as removing its volume; that separation is what makes persistence possible. It also means experimental volumes can remain on disk. Find the consumers first, then remove the exact volume by name.

safe-ish lab cleanup
$ docker ps -a --filter volume=pg-restored
$ docker rm -f pg-restored
pg-restored

$ docker volume inspect pg-restored
[
  { "Name": "pg-restored", "Mountpoint": "..." }
]

$ docker volume rm pg-restored
pg-restored

$ docker volume ls

با docker volume prune مثل جاروبرقی رفتار نکن. volumeای که الان container فعالی به آن وصل نیست، ممکن است هنوز تنها نسخهٔ یک دادهٔ مهم باشد. در تمرین‌ها نام دقیق volume را پاک کن و اگر داده ارزش دارد، قبلش مطمئن شو پشتیبان واقعاً وجود دارد و قابل‌بازیابی است.

Do not treat docker volume prune as a mindless vacuum; an unused volume may still be your only local backup. In a lab, remove the exact volume name. If the data matters, check that an archive exists and can be restored before deletion.

وقتی داده «نیست»، از کجا بررسی را شروع کنیم؟When data is “missing,” where do you start?

وقتی می‌گویی «داده نیست»، قبل از هر چیز بپرس کجا انتظار داشتی باشد. اسم volume چیست؟ داخل container روی چه مسیری mount شده؟ اصلاً همان container و همان volume را اجرا کرده‌ای؟ این سؤال‌های ساده معمولاً سریع‌تر از تغییر مجوز و ساخت volumeهای تازه جواب می‌دهند.

Most failures in this chapter get worse when we guess. Ask Docker for the name, source, destination, and container state; then compare the output with the command you actually ran.

نشانهاحتمال قویبررسی و اصلاح
source bind پیدا نمی‌شودمسیر host اشتباه یا روی daemon دیگری استمسیر absolute، وجود directory و Docker context را بررسی کن
فایل‌های image ناپدید شده‌اندmount روی target آن‌ها را پوشاندهبدون mount اجرا کن یا target را تغییر بده؛ volume-nocopy را آگاهانه بفهم
Permission deniedUID/GID یا mode مناسب نیستid و ls -ld را داخل helper/کانتینر ببین
volume خالی به نظر می‌رسدنام یا destination اشتباه استdocker inspect و volume inspect را مقایسه کن
archive ساخته شده اما restore خالی استمسیر tar -C یا bind archive اشتباه بودهلیست archive را با tar tzf ببین و مسیر helper را یکی کن
PostgreSQL data نداردmajor/path mismatch یا container هنوز آماده نیستنسخه، target /var/lib/postgresql/data و log را بررسی کن
three inspection questions
$ docker inspect pg-data --format '{{json .Mounts}}'
[{"Type":"volume","Name":"pg-data","Source":"...","Destination":"/var/lib/postgresql/data","RW":true}]

$ docker volume inspect pg-data
[
  {"Name":"pg-data","Driver":"local","Mountpoint":"..."}
]

$ docker run --rm --mount type=volume,src=pg-data,dst=/volume alpine \
  sh -c 'find /volume -maxdepth 2 -type f | head'

تمرین‌ها: محل داده را خودت انتخاب کنExercises: choose the data's home

این تمرین‌ها را مثل سؤال امتحانی حل نکن. قبل از دیدن جواب، برای هر سناریو فقط یک جمله بنویس: «داده کجاست و عمرش به چه چیزی وابسته است؟» اگر همین را درست جواب بدهی، انتخاب بین writable layer، volume و bind mount خیلی راحت‌تر می‌شود.

Each exercise includes a short mini-lesson. Run the command before reading the solution, then explain why the data belongs in the writable layer, a volume, or a bind mount.

۱. stop یا rm؟1. Stop or remove?

یک فایل در کانتینر بساز، آن را stop و start کن، سپس container را rm کن. کدام مرز باعث ناپدیدشدن فایل شد؟

Create a file in a container, stop and start it, then remove the container. Which boundary made the file disappear?

پاسخ و نکتهSolution and lesson

تا وقتی همان container وجود دارد، stop/start همان writable layer را برمی‌گرداند. rm layer را همراه container حذف می‌کند؛ image فقط base را دوباره می‌دهد.

As long as the same container exists, stop/start returns the same writable layer. rm removes that layer with the container; the image only provides the base again.

۲. volume بساز2. Create a volume

volumeای به نام lesson-data بساز و با یک container Alpine فایل /data/answer.txt را در آن بنویس.

Create a volume named lesson-data and use an Alpine container to write /data/answer.txt into it.

پاسخ و نکتهSolution and lesson

با docker volume create lesson-data و سپس docker run --rm --mount type=volume,src=lesson-data,dst=/data alpine sh -c 'echo answer > /data/answer.txt' انجامش بده. mount محل داده را از عمر container جدا می‌کند.

Use docker volume create lesson-data, then docker run --rm --mount type=volume,src=lesson-data,dst=/data alpine sh -c 'echo answer > /data/answer.txt'. The mount separates the data location from container lifetime.

۳. با همان volume بخوان3. Read from the same volume

یک container دوم بساز که بدون ساخت فایل جدید، answer.txt را از lesson-data بخواند.

Create a second container that reads answer.txt from lesson-data without creating the file again.

پاسخ و نکتهSolution and lesson

docker run --rm --mount type=volume,src=lesson-data,dst=/data alpine cat /data/answer.txt. container دوم فقط consumer است؛ volume صاحب داده است.

docker run --rm --mount type=volume,src=lesson-data,dst=/data alpine cat /data/answer.txt. The second container is only a consumer; the volume owns the data's lifetime.

۴. inspect را بخوان4. Read inspect

خروجی docker volume inspect lesson-data را پیدا کن و توضیح بده چرا Mountpoint را نباید به‌عنوان مسیر برنامه hard-code کرد.

Find the output of docker volume inspect lesson-data and explain why the Mountpoint should not be hard-coded into an application.

پاسخ و نکتهSolution and lesson

Mountpoint مسیر داخلی Docker روی host است و بین Linux، Docker Desktop و راه‌دور daemon فرق می‌کند. برنامه باید با مسیر داخل container مثل /data کار کند.

Mountpoint is Docker's internal host path and varies across Linux, Docker Desktop, and remote daemons. The application should use its container path such as /data.

۵. شکل کوتاه را ترجمه کن5. Translate the shorthand

-v lesson-data:/data:ro را به --mount تبدیل کن و اثر ro را بگو.

Convert -v lesson-data:/data:ro to --mount syntax and state what ro does.

پاسخ و نکتهSolution and lesson

شکل صریح --mount type=volume,src=lesson-data,dst=/data,readonly است. container می‌تواند بخواند، اما نوشتن در volume از این mount باید fail شود.

The explicit form is --mount type=volume,src=lesson-data,dst=/data,readonly. The container can read, but writes through this mount should fail.

۶. bind mount بساز6. Create a bind mount

یک پوشهٔ host به نام site بساز، فایلی داخلش بگذار و آن را در /site داخل Alpine بخوان.

Create a host folder named site, put a file in it, and read it at /site inside Alpine.

پاسخ و نکتهSolution and lesson

روی POSIX از mkdir -p site و docker run --rm --mount type=bind,src="$PWD/site",dst=/site alpine ls -l /site استفاده کن. مبدأ اینجا نام volume نیست؛ مسیر واقعی host است.

On POSIX use mkdir -p site and docker run --rm --mount type=bind,src="$PWD/site",dst=/site alpine ls -l /site. Here the source is a real host path, not a volume name.

۷. مسیر اشتباه host7. A wrong host path

با --mount type=bind یک مبدأ وجودنداشته بده. چه خطایی می‌بینی و چرا در -v ممکن است گمراه‌کننده‌تر باشد؟

Pass a missing source to a bind mount with --mount. What error appears, and why can -v be more misleading?

پاسخ و نکتهSolution and lesson

--mount معمولاً missing مبدأ را خطا می‌کند؛ -v ممکن است پوشه بسازد و container با پوشهٔ خالی بالا بیاید. خطای زودهنگام برای پیدا کردن typo بهتر است.

--mount normally errors on a missing source; -v may create a directory and let the container start with an empty folder. An early error is easier to debug.

۸. مقصد را اشتباه انتخاب کن8. Choose the wrong target

یک bind mount را روی /etc خالی بگذار و اثرش را روی /etc/alpine-release توضیح بده.

Place an empty bind mount over /etc and explain its effect on /etc/alpine-release.

پاسخ و نکتهSolution and lesson

محتوای host روی /etc دیده می‌شود و فایل image زیر mount پنهان می‌شود. mount داده را در مسیر اختصاصی بگذار تا فایل‌های base ناخواسته پوشانده نشوند.

The host contents become visible at /etc and the image file underneath is hidden. Use a dedicated data target so base files are not obscured accidentally.

۹. read-only را ثابت کن9. Prove read-only

یک bind mount با readonly بساز و تلاش کن داخلش فایل بنویسی. تفاوت خطای آن با مجوز معمولی چیست؟

Create a read-only bind mount and try to write a file. How is that failure different from an ordinary permission problem?

پاسخ و نکتهSolution and lesson

در mount read-only، حتی پردازش مجاز هم از این مسیر اجازهٔ write ندارد؛ علت در option mount است. مجوز معمولی به UID/GID و mode فایل مربوط می‌شود.

With a read-only mount, even an otherwise permitted process cannot write through that path; the cause is the mount option. Ordinary permission depends on UID/GID and file mode.

۱۰. سه محل را مقایسه کن10. Compare the three locations

برای cache موقت، پایگاه‌داده و کد منبع به‌ترتیب writable layer، volume یا bind mount را انتخاب کن و برای هرکدام یک جمله دلیل بنویس.

Choose writable layer, volume, or bind mount for temporary cache, a database, and source code, and give one reason for each.

پاسخ و نکتهSolution and lesson

cache موقت می‌تواند writable layer باشد؛ پایگاه‌داده معمولاً named volume می‌خواهد تا با حذف container از بین نرود؛ کد منبع در توسعه bind mount می‌خواهد تا تغییر host فوری دیده شود.

Temporary cache can use the writable layer; a database usually needs a named volume so it outlives the container; development source code benefits from a bind mount so host edits appear immediately.

۱۱. UID/GID را ببین11. Inspect UID/GID

داخل یک container دستور id و ls -ld /data را اجرا کن. اگر پردازش غیرroot بود، قبل از تغییر مجوز چه چیزی را باید هماهنگ کنی؟

Run id and ls -ld /data inside a container. If the process is non-root, what should you align before changing permissions?

پاسخ و نکتهSolution and lesson

UID/GID پردازش، مالک پوشه و mode آن را مقایسه کن. اول user model image و مالکیت داده را بفهم؛ تغییر مجوز بدون این سه عدد معمولاً حدس است.

Compare the process UID/GID with the directory owner and mode. Understand the image's user model and data ownership before changing permissions; without those facts, the change is guesswork.

۱۲. volume را بین دو container به اشتراک بگذار12. Share one volume

container اول فایل بسازد و container دوم آن را بخواند. در توضیح خود فرق «دو پردازش» و «دو کپی داده» را روشن کن.

Have one container create a file and another read it. Explain the difference between two processes and two copies of the data.

پاسخ و نکتهSolution and lesson

هر دو را با --mount type=volume,src=shared,dst=/data اجرا کن. دو پردازش به یک volume و همان bytes وصل‌اند؛ containerها جدا هستند اما کپی جدا ساخته نشده.

Run both with --mount type=volume,src=shared,dst=/data. The processes are separate but point at the same volume and bytes; no second copy was created.

۱۳. PostgreSQL را pin کن13. Pin PostgreSQL

فرمان اجرای PostgreSQL را طوری بنویس که هم postgres:17 ثابت باشد و هم volume در مقصد درست mount شود.

Write a PostgreSQL run command that pins postgres:17 and mounts the volume at the correct target.

پاسخ و نکتهSolution and lesson

از --mount type=volume,src=pg-data,dst=/var/lib/postgresql/data همراه -e POSTGRES_PASSWORD=dev-only و image postgres:17 استفاده کن. pin و مقصد هر دو بخشی از قرارداد این آزمایشگاه هستند.

Use --mount type=volume,src=pg-data,dst=/var/lib/postgresql/data with -e POSTGRES_PASSWORD=dev-only and image postgres:17. The pin and target are both part of this lab's contract.

۱۴. container را recreate کن14. Recreate the container

یک ردیف در PostgreSQL بساز، container را remove کن و با همان volume بالا بیاور. قبل از پرس‌وجو چه وضعیتی را باید صبر کنی؟

Create a PostgreSQL row, remove the container, and start a new one with the same volume. What state should you wait for before querying?

پاسخ و نکتهSolution and lesson

باید منتظر آماده شدن server بمانی؛ start شدن پردازش با آماده‌بودن PostgreSQL یکی نیست. بعد docker exec ... psql را اجرا کن و همان ردیف را ببین.

Wait until the server is ready; a started process is not necessarily a ready PostgreSQL server. Then run docker exec ... psql and verify the same row.

۱۵. فایل پشتیبان را فهرست کن15. List an archive

بعد از tar گرفتن از volume، با tar tzf ببین فایل پشتیبان چه مسیرهایی دارد. چرا دانستن prefix برای بازیابی مهم است؟

After creating a tar archive from a volume, use tar tzf to see its paths. Why does the prefix matter during restore?

پاسخ و نکتهSolution and lesson

tar tzf backups/pg-data.tgz | head نشان می‌دهد فایل‌ها با چه ریشه‌ای ذخیره شده‌اند. prefix اضافی ممکن است فایل‌ها را یک سطح پایین‌تر از داده پوشه واقعی بازکردن کند.

tar tzf backups/pg-data.tgz | head shows the stored root. An unwanted prefix can extract files one level below the real data directory and make PostgreSQL see an empty path.

۱۶. بازیابی به volume تازه16. Restore into a fresh volume

یک volume به نام restored-data بساز و فایل پشتیبان را با کمکی داخل آن بازکردن کن. چرا کمکی لازم است؟

Create a volume named restored-data and extract the archive into it with a helper. Why is a helper needed?

پاسخ و نکتهSolution and lesson

کمکی هم‌زمان volume و bind mount فایل پشتیبان را می‌بیند و ابزار tar را اجرا می‌کند، بدون اینکه image برنامه را تغییر بدهی. این container بعد از کار با --rm حذف می‌شود.

The helper sees both the volume and the archive bind mount and runs tar without changing the application image. It is disposable and can be removed with --rm.

۱۷. قبل از prune فکر کن17. Think before prune

دو دلیل بنویس که چرا docker volume prune می‌تواند خطرناک باشد، حتی اگر volume به container در حال اجرا وصل نباشد.

Give two reasons why docker volume prune can be dangerous even when a volume is not attached to a running container.

پاسخ و نکتهSolution and lesson

ممکن است volume متعلق به container متوقف‌شده یا تنها کپی local داده باشد و هنوز فایل پشتیبان سالمی نداشته باشی. «استفاده‌نشدن فعلی» مساوی «بی‌ارزش بودن» نیست.

It may belong to a stopped container or be the only local copy before a verified archive exists. “Not currently used” does not mean “worthless.”

۱۸. یک runbook کوتاه بنویس18. Write a short runbook

برای سرویسی که volume آن خالی دیده می‌شود، چهار دستور بررسی به‌ترتیب بنویس: نام container، Mounts، volume inspect و محتوای داخل mount.

For a service whose volume looks empty, write four inspection commands in order: container name, Mounts, volume inspect, and the mounted contents.

پاسخ و نکتهSolution and lesson

docker ps -a، docker inspect NAME --format '{{json .Mounts}}'، docker volume inspect VOLUME و docker run --rm --mount type=volume,src=VOLUME,dst=/volume alpine ls -la /volume. این زنجیره نام و مسیر را از حدس جدا می‌کند.

docker ps -a, docker inspect NAME --format '{{json .Mounts}}', docker volume inspect VOLUME, and docker run --rm --mount type=volume,src=VOLUME,dst=/volume alpine ls -la /volume. This chain separates names and paths from guesses.

آزمایشگاه کامل: ساخت، حذف، پشتیبان و بازیابی PostgreSQLFull lab: build, remove, back up, and restore PostgreSQL

این آزمایشگاه را مثل یک حادثهٔ واقعیِ کوچک انجام بده: یک رکورد می‌سازی، container را حذف می‌کنی، با همان volume برش می‌گردانی، بعد از volume نسخهٔ پشتیبان می‌گیری و روی یک volume تازه بازیابی می‌کنی. اگر در پایان فقط «دستورها کار کردند» بگویی کافی نیست؛ باید بتوانی نشان بدهی هر بار داده دقیقاً کجا بوده.

Run this lab as a small operational story. By the end, you should be able to show that a row survived container removal, an archive exists on the host, and a fresh volume restored the same data.

  1. آماده‌سازی: volumeهای lab-pg و lab-pg-restored را فقط اگر از تمرین قبلی مانده‌اند حذف کن؛ اسم‌های دقیق را چک کن.Prepare: remove lab-pg and lab-pg-restored only if they are leftovers from this lab; check the exact names.
  2. راه‌اندازی: postgres:17 را با named volume lab-pg و مقصد /var/lib/postgresql/data اجرا کن. تا آماده شدن log صبر کن.Start: run postgres:17 with named volume lab-pg at /var/lib/postgresql/data. Wait for the ready log.
  3. داده: با docker exec جدول lesson_events و یک ردیف به نام volume-survived بساز.Data: use docker exec to create lesson_events and insert one row named volume-survived.
  4. بازسازی: container را rm -f کن و با همان volume دوباره بساز. پرس‌وجو را اجرا کن و ردیف را ثبت کن.Recreate: rm -f the container and create it again with the same volume. Query and record the row.
  5. پشتیبان: پوشهٔ backups روی host بساز، container را stop کن و با کمکی و mount read-only فایل پشتیبان بگیر. اندازه و فهرست فایل پشتیبان را ببین.Back up: create host folder backups, stop the container, and create an archive with a helper and a read-only mount. Check its size and listing.
  6. بازیابی: lab-pg-restored را بساز، فایل پشتیبان را داخلش بازکردن کن، PostgreSQL دیگری با همان اصلی version روی آن بالا بیاور و پرس‌وجو بگیر.Restore: create lab-pg-restored, extract the archive, start another PostgreSQL with the same major version, and query it.
  7. خرابی عمدی: یک بار مقصد اشتباه مثل /wrong-data بده و توضیح بده چرا PostgreSQL از آن استفاده نکرد. سپس دستور درست را اصلاح کن.Intentional failure: once use a wrong target such as /wrong-data and explain why PostgreSQL did not use it. Then correct the command.
  8. تحویل: خروجی docker inspect، نام volume، مسیر فایل پشتیبان و پرس‌وجو نهایی را کنار هم نگه دار؛ این‌ها زنجیرهٔ قابل بررسی آزمایشگاه هستند.Hand off: keep the docker inspect output, volume names, archive path, and final query together; they are the lab's inspection trail.
lab checkpoint
$ docker inspect lab-pg --format '{{range .Mounts}}{{.Name}} -> {{.Destination}} (rw={{.RW}}){{end}}'
lab-pg -> /var/lib/postgresql/data (rw=true)

$ tar tzf backups/lab-pg.tgz | head
./
./PG_VERSION
./base/
...

$ docker exec -i lab-pg-restored psql -U postgres -c "SELECT event FROM lesson_events;"
      event
-----------------
 volume-survived
(1 row)

این آزمایشگاه عمداً وارد network، Compose، registry یا deployment نمی‌شود. سؤال فصل فقط این است: داده کجا نوشته شد، چه چیزی آن را نگه داشت، و چطور آن را به یک volume تازه برگرداندیم.

This lab intentionally does not enter networking, Compose, registries, or deployment. The chapter's question is narrower: where were the bytes written, what kept them alive, and how did we restore them into a fresh volume?

مرز فصل و قدم بعدThe boundary and the next step

حالا فرق سه خانه روشن شده: writable layer خانهٔ موقتِ خود container است؛ named volume خانه‌ای جداست که Docker مدیریت می‌کند؛ bind mount پنجره‌ای به یک مسیر واقعی روی host است. و پشتیبان هم هیچ‌کدام از این‌ها نیست؛ یک نسخهٔ جداست که باید بتوانی واقعاً از آن برگردی.

You now know why “containers are ephemeral” does not mean “data is ephemeral.” The writable layer is for local changes to one container; a named volume is for data independent of the container; a bind mount is for files the host must see and manage directly. Backup is a separate operation, not an automatic property of a volume.

اگر سؤال تو این است...از این فصل چه بردارعمداً در فصل بعدی نیست
چرا بعد از rm فایل رفت؟writable layer به عمر container وصل استشبکه و سرویس‌های چندکانتینری
database را کجا بگذارم؟named volume و target درستتنظیمات کامل database و secret
فایل host را داخل app ببینم؟bind mount، path و readonlydeployment و production storage
اگر volume حذف شد چه؟helper، tar، restore و بررسی archivepolicy کامل disaster recovery

فصل بعد سؤال تازه‌ای داریم: حالا که داده را نگه داشتیم، اگر web و پایگاه‌داده در دو container جدا باشند چطور همدیگر را پیدا می‌کنند؟ قبل از رفتن به شبکه فقط یک عادت را با خودت ببر: پیش از هر rm یا prune بپرس «داده دقیقاً کجاست؟»

Chapter 7 moves to networking; Chapter 8 composes these services, and Chapter 9 organizes environment and secrets. Finish this chapter with one habit: before rm or prune, ask exactly where the data lives.

برگهٔ تقلبQuick reference

the commands worth remembering
docker volume create NAME
docker volume ls
docker volume inspect NAME
docker volume rm NAME

docker run --rm --mount type=volume,src=NAME,dst=/data IMAGE COMMAND
docker run --rm --mount type=bind,src=/host/path,dst=/data,readonly IMAGE COMMAND

docker inspect CONTAINER --format '{{json .Mounts}}'
docker run --rm --mount type=volume,src=NAME,dst=/volume alpine sh -c 'tar czf /backup/data.tgz -C /volume .'

برای مطالعهٔ بیشتر، مستندات رسمی Docker volumes، bind mounts و image رسمی PostgreSQL را ببین. در هر سه منبع، نحوهٔ نوشتن را با نسخهٔ Docker و image‌ای که واقعاً اجرا می‌کنی تطبیق بده.

For further reading, use Docker's official volumes, bind mounts, and PostgreSQL image documentation. Match the syntax to the Docker and image versions you actually run.