کدنامهمرجع‌های مهندسی نرم‌افزار، به فارسی
Docker · فصل ۱۴Docker · Chapter 14

استقرار روی VPS

Deploying Docker applications to a VPS

برنامه روی لپ‌تاپ کار می‌کند و image در registry است. حالا مسیر را تا جایی ادامه می‌دهیم که کاربر واقعی بتواند درخواست بفرستد؛ با نسخهٔ مشخص، health قابل‌بررسی و راه برگشت.

The application works on a laptop and its image is in a registry. Now we carry it to the point where a real user can send a request—with a named release, verifiable health, and a way back.

Ubuntuمسیر اصلیmain server path
5نمودارdiagrams
18تمرین پاسخ‌دارsolved exercises

برنامه روی لپ‌تاپ سالم است؛ حالا باید از بیرون واقعاً به آن رسیدIt works on our laptop—how does a real user reach it?

image را در registry داریم و روی سیستم خودمان هم همه‌چیز کار می‌کند. حالا سؤال عوض شده: کاربر واقعی چطور به این برنامه می‌رسد؟ از اینجا به بعد فقط container مهم نیست؛ image درست باید به سرور برسد، فقط portهای لازم عمومی باشند، درخواست از proxy عبور کند و بعد از هر انتشار بتوانیم ثابت کنیم نسخهٔ تازه واقعاً سالم است.

The image is in a registry. A teammate asks, “Okay, how does a real user reach it?” The problem is no longer just starting a container: the right image must reach the server, public traffic must reach the app, and after a change we need evidence that the new release works.

Developer publishes a versioned image through a registry to a VPS, then a proxy routes requests to the applicationdeveloperbuild · tag · pushregistryversioned imageUbuntu VPSpull · ComposeCaddypublic 80 / 443appprivate portpublishpullrequest

برای اینکه مسیر گم نشود، آن را از اول به چند مرز تقسیم می‌کنیم: ماشین توسعه image را build و push می‌کند؛ VPS همان نسخه را pull می‌کند؛ Compose سرویس‌ها را بالا می‌آورد؛ reverse proxy ورودی عمومی را می‌گیرد و app روی شبکهٔ داخلی پاسخ می‌دهد. هر خرابی را هم در همان مرزی که رخ داده دنبال می‌کنیم.

The arrows show image delivery followed by request routing: the developer builds, the registry stores, the VPS pulls, and Caddy is the application's public entry point.

در این فصل مسیر اصلی Ubuntu VPS است. اگر هنوز VPS نداری، Linux VM می‌تواند بیشتر مراحل Docker، SSH، Compose و proxy را بازتولید کند؛ اما DNS عمومی و HTTPS قابل‌اعتماد برای کاربران به domain واقعی و سروری نیاز دارد که از اینترنت reachable باشد.

Ubuntu on a VPS is our main path. A Linux VM can reproduce most Docker, SSH, Compose, and proxy steps; public DNS and publicly trusted HTTPS, however, require a real domain and a server reachable from the internet.

ورودی عمومی را به proxy بده؛ app لازم نیست مستقیم روی اینترنت باز باشدSend public traffic to the proxy, not directly to the app

فرض کن app داخل container روی 8080 گوش می‌دهد. ساده‌ترین راه این است که همان port را عمومی کنیم، اما معماری تمیزتر این است که فقط reverse proxy روی 80 و 443 دیده شود. proxy درخواست را روی شبکهٔ Docker به نام سرویس app و port داخلی می‌فرستد.

If the app listens on port 8080, must we expose it to the internet? Usually not. A reverse proxy provides an HTTP/HTTPS entry point and forwards requests over Docker networking to the app service and internal port.

Internet reaches published Caddy ports, then Caddy routes privately to the app containerInternetHTTP / HTTPSCaddypublished 80 / 443TLS · routingapp:8080Docker network only

این جداسازی چند مزیت دارد: مسیر HTTPS یک‌جا مدیریت می‌شود، app لازم نیست با جزئیات TLS درگیر باشد و پایگاه‌داده هم اصلاً port عمومی نمی‌گیرد. در این فصل Caddy را به‌عنوان نمونه نگه می‌داریم تا بحث proxy خودش تبدیل به یک دورهٔ جدا نشود.

Only 80/443 are published on the host; the second arrow is inside Docker networking and the app port is not public.

Caddy و app روی network مشترک‌اند؛ Caddy به app:8080 وصل می‌شود، نه localhost:8080. localhost در container یعنی خود همان container. پایگاه‌داده روی network جدا می‌ماند و port عمومی ندارد.

Caddy and the app share a network; Caddy connects to app:8080, not localhost:8080. Localhost inside a container means that same container. The database stays on a separate network with no public port.

proxy/Caddyfile · domain عمومی
app.example.com {
    reverse_proxy app:8080
}

با domain واقعی، رکورد A و در صورت استفاده AAAA باید به IP قابل‌دسترسی همین سرور اشاره کنند. اگر 80/443 از اینترنت به Caddy برسند، HTTPS خودکار می‌تواند گواهی عمومی بگیرد و تمدید کند؛ volume دادهٔ Caddy را پایدار نگه دار. DNS نادرست، port مسدود، AAAA قدیمی یا data غیرپایدار این مسیر را می‌شکند.

With a real domain, A and any used AAAA records must point to this reachable server. If internet traffic on 80/443 reaches Caddy, automatic HTTPS can obtain and renew a public certificate; keep Caddy's data volume persistent. Wrong DNS, blocked ports, a stale AAAA, or non-persistent data can break this path.

بدون domain یا دسترسی عمومی، Caddy را با :80 و IP یا شبکهٔ محلی VM به کار ببر. این HTTP تمرینی است، نه HTTPS عمومی؛ گواهی محلی را با گواهی مورداعتماد کاربران اشتباه نگیر.

Without a domain or public reachability, use Caddy on :80 with the VM IP or local network. This is practice HTTP, not public HTTPS; do not confuse a local certificate with one trusted by users.

Caddy and app share edge; app and database share internal backend; database has no published portedge networkCaddybackend · internalappdatabaseno host port

app عضو هر دو network است؛ Caddy فقط edge را دارد و DB فقط backend را. جداسازی دسترسی را محدود می‌کند، نه اینکه جای secret و مجوز را بگیرد.

The app joins both networks; Caddy only edge and the DB only backend. This narrows reachability but does not replace secrets or permissions.

فصل‌های قبلی را دوباره درس نمی‌دهیم؛ اینجا آن‌ها را روی سرور کنار هم می‌گذاریمCompose holds release configuration together

healthcheck، secret، volume، non-root و read-only را قبلاً یاد گرفته‌ایم. اینجا همان تصمیم‌ها را وارد تعریف deployment می‌کنیم: image نسخه‌دار، app غیرریشه، filesystem محدود، healthcheck، restart policy و فقط proxy با port عمومی. اگر پایگاه‌داده داریم، روی شبکهٔ داخلی و volume خودش می‌ماند.

We do not reteach healthchecks, secrets, volumes, and hardening; we apply them: a versioned image, non-root user, read-only filesystem with required writable paths, healthcheck, restart policy, and proxy as the only service with public ports. The DB stays on an internal network and persistent volume.

compose.yaml · نمونه؛ image و healthcheck را متناسب کن
services:
  proxy:
    image: caddy:2.11.4
    restart: unless-stopped
    ports: ["80:80", "443:443"]
    volumes:
      - ./proxy/Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks: [edge]
  app:
    image: ${APP_IMAGE:?Set APP_IMAGE to an explicit registry image and version}
    user: "10001:10001"
    read_only: true
    tmpfs: ["/tmp:rw,noexec,nosuid,size=16m,uid=10001,gid=10001"]
    restart: unless-stopped
    mem_limit: 384m
    cpus: 1.0
    pids_limit: 128
    expose: ["8080"]
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=2)"]
      interval: 20s
      timeout: 3s
      retries: 3
      start_period: 15s
    environment:
      DB_HOST: db
      DB_NAME: app
      DB_USER: app
      DB_PASSWORD_FILE: /run/secrets/db_password
    secrets: [db_password]
    depends_on:
      db:
        condition: service_healthy
    networks: [edge, backend]
  db:
    image: postgres:17
    restart: unless-stopped
    environment:
      POSTGRES_USER: app
      POSTGRES_DB: app
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets: [db_password]
    volumes: ["db_data:/var/lib/postgresql/data"]
    networks: [backend]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 20s
networks:
  edge: {}
  backend:
    internal: true
volumes:
  caddy_data: {}
  caddy_config: {}
  db_data: {}
secrets:
  db_password:
    file: ./secrets/db_password

هدف این نیست که compose.yaml را با هر گزینهٔ ممکن شلوغ کنیم. هر خط باید دلیل عملی داشته باشد و بتوانیم بعداً از روی docker compose config و ps بفهمیم سرور قرار است چه چیزی اجرا کند.

Only the proxy publishes ports; expose does not publish the app port on the host. The healthcheck assumes Python and a /health endpoint; verify the binary exists in your image. Memory, CPU, and PID values are examples, not universal settings; size them from observed use and Chapter 13.

برای تولید واقعی، tag image برنامه، Caddy و پایگاه‌داده را مطابق نسخهٔ آزموده‌شدهٔ پروژه pin کن؛ حتی tag نسخهٔ بزرگ مثل postgres:17 یا caddy:2 می‌تواند با انتشار جزئی جدید جابه‌جا شود. CI باید همان خروجی ساخته‌شده نام‌دار یا digest را برای VPS منتشر کند.

For production, pin the app, Caddy, and database images to the versions tested by the project; even a major tag such as postgres:17 or caddy:2 can move as patch releases appear. CI should publish the same named artifact or digest that the VPS deploys.

secret و داده چرخهٔ جدا دارندSecrets and data have separate lifecycles

secret را طبق فصل 09 اضافه کن؛ مقدار واقعی را در image، Git یا .env نگذار. تعویض image یعنی حذف db_data نیست. برای DB پشتیبان/restore جدا داشته باش و پیش از migration پرخطر پشتیبان قابل‌بازیابی بساز.

Add secrets using Chapter 09; do not put real values in the image, Git, or .env. Replacing the image does not mean deleting db_data. Plan DB backup/restore separately and make a restorable backup before a risky migration.

انتشار فقط pull و up نیست؛ باید نسخهٔ تازه را از بیرون هم ثابت کنیFollow a release from pull to HTTP response

روی VPS image reference را روی tag مشخصی مثل 1.4.0 می‌گذاریم. اول فایل نهایی Compose را بررسی می‌کنیم، بعد image را pull می‌کنیم و up -d می‌زنیم. تا اینجا فقط می‌دانیم Docker تلاش کرده نسخهٔ تازه را اجرا کند.

Create the deployment files on the VPS and set APP_IMAGE to the exact pushed tag. For a private registry, authenticate securely with a read-only token. Review rendered configuration, then pull and apply.

روی VPS، از پوشهٔ deployment
cd /opt/codenames-app
docker compose config
docker compose pull app
docker compose up -d
docker compose ps
docker compose logs --tail=80 app proxy

مرحلهٔ بعد مهم‌تر است: compose ps، health، لاگ و در نهایت یک درخواست HTTP از بیرون. انتشار زمانی تمام شده که همان مسیری که کاربر طی می‌کند جواب درست بدهد. فقط دیدن «Started» در خروجی Compose برای اعلام موفقیت کافی نیست.

Compose config catches YAML and interpolation errors early; review output for secrets too. Pull fetches the image and up reconciles state with the file; changing an image may recreate a container. Named volumes are not removed.

سه شاهد جدا بگیر: compose ps برای state/health، compose لاگ‌ها برای گفته‌های سرویس و HTTP بیرونی برای مسیر کامل. هیچ‌کدام به‌تنهایی دیگری را ثابت نمی‌کند.

Collect three separate kinds of evidence: compose ps for state/health, compose logs for service output, and external HTTP for the full path. None proves the others.

health، logs و درخواست
docker compose ps
docker inspect "$(docker compose ps -q app)" --format '{{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{end}}'
docker compose logs --since=10m app proxy
curl -fsS https://app.example.com/health

بدون domain از http://SERVER_IP/health و Caddy روی :80 استفاده کن؛ provider و میزبان firewall باید اجازه دهند. اگر health سالم ولی curl timeout است، DNS/firewall/port proxy را بسنج؛ برای 502 نام سرویس، network مشترک و port داخلی را بررسی کن.

Without a domain use http://SERVER_IP/health and Caddy on :80; provider and host firewalls must allow it. If health passes but curl times out, check DNS, firewalls, and proxy ports; for 502, inspect service name, shared network, and internal port.

A versioned image is pulled, applied by Compose, then checked through health, logs, and external HTTPpush :1.4.0registrypullVPScompose upreconcilehealth + logsserver evidenceHTTPoutside

ترتیب بررسی است، نه ضمانت بی‌وقفه‌بودن: خروجی ساخته‌شده، reconciliation، health و لاگ‌ها، سپس مسیر درخواست کاربر.

This is a verification sequence, not a zero-downtime guarantee: artifact, reconciliation, health and logs, then the user's request path.

Rollback نقشهٔ فرار نیست؛ بخشی از طراحی انتشار استIf the release is bad, rollback is part of deployment

فرض کن 1.4.0 بالا می‌آید اما unhealthy می‌شود یا پاسخ اشتباه می‌دهد. اگر نسخهٔ سالم قبلی 1.3.2 بوده، باید بتوانیم image reference را صریح به همان نسخه برگردانیم و دوباره apply کنیم. «شاید image قبلی هنوز در cache باشد» برنامهٔ rollback نیست.

Suppose 1.4.0 becomes unhealthy after an update. Preserve state, health, logs, and a real request. If 1.3.2 was the known-good version, explicitly restore its image reference; do not rely on an accidental cache. Pull and apply it, then verify version and response.

rollback به tag معلوم
# Edit .env so APP_IMAGE names :1.3.2
docker compose config
docker compose pull app
docker compose up -d
docker compose ps
docker compose logs --since=10m app
curl -fsS https://app.example.com/health

بعد از rollback همان بررسی‌های انتشار را دوباره انجام می‌دهیم: state، health، لاگ‌ها و HTTP بیرونی. اگر فقط container قبلی را بالا آوردیم ولی درخواست کاربر هنوز خطا می‌گیرد، rollback کامل نشده است.

Rolling back the image does not necessarily reverse a database migration. Schema changes need version compatibility and a backup/restore plan. Do not delete a volume as cleanup; distinguish a code failure from a data change.

Deploy known-good 1.3.2, try unhealthy 1.4.0, restore 1.3.2 and verify1.3.2known good1.4.0unhealthyrestore prior tagpull · up · verify

rollback یعنی نسخهٔ قبلی را آگاهانه انتخاب و health و درخواست بیرونی را دوباره بررسی کنیم؛ Up به‌تنهایی اثبات نیست.

Rollback means deliberately choosing the previous version and rechecking health and an external request; Up alone is not proof.

یک app container یعنی هنگام جایگزینی، جایی برای فرستادن ترافیک نداریمOne instance means no alternate target

اگر فقط یک نمونهٔ app داریم، هنگام جایگزینی نسخه ممکن است چند لحظه هیچ upstream آماده‌ای پشت proxy نباشد. healthcheck می‌تواند بگوید نمونهٔ تازه آماده شده یا نه، اما خودش نمونهٔ جایگزین نمی‌سازد. restart policy هم اگر فرایند بمیرد دوباره تلاش می‌کند؛ ترافیک را به container دیگری منتقل نمی‌کند.

In a single-container deployment, Compose may stop the old instance for a new image. Until the replacement is ready, Caddy has no ready upstream and some requests may fail. A short gap is not zero downtime.

پس روی معماری تک‌نمونه‌ای عبارت «zero downtime» را بی‌دلیل استفاده نمی‌کنیم. می‌توانیم وقفه را کم کنیم، اما برای بی‌وقفه‌بودن واقعی معمولاً به چند نمونه یا الگوی blue/green و یک نقطهٔ مسیریابی نیاز داریم. پروژهٔ نهایی همین موضوع را عملی‌تر می‌کند.

Reduce impact with predictable startup, a meaningful healthcheck, compatible migrations, and a suitable deployment window. Stronger availability requires multiple instances and traffic shifting: blue/green, rolling, or a load balancer. Project 3 covers the deeper implementation.

وقتی انتشار خراب شد، مسیر را از مرز درست شروع کنStart at the boundary that may have failed

نشانهSymptomشاهد بعدیNext evidenceمسیر بررسیInvestigation
pull: unauthorizedlogin, token scope, expiryاجازهٔ pull را بسنج؛ tag را جدا بررسی کن.Check pull permission; verify the tag separately.
manifest unknownAPP_IMAGE, repo, pushed tagsمسیر registry و tag را تطبیق بده.Compare registry path and tag.
port 80 allocatedsudo ss -lntpصاحب port را پیدا کن؛ کورکورانه متوقفش نکن.Identify the port owner; do not stop it blindly.
timeout / refusedDNS, provider and host firewall, listenersمسیر را از اینترنت تا میزبان دنبال کن.Trace the route from internet to host.
Caddy 502shared network, service DNS, internal port, app logsupstream شبکه را بسنج؛ localhost همان Caddy است.Check network upstream; localhost is Caddy itself.
app unhealthystate, health, logs, external curltag، تنظیمات، مسیر سرویس و startup؛ اگر لازم شد rollback.Check tag, config, endpoint, startup; roll back if needed.
secret missing / disk lowpath/permissions; df -h, docker system dfداده و فضا را بسنج؛ volume مهم را prune نکن.Verify data and space; do not prune important volumes.

یک خطای deployment ممکن است از registry، SSH، firewall، proxy، app یا storage بیاید. همهٔ سرویس‌ها را restart نکن. اول بپرس درخواست یا image دقیقاً تا کدام مرز جلو رفته است. اگر VPS اصلاً image را pull نکرده، لاگ app چیزی برای گفتن ندارد.

Ask “which boundary failed?” Registry, SSH, firewall, proxy, app, or data? A random restart can hide a symptom without revealing its cause.

Healthcheck آماده‌بودن را می‌سنجد؛ ظرفیت جایگزین را معماری باید بسازدReplacing one container can leave a short gap

وقتی Compose یک app تک‌نمونه را recreate می‌کند، ممکن است قبلی قبل از آماده‌شدن کامل نمونهٔ تازه کنار برود. این فاصله کوچک هم downtime است. healthcheck و restart policy اطلاعات و بازیابی فرایند می‌دهند، اما به‌تنهایی availability چندنمونه‌ای نمی‌سازند.

With only one app instance, Compose usually stops the old container and creates a new one. Requests may fail in between. Healthchecks and restart policies help detect and recover, but they do not route that traffic to another instance.

وعده‌ای که نباید بدهیمA promise we should not make

این مرز را از همین فصل روشن نگه می‌داریم تا در Project 3 معماری blue/green را واقعی و بدون ادعای ساختگی پیاده کنیم: نسخهٔ آزمایشی جدا بالا می‌آید، سالم‌بودنش بررسی می‌شود و بعد مسیر ترافیک تغییر می‌کند.

A single container cannot guarantee zero downtime during every replacement. We can minimize interruption, but true zero downtime generally requires redundancy and an architecture such as blue/green, multiple instances behind a load balancer, or a rolling rollout. Project 3 builds that deeper design.

قبل از deploy، خود سرور باید قابل‌اعتماد و قابل‌مدیریت باشدFirst, make the server manageable

یک VPS تازه فقط یک IP و راه ورود است. قبل از آوردن app، دسترسی SSH را با key مرتب می‌کنیم، سیستم را به‌روز می‌کنیم، Docker Engine را از مسیر رسمی نصب می‌کنیم و firewall را طوری می‌بندیم که فقط ورودی‌های واقعاً لازم باز باشند.

You have a fresh VPS, an IP address, and an initial account. Before shipping the app, we establish dependable access, install Docker from its official repository, and allow only the required inbound traffic.

SSH با کلید؛ private key روی دستگاه خودت می‌ماندSSH keys; keep the private key on your own machine

در این معماری معمولاً SSH، HTTP و HTTPS ورودی‌های عمومی‌اند؛ پایگاه‌داده نیست. همچنین Docker daemon را روی TCP عمومی باز نمی‌کنیم. دسترسی به daemon قدرت زیادی روی میزبان می‌دهد و باید همان‌قدر جدی گرفته شود.

Create a key on your laptop or use a protected existing one; install the public key on the VPS and keep the private key on your own machine. Never copy it into the project, image, Git, or server project directory. After confirming key-based login, you may restrict password login according to server policy; keep the current SSH session open until the new path is verified.

از دستگاه خودت
ssh -i ~/.ssh/id_ed25519 deploy@SERVER_IP

جایگزین‌کردن IP با مقدار VPS را فراموش نکن. اگر اتصال شکست خورد، ابتدا username، آدرس و نصب public key را بسنج؛ private key را به سرور کپی‌کردن راه‌حل نیست.

Replace the address with your VPS IP. If login fails, check the username, address, and installed public key first; copying the private key to the server is not the fix.

اگر provider هنگام ساخت VPS کلید را نصب نکرده، با حساب مدیریتی اولیه یک حساب deploy بساز و فقط public key را در authorized_keys بگذار. مسیر فایل کلید عمومی را با فایل خودت جایگزین کن.

If the provider did not install the key during VPS creation, use the initial administrative account to create deploy and place only the public key in authorized_keys. Replace the public-key path with your own file.

روی Ubuntu، با حساب مدیریتی اولیه
sudo adduser --disabled-password --gecos "" deploy
sudo install -d -o deploy -g deploy -m 0700 /home/deploy/.ssh
sudo install -o deploy -g deploy -m 0600 /path/to/deploy_key.pub /home/deploy/.ssh/authorized_keys

فایل .pub عمومی است و private key کنار آن نیست؛ پس از نصب، از نشست دیگری ورود deploy را آزمایش کن. مجوزهای سخت‌گیرانه روی پوشه و فایل باعث می‌شود SSH کلید را بپذیرد.

The .pub file is public; its private counterpart does not belong beside it. Test deploy login from a second session after installation. Restrictive directory and file permissions let SSH accept the key.

به‌روزرسانی Ubuntu و نصب Docker EngineUpdate Ubuntu and install Docker Engine

Docker نصب Ubuntu را از مخزن رسمی apt مستند می‌کند: کلید امضای مخزن، apt source و سپس Engine، CLI، Buildx و Compose plugin. جزئیات repository ممکن است با codename اوبونتو تغییر کند؛ صفحهٔ رسمی را برای نسخهٔ سرورت دنبال کن. convenience script را مسیر اصلی نصب production قرار نده.

Docker documents Ubuntu installation through its official apt repository: repository signing key, apt source, then Engine, CLI, Buildx, and Compose plugin. Repository details can vary with the Ubuntu codename; follow the official instructions for your server release. Do not make the convenience script your production installation path.

Ubuntu · نصب بسته‌ها (repository را طبق راهنمای رسمی ثبت کن)
sudo apt update
sudo apt upgrade
sudo apt install ca-certificates curl
# Configure Docker's official apt repository for this Ubuntu release.
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl status docker
sudo docker run hello-world

بخش ثبت کلید و apt source را عمداً به راهنمای رسمی واگذار می‌کنیم، چون باید codename درست انتخاب شود. hello-world ثابت می‌کند daemon توانسته image بگیرد و container اجرا کند؛ هنوز دربارهٔ برنامه یا مسیر عمومی چیزی نمی‌گوید.

Repository key and apt-source setup is deliberately delegated to the official guide so the correct codename is used. hello-world proves the daemon fetched an image and ran a container; it says nothing yet about the app or public traffic.

کاربر deploy و اختیار DockerThe deploy user and Docker authority

یک حساب جدا مثل deploy و SSH key بساز تا مالکیت فایل‌های انتشار روشن باشد. اما عضویت در گروه docker اختیار بسیار بالایی، عملاً نزدیک به root میزبان، می‌دهد؛ این گروه مرز امنیتی کم‌خطر نیست. فقط به کاربر کاملاً مورداعتماد بده، وگرنه Docker را با sudo اجرا کن. Docker daemon را با TCP عمومی و بدون محافظت هرگز در دسترس نگذار.

Create a separate account such as deploy with an SSH key so release-file ownership is clear. But membership in the docker group grants very powerful access, effectively close to host root; it is not a low-risk security boundary. Give it only to a fully trusted user; otherwise use Docker through sudo. Never expose the Docker daemon through unprotected public TCP.

فایروال را بررسی کن؛ خاموشش نکنReview the firewall; do not switch it off

ورودی عمومی این معماری معمولاً SSH برای مدیریت و 80/443 برای proxy است. درگاه پایگاه‌داده را عمومی نمی‌کنیم؛ app از شبکهٔ داخلی Docker به آن می‌رسد. VPS ممکن است فایروال ارائه‌دهنده هم داشته باشد، پس میزبان و provider را جدا بررسی کن. پیش از فعال‌کردن UFW، SSH را مجاز کن و نشست فعلی را باز نگه دار.

Public inbound access for this architecture is usually SSH for administration and ports 80/443 for the proxy. The database stays private; the app reaches it over an internal Docker network. A VPS may also have a provider firewall, so inspect host and provider controls separately. Allow SSH before enabling UFW and keep the current session open.

نمونهٔ UFW · پیش از enable، قواعد SSH را بازبینی کن
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status verbose
sudo ufw enable
نکتهٔ مهم دربارهٔ Docker و firewallImportant Docker firewall detail

app و DB را اصلاً با ports: publish نمی‌کنیم. Docker هشدار می‌دهد portهای منتشرشده می‌توانند با قواعد عادی UFW تداخل کنند یا از آن‌ها عبور کنند؛ پس فایروال تنها کنترل دسترسی نیست. bindingها، firewall ارائه‌دهنده و قواعد پشتیبانی‌شدهٔ میزبان را بررسی کن.

We do not publish the app or DB with ports:. Docker warns that published ports can interact with or bypass ordinary UFW rules; the firewall is not the only access control. Review port bindings, provider firewall, and supported host rules.

سرور جای build نیست؛ همان image آزموده‌شده را اجرا کنShip a release; do not turn the server into a build machine

کد روی ماشین توسعه یا CI build و test می‌شود، بعد image با tag مشخص به registry می‌رود. VPS همان خروجی ساخته‌شده را pull می‌کند. این کار باعث می‌شود چیزی که روی سرور اجرا می‌شود همان خروجی‌ای باشد که قبل‌تر بررسی کرده‌ایم، نه build تازه‌ای با شرایط ناشناخته روی سرور.

Build and test source on a development machine or CI, assign an explicit tag, push it, and let the VPS pull that artifact. Avoiding production builds on the VPS means the server needs no compiler, source tree, or build tools, and the tested image is the image that runs—making releases easier to reproduce and compare.

روی build machine یا CI
docker build -t registry.example.com/team/my-app:1.4.0 .
docker run --rm registry.example.com/team/my-app:1.4.0
docker push registry.example.com/team/my-app:1.4.0

وقتی build را از VPS بیرون نگه می‌داریم، سرور هم به source tree و build tools کمتری نیاز دارد و rollback واضح‌تر می‌شود: فقط image reference را به نسخهٔ شناخته‌شدهٔ قبلی برمی‌گردانیم.

Starting an image is not a substitute for testing its endpoint. If the VPS architecture differs from the build machine, build for the server platform or publish a multi-platform image. Pull an explicit tag such as 1.4.0; latest is a moving pointer and does not provide a traceable rollback. A digest can identify the exact artifact.

برای registry خصوصی، با token محدود به pull وارد شو. از ورود تعاملی یا --password-stdin استفاده کن؛ token را در history، Compose یا Git نگذار. فایل Docker تنظیمات کاربر ممکن است اعتبارنامه داشته باشد، پس مجوزش را محافظت کن.

For a private registry, authenticate with a pull-scoped token. Use interactive login or --password-stdin; do not put the token in history, Compose, or Git. The user's Docker config may contain credentials, so protect its permissions.

روی سرور هر چیز جای مشخص داشته باشدGive server files a clear home

برای مثال فایل‌های deployment را زیر /opt/codenames-app می‌گذاریم: Compose، تنظیمات proxy، فایل‌های غیرحساس، secretها با permission محدود و پوشهٔ پشتیبان. این اسم و ساختار قانون جهانی نیست؛ نکته این است که نفر بعد بتواند بدون حدس بفهمد هر چیز کجاست.

We use /opt/codenames-app/ as an example, not a universal rule. The important points are that deploy can manage release files, secrets are readable only by the accounts that need them, and backups are protected from accidental deletion.

یک چیدمان ممکن
/opt/codenames-app/
├── compose.yaml
├── .env                 # image/version; not secret values
├── proxy/Caddyfile
├── secrets/             # protected host files; never commit
└── backups/             # copy off-server too
ساخت مسیر با مالک انتشار
sudo install -d -o deploy -g deploy -m 0750 /opt/codenames-app
sudo install -d -o root -g deploy -m 0750 /opt/codenames-app/secrets
sudo install -d -o deploy -g deploy -m 0750 /opt/codenames-app/proxy /opt/codenames-app/backups

مالکیت فایل‌ها هم بخشی از deployment است. کاربر deploy باید بتواند فایل‌های لازم را به‌روزرسانی کند، اما secret یا پشتیبان نباید بی‌دلیل برای همه خواندنی باشند. «روی سرور هست» جایگزین طراحی permission نیست.

Pass real secrets using the Chapter 09 pattern, not in the image or .env. Persistent data has a separate lifecycle: an app update must not erase the database. Before risky changes, have a restorable backup; a directory named backups is not proof of a backup.

آزمایشگاه: این‌بار واقعاً برنامه را از لپ‌تاپ تا VPS دنبال کنLab: deploy the app to a VPS together

در این آزمایش image نسخه‌دار را push می‌کنیم، روی Ubuntu VPS یا VM pull می‌کنیم، Compose و Caddy را بالا می‌آوریم و فقط proxy را عمومی می‌گذاریم. بعد نسخهٔ دوم را منتشر می‌کنیم، health و پاسخ بیرونی را می‌سنجیم و یک نسخهٔ خراب را عمداً وارد می‌کنیم تا rollback را تمرین کنیم.

Move an image from a build machine through a registry to an Ubuntu VPS/VM, publish only Caddy, and deliberately deploy a broken version to practice rollback. A domain and public HTTPS are optional; without them, HTTP by IP is only a practice path.

۱. app آماده‌سازی1. Prepare the app

اگر domain واقعی داری، HTTPS را هم از مسیر رسمی Caddy آزمایش کن. اگر نداری، با IP و HTTP همان زنجیرهٔ آموزشی را انجام بده و صریح بنویس کدام بخش به DNS و certificate عمومی وابسته بوده است. چیزی را صرفاً برای کامل‌به‌نظررسیدن جعل نمی‌کنیم.

Create two files in a fresh directory. The app reports its version, becomes ready after eight seconds, and returns 503 from health in the broken build. This is not production software.

server.py · readiness و stdout log
import os
import time
from http.server import BaseHTTPRequestHandler, HTTPServer

VERSION = os.getenv("APP_VERSION", "dev")
BROKEN = os.getenv("BROKEN", "0") == "1"
STARTED = time.monotonic()

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        ready = time.monotonic() - STARTED >= 8
        if self.path == "/health":
            ok = ready and not BROKEN
            self.send_response(200 if ok else 503)
            self.end_headers()
            self.wfile.write(b"healthy\n" if ok else b"not ready\n")
            print(f"health version={VERSION} ready={ready} ok={ok}", flush=True)
            return
        self.send_response(200)
        self.end_headers()
        self.wfile.write(f"version={VERSION} ready={ready}\n".encode())
        print(f"request path={self.path} version={VERSION}", flush=True)

HTTPServer(("0.0.0.0", 8080), Handler).serve_forever()

برنامه log را به stdout می‌نویسد. درخواست فوری health ممکن است 503 باشد؛ پس از warm-up دوباره تست کن. نسخهٔ خراب پس از آن هم 503 می‌دهد.

The app writes logs to stdout. An immediate health request may return 503; test again after warm-up. The broken version keeps returning 503.

Dockerfile · non-root و healthcheck
FROM python:3.12-alpine
ARG APP_VERSION=dev
ARG BROKEN=0
RUN addgroup -S app && adduser -S -G app app && mkdir -p /app && chown app:app /app
WORKDIR /app
COPY --chown=root:root server.py /app/server.py
ENV APP_VERSION=${APP_VERSION} BROKEN=${BROKEN}
USER app:app
EXPOSE 8080
HEALTHCHECK --interval=5s --timeout=2s --retries=3 --start-period=2s \
  CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=1)"]
CMD ["python", "-u", "/app/server.py"]

تأخیر ۸ثانیه‌ای از start-period بیشتر است تا حالت‌ها دیده شوند. interval پنج‌ثانیه‌ای فقط آزمایش را کوتاه می‌کند، نه اینکه production پیشنهاد شود. healthcheck به Python و همین مسیر سرویس وابسته است. در کار واقعی base image را به نسخهٔ patch/digest بازبینی‌شده pin کن.

The eight-second delay exceeds start-period so states are visible. The five-second interval only shortens this experiment; it is not a production recommendation. The healthcheck depends on Python and this endpoint. In real work, pin a reviewed base-image patch/digest.

۲. build، تست و push2. Build, test, and push

در Bash/WSL مسیر registry خودت را جایگزین کن. سه tag جدا می‌سازیم: دو release سالم و یک release خراب. برای registry خصوصی از ورود امن استفاده کن و token را در history ذخیره نکن.

In Bash/WSL, replace the registry path with yours. Create three separate tags: two working releases and one broken release. Use secure login for a private registry and keep the token out of history.

ماشین build · IMAGE را تغییر بده
export IMAGE=registry.example.com/account/vps-lab
docker login registry.example.com
docker build --build-arg APP_VERSION=1.0.0 --build-arg BROKEN=0 -t "$IMAGE:1.0.0" .
docker run --rm -d --name vps-lab-local -p 127.0.0.1:18080:8080 "$IMAGE:1.0.0"
sleep 10
curl -i http://127.0.0.1:18080/health
docker logs vps-lab-local
docker rm -f vps-lab-local
docker build --build-arg APP_VERSION=1.1.0 --build-arg BROKEN=0 -t "$IMAGE:1.1.0" .
docker build --build-arg APP_VERSION=1.2.0 --build-arg BROKEN=1 -t "$IMAGE:1.2.0" .
docker push "$IMAGE:1.0.0"
docker push "$IMAGE:1.1.0"
docker push "$IMAGE:1.2.0"

اگر VPS با ARM و build میزبان با x86 است، برای معماری سرور یا multi-platform بساز؛ pull موفق، سازگاری executable را ثابت نمی‌کند.

If the VPS is ARM and the build host is x86, build for the server architecture or publish multi-platform; a successful pull does not prove executable compatibility.

۳. VPS و Compose3. VPS and Compose

با SSH key وارد Ubuntu شو؛ سیستم و Docker Engine را از دستور رسمی به‌روز/نصب کن و hello-world را بررسی کن. حساب deploy را بساز و آگاهانه انتخاب کن sudo یا docker group؛ گروه اختیار نزدیک به root دارد. Provider firewall و UFW را برای SSH و پورت 80 تنظیم کن. private key را منتقل نکن و daemon TCP را باز نگذار.

Connect to Ubuntu using an SSH key, update the system, install Docker Engine from the official guide, and verify hello-world. Create deploy and consciously choose sudo or docker group; that group is near-root authority. Configure provider firewall and UFW for SSH and port 80. Do not transfer the private key or expose daemon TCP.

در /opt/codenames-app فایل‌های Caddy و Compose بساز. این Caddyfile برای HTTP آزمایشی بدون domain است؛ برای domain واقعی باید DNS، portهای 80/443 و persistent Caddy data برقرار باشند.

Create Caddy and Compose files under /opt/codenames-app. This Caddyfile is practice HTTP without a domain; a real domain requires correct DNS, ports 80/443, and persistent Caddy data.

Caddyfile · HTTP آزمایشی
:80 {
    reverse_proxy app:8080
}
compose.yaml · فقط proxy عمومی
services:
  proxy:
    image: caddy:2.11.4
    restart: unless-stopped
    ports: ["80:80"]
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks: [edge]
  app:
    image: ${APP_IMAGE:?Set APP_IMAGE to an explicit tag}
    restart: unless-stopped
    read_only: true
    tmpfs: ["/tmp:rw,noexec,nosuid,size=16m"]
    mem_limit: 128m
    cpus: 0.50
    pids_limit: 64
    expose: ["8080"]
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=1)"]
      interval: 5s
      timeout: 2s
      retries: 3
      start_period: 2s
    networks: [edge]
networks:
  edge: {}
volumes:
  caddy_data: {}
  caddy_config: {}

app روی میزبان publish نشده و فقط proxy عمومی است. production image را با tag/digest آزمایش‌شده pin کن، user غیرریشه و secret فصل 09 را اضافه کن. limitهای بالا کوچک و مخصوص آزمایش‌اند. Caddy هم در چرخهٔ نگهداری به نسخهٔ موردبازبینی به‌روز شود.

The app is not published on the host; only the proxy is public. Pin a tested production image tag/digest and add the non-root user and Chapter 09 secret. These small limits are for the lab. Keep Caddy updated through a reviewed maintenance process.

۴. deploy نسخهٔ اول و دیدن warm-up4. Deploy release one and observe warm-up

روی VPS، .env را فقط با مسیر image پر کن، نه token. Registry خصوصی را با روش امن login کن، سپس health را فوری و بعد از ۱۰ ثانیه بسنج.

On the VPS, put only the image reference in .env, not a token. Authenticate securely to a private registry, then check health immediately and after ten seconds.

VPS · نسخهٔ 1.0.0
cd /opt/codenames-app
echo 'APP_IMAGE=registry.example.com/account/vps-lab:1.0.0' > .env
docker compose config
docker compose pull
docker compose up -d
docker compose ps
curl -i http://SERVER_IP/health
sleep 10
curl -i http://SERVER_IP/health
docker compose logs --since=2m app proxy

503 فوری طبیعی است؛ بعد باید 200 و health سبز شود. curl، health و stdout لاگ‌ها شواهد متفاوت‌اند. timeout به firewall/port اشاره می‌کند؛ 502 به اتصال proxy/upstream.

An immediate 503 is expected; it should later return 200 and show healthy. Curl, health, and stdout logs are distinct evidence. Timeout points to firewall/ports; 502 points to proxy/upstream connectivity.

۵. نسخهٔ دوم، شکست و rollback5. Second release, failure, and rollback

APP_IMAGE را به 1.1.0 تغییر بده، pull/up کن و پاسخ را ثبت کن. سپس 1.2.0 را deploy کن؛ پس از warm-up احتمالاً container running ولی unhealthy است. log و HTTP را جمع کن و صریحاً به 1.1.0 برگرد، pull/up کن و recovery را ثابت کن.

Change APP_IMAGE to 1.1.0, pull/apply, and record the response. Then deploy 1.2.0; after warm-up the container should be running but unhealthy. Collect logs and HTTP evidence, explicitly restore 1.1.0, pull/apply, and prove recovery.

اجرای مرحله‌ای؛ APP_IMAGE را طبق توضیح عوض کن
# Set APP_IMAGE to :1.1.0
docker compose pull app
docker compose up -d
docker compose ps
curl -fsS http://SERVER_IP/
# Set APP_IMAGE to :1.2.0, then:
docker compose pull app
docker compose up -d
sleep 12
docker compose ps
docker inspect "$(docker compose ps -q app)" --format '{{.State.Status}} {{.State.Health.Status}}'
docker compose logs --since=5m app
curl -i http://SERVER_IP/health
# Restore APP_IMAGE to :1.1.0, then:
docker compose pull app
docker compose up -d
docker compose ps
curl -fsS http://SERVER_IP/health
curl -fsS http://SERVER_IP/
docker compose logs --since=5m app

recovery یعنی tag درست، health سالم، مسیر سرویس با نسخهٔ موردانتظار و logهای تازهٔ قابل‌قبول؛ فقط Up بودن کافی نیست. در گزارش symptom، مدرک، diagnosis، repair و بررسی را ثبت کن؛ secret/private key را هرگز ننویس.

Recovery means the right tag, healthy status, expected endpoint version, and acceptable recent logs; Up alone is insufficient. Record symptom, evidence, diagnosis, repair, and verification; never include secrets or private keys.

۶. HTTPS اختیاری و پاک‌سازی6. Optional HTTPS and cleanup

با domain واقعی DNS را به VPS ببر، Caddyfile را به نام domain تغییر بده و 80/443 را در هر دو firewall مجاز کن؛ پس از صدور، از client بیرونی HTTPS را تست کن. بدون domain این گام را انجام‌نشده ثبت کن. پس از جمع‌کردن مدرک، فقط در VPS/VM اختصاصی lab، docker compose down اجرا کن. named volumeها باقی می‌مانند؛ بررسی‌نشده یا مهم را حذف نکن و prune عمومی نزن.

With a real domain, point DNS to the VPS, use that domain in Caddyfile, allow 80/443 in both firewalls, and test HTTPS externally after issuance. Without a domain, mark this step not done. After collecting evidence, run docker compose down only on a lab-dedicated VPS/VM. Named volumes remain; do not delete unreviewed/important data or run global prune.

انتشار وقتی تمام است که کاربر دوباره همان مسیر را با موفقیت طی کندA deployment ends when we verify the path again

این فصل image را از registry تا VPS آورد، proxy را جلوی app گذاشت و update و rollback را با همان شواهد فصل‌های قبلی سنجید. نکتهٔ اصلی خود فرمان‌ها نیست: هر انتشار یک مسیر قابل‌ردگیری دارد و باید از ابتدا تا مسیر سرویس عمومی دوباره بررسی شود.

Push a versioned image, pull it on the server, and reconcile with Compose; public traffic reaches only the proxy. Check state, health, logs, and HTTP separately. If a release fails, explicitly restore the prior tag and repeat verification. Do not call a single-container replacement zero downtime.

مرجع سریعQuick reference

هدفGoalکارActionمدرکEvidence
publishdocker build -t IMAGE:VERSION .; docker push IMAGE:VERSIONخروجی ساخته‌شده نسخه‌دار؛ نه سلامت production.Versioned artifact, not production health.
render configdocker compose configYAML/interpolation؛ خروجی را برای secret بازبینی کن.YAML/interpolation; review for secrets.
updatedocker compose pull app; docker compose up -dimage دریافت و سرویس reconcile می‌شود.Image is fetched and service reconciled.
state/healthdocker compose psstate و healthcheck تنظیم‌شده؛ نه مسیر بیرونی کامل.State and configured healthcheck, not full external path.
logsdocker compose logs --since=10m app proxyخروجی سرویس‌ها در بازهٔ زمانی.Service output in the selected time window.
external requestcurl -fsS https://DOMAIN/healthمسیر client تا پاسخ domain.Client-to-domain request and response.
rollbacktag قبلی را صریح انتخاب، pull، up و verify کن.Explicitly restore prior tag, pull, apply, verify.خروجی ساخته‌شده معلوم؛ سازگاری داده جدا سنجیده شود.Known artifact; assess data compatibility separately.

برای Ubuntu به راهنمای رسمی Docker Engine و برای مجوزها به راهنمای پس از نصب رجوع کن. رفتار جاری را در Compose pull و Compose up ببین.

For Ubuntu, consult the official Docker Engine guide and for permissions the post-install guide. See current behavior in Compose pull and Compose up.

برای proxy بخوان Caddy reverse_proxy، شرط‌های HTTPS خودکار و اجرای Caddy با Compose. هشدار firewall مربوط به portهای منتشرشده در مستند Docker است.

For the proxy, read Caddy reverse_proxy, automatic HTTPS requirements, and its Compose guide. Docker's warning about published ports is in its firewall documentation.

۱۸ تمرین؛ این‌بار هر سؤال بخشی از مسیر انتشار است18 release and incident exercises

۱. مسیر image1. Image path

در هر تمرین اول مشخص کن مشکل در کدام مرز است: registry، سرور، firewall، proxy، app یا data. بعد فقط ابزاری را انتخاب کن که همان مرز را بررسی می‌کند. هدف حفظ‌کردن فرمان‌های deployment نیست؛ باید بتوانی مسیر درخواست و مسیر image را جدا از هم دنبال کنی.

The image is local and the VPS cannot see it; order the path from build to execution.

مسیر را از build تا کاربر دنبال کن · Trace the path

build/tag → registry push → VPS pull → Compose اجرا. image محلی هنوز منتقل نشده است.

Build/tag → registry push → VPS pull → Compose run. A local image has not been delivered.

۲. Running یعنی چه؟2. What does Running prove?

Compose می‌گوید app در حال اجراست، ولی curl بیرونی timeout می‌شود. چه چیزی ثابت نیست؟

Compose says the app is running, but external curl times out. What remains unproved?

Running فقط یک مرز را ثابت می‌کند · Running proves one boundary

فقط state container معلوم است؛ health، DNS، proxy port و firewall ثابت نشده‌اند. آن‌ها را جدا بسنج.

Only container state is known; health, DNS, proxy port, and firewall are not. Test them separately.

۳. فقط latest3. Latest only

ticket می‌گوید latest را deploy کن. دو پرسش rollback قبل از اقدام چیست؟

A ticket says to deploy latest. What two rollback questions do you ask?

قبل از latest، راه برگشت را معلوم کن · Plan rollback first

کدام خروجی ساخته‌شده آزمایش شده و کدام نسخهٔ سالم را برمی‌گردانیم؟ tag متحرک جواب پایدار نیست؛ release tag/digest و نسخهٔ قبلی را ثبت کن.

Which artifact was tested, and which known-good version will we restore? A moving tag is not stable; record a release tag/digest and prior version.

۴. registry خصوصی4. Private registry

pull خطای unauthorized می‌دهد اما image مسیر درست است؛ چه مجوزی لازم داری؟

Pull returns unauthorized although the image path is right; what permission do you need?

نام درست است؛ حالا مجوز را بررسی کن · Check registry access

اعتبارنامه و expiry را بسنج و token جداگانهٔ read/pull بساز. امن login کن، فایل Docker تنظیمات را محافظت کن و token را در history یا Compose نگذار.

Check credentials and expiry; create a separate read/pull token. Authenticate securely, protect Docker config, and keep the token out of history and Compose.

۵. پورت DB5. DB port

DB روی میزبان:5432 منتشر شده اما فقط appهای Compose نیاز دارند. اصلاح؟

DB is published on host port 5432 but only Compose apps need it. Fix?

پایگاه‌داده را از اینترنت جمع کن · Keep the DB private

mapping عمومی را بردار و app/DB را به network داخلی مشترک وصل کن. راه مدیریت موقت را جدا و محدود بساز؛ DB را دائماً اینترنتی نکن.

Remove public mapping and attach app/DB to an internal network. Design temporary admin access separately; do not leave the DB public.

۶. خطای 5026. A 502

Caddy به localhost:8080 می‌فرستد و app سالم است، ولی 502 می‌بینی. چرا؟

Caddy forwards to localhost:8080; app is healthy but returns 502. Why?

localhost از داخل proxy جای دیگری است · Follow the network scope

localhost داخل Caddy یعنی خود Caddy. network مشترک بساز و به نام سرویس app:8080 بفرست. health اپ ارتباط بین containerها را ثابت نمی‌کند.

Localhost inside Caddy means Caddy itself. Share a network and route to app:8080. App health does not prove inter-container connectivity.

۷. SSH پس از UFW7. SSH after UFW

فعال‌کردن UFW دسترسی SSH جدید را قطع کرده؛ کدام دو لایه را از کنسول بررسی می‌کنی؟

Enabling UFW blocked new SSH; which two layers do you inspect from the console?

دسترسی SSH را قبل از ادامه نجات بده · Recover access safely

میزبان firewall و firewall/security group ارائه‌دهنده را جدا ببین و از کنسول provider بازیابی کن. در آینده OpenSSH را پیش از enable مجاز و نشست دوم را آزمایش کن.

Inspect host and provider firewalls separately and recover through the provider console. Next time allow OpenSSH before enabling and test a second session.

۸. port 80 اشغال8. Port 80 occupied

Caddy address already in use می‌دهد. چرا نباید هر فرایند روی 80 را kill کرد؟

Caddy reports address already in use. Why not kill any process on port 80?

اول مالک پورت را پیدا کن · Identify the listener first

ممکن است وب‌سرور لازم باشد. با sudo ss -lntp صاحب port را بیاب، نقش را روشن کن و بعد تغییر کنترل‌شده بده.

It may be a required web server. Find the listener with sudo ss -lntp, understand its role, then change it deliberately.

۹. replica و zero-downtime9. Replicas and zero downtime

آیا replicas: 2 در Compose تک‌میزبانه به‌تنهایی update بی‌وقفه را تضمین می‌کند؟

Do replicas: 2 in single-host Compose alone guarantee zero-downtime updates?

دو replica به‌تنهایی deployment strategy نیست · Replicas are not a rollout plan

نه؛ rollout پشتیبانی‌شده، چند backend، traffic routing، readiness و سازگاری داده لازم است. تعداد replica به‌تنهایی failover نیست.

No; supported rollout, multiple backends, traffic routing, readiness, and data compatibility are required. Replica count alone is not failover.

۱۰. health سالم، بیرون timeout10. Healthy, external timeout

Compose می‌گوید healthy، ولی بیرون timeout است. سه شاهد بعدی؟

Compose says healthy, but external traffic times out. What three checks come next?

از بیرون به داخل مرزها را بررسی کن · Trace external reachability

DNS A/AAAA، firewall ارائه‌دهنده و UFW، و listener/published port پروکسی. از VPS و client بیرونی جدا curl بزن؛ health داخلی مسیر اینترنت را نیازموده.

Check DNS A/AAAA, provider/UFW rules, and proxy listener/published ports. Curl from VPS and external client separately; internal health did not test internet routing.

۱۱. manifest unknown11. Manifest unknown

registry tag 1.5.0 را پیدا نمی‌کند؛ پیش از تغییر کد چه می‌سنجی؟

The registry cannot find tag 1.5.0; what do you inspect before changing code?

قبل از کد، artifact را بررسی کن · Verify the artifact first

APP_IMAGE، account/repo، registry مسیر سرویس و tagهای واقعاً pushشده را تطبیق بده. این مرز انتشار خروجی ساخته‌شده است، نه لزوماً runtime app.

Compare APP_IMAGE, account/repository, registry endpoint, and tags actually pushed. This is the artifact publication boundary, not necessarily app runtime.

۱۲. مسیر سرویس اصلی خراب12. Main route failing

healthcheck روی /health موفق است اما login برای همه 500 می‌دهد. آیا app برای کاربر سالم است؟

The /health check passes but login returns 500 for everyone. Is the app healthy for users?

healthcheck تمام رفتار برنامه نیست · Health is a limited promise

تنها آزمون تنظیم‌شده موفق است و احتمالاً کم‌معناست. لاگ‌ها، dependency و مسیر اصلی را بررسی؛ signal را بهتر کن و اگر release مقصر است rollback کن.

Only the configured test passes and it may be too weak. Inspect logs, dependencies, and the main route; improve the signal and roll back if the release caused it.

۱۳. نسخهٔ قبلی پیدا نمی‌شود13. Previous tag unavailable

پس از reboot، VPS نمی‌تواند image قبلی را pull کند. چه نقصی آشکار شده؟

After reboot, the VPS cannot pull the old image. What process flaw is exposed?

rollback به نسخهٔ قابل‌دسترسی نیاز دارد · Rollback needs a retrievable version

tag متحرک تنها استفاده شده یا خروجی ساخته‌شده قبلی حذف شده. tag/digest قبلی را در registry نگه دار؛ image cacheشدهٔ احتمالی rollback plan نیست.

Only a moving tag was used or the prior artifact was deleted. Retain the prior tag/digest in the registry; a possibly cached image is not a rollback plan.

۱۴. secret file غایب14. Secret file missing

Compose فایل secrets/db_password را نمی‌خواند. چه چیزی را بررسی می‌کنی، بدون قراردادن secret در image؟

Compose cannot read secrets/db_password. What do you inspect without putting it in the image?

مسیر و permission فایل secret را بررسی کن · Check the secret boundary

مسیر نسبت به Compose file، وجود فایل، مالکیت و مجوز میزبان و تعریف secret فصل 09 را بسنج. محتوا را در terminal یا لاگ‌ها چاپ نکن.

Check the path relative to Compose, file existence, host ownership/permissions, and Chapter 09 declaration. Never print contents into terminal or logs.

۱۵. دیسک کم است15. Low disk

pull می‌گوید no space left on device؛ چرا system prune -a --volumes پاسخ پیش‌فرض نیست؟

Pull reports no space left on device; why is system prune -a --volumes not the default fix?

اول مصرف‌کنندهٔ فضا را پیدا کن · Diagnose before pruning

با df -h، df -i و docker system df مصرف‌کننده را بیاب. prune می‌تواند volume مهم را پاک کند؛ بپرس چه داده‌ای آنجاست و فقط هدف امن شناخته‌شده را حذف کن.

Use df -h, df -i, and docker system df to identify the consumer. Prune can remove important volumes; ask what data is there and remove only a known safe target.

۱۶. rollback و schema16. Rollback and schema

پس از rollback برنامهٔ قدیمی با schema تازه ناسازگار است. چه چیزی باید migration plan می‌داشت؟

After rollback, the old app is incompatible with the new schema. What should the migration plan include?

rollback برنامه بدون rollback داده کافی نیست · Schema compatibility matters

سازگاری موقت دو نسخه، ترتیب migration، پشتیبان قابل restore و مسیر بازیابی داده. rollback image کد را عوض می‌کند، نه schema را.

Temporary compatibility, migration order, restorable backup, and data recovery. Image rollback changes code, not schema.

۱۷. AAAA قدیمی17. Stale AAAA

A درست است اما Caddy گواهی نمی‌گیرد؛ AAAA به IP قدیمی اشاره دارد. چرا مهم است؟

A is correct but Caddy cannot issue a certificate; AAAA points to an old IP. Why does it matter?

DNS دو خانواده دارد · Check A and AAAA

client یا CA ممکن است IPv6 را انتخاب کند و به سرور دیگری برسد. هر دو رکورد عمومی را بررسی و AAAA را فقط با IPv6 درست نگه دار.

A client or CA may choose IPv6 and reach another server. Check both public records and keep AAAA only when IPv6 is correctly routed.

۱۸. یادداشت رخداد18. Incident note

نسخهٔ 2.0.0 running ولی unhealthy است و لاگ‌ها خطای DB دارند. symptom، مدرک، diagnosis، repair و بررسی را بنویس.

Version 2.0.0 is running but unhealthy, with DB errors in logs. Write symptom, evidence, diagnosis, repair, and verification.

رخداد را با مدرک ببند · Close the incident with evidence

نشانه: آماده‌نبودن release. مدرک: state running، health ناموفق و خطای اتصال DB. علت محتمل secret، network یا تنظیمات است، نه هنوز قطعی. مقایسه کن و در صورت اثر بر کاربر rollback کن. تأیید: health سالم، لاگ‌ها تازه بدون خطا و مسیر سرویس با نسخهٔ موردانتظار.

Symptom: release not ready. Evidence: running state, failed health, DB connection error. A secret, network, or config issue is plausible, not yet certain. Compare configurations and roll back if users are affected. Verify healthy status, clean recent logs, and the expected endpoint version.

در نمونهٔ Compose، depends_on.condition: service_healthy منتظر healthcheck دیتابیس می‌ماند و فقط ترتیب آغاز را به readiness وصل می‌کند؛ آماده‌بودن DB هنوز سلامت همهٔ قابلیت‌های app را تضمین نمی‌کند. تعریف جاری این شرط را در مرجع رسمی سرویس‌های Compose ببین.

In the Compose example, depends_on.condition: service_healthy waits for the database healthcheck and ties startup order to readiness; a ready DB still does not guarantee every app feature works. See the current official Compose services reference.