زیرساخت: رمزهای اجباری از .env + لایه تولید با TLS + healthcheck و مسیر سئوی Traefik + پشتیبانگیری
- docker-compose.yml همه مقادیر محرمانه را با ${VAR:?} از .env میخواند — بدون .env استک بالا نمیآید
- docker-compose.prod.yml: ورودی 443 + ریدایرکت HTTP به HTTPS + گواهی Let's Encrypt و قواعد Host برای همه روترها
- healthcheck بکاند (api/health) و فرانت (wget) + سوار کردن frontend/dist در بکاند برای تزریق متا + روتر سئو (robots/sitemap/blog/projects) با اولویت 50
- سرویس mc با پروفایل tools + scripts/backup.sh (pg_dump + آینه MinIO با نگهداری ۱۴ روزه)
- .env.example ریشه + بروزرسانی READMEها
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# کپی کنید: cp .env.example .env — و همه مقادیر change-me را عوض کنید.
|
||||
# docker compose بهطور خودکار این فایل را میخواند؛ بدون آن استک بالا نمیآید.
|
||||
|
||||
# --- رمزهای اصلی (اجباری) ---
|
||||
# تولید کلید تصادفی: python -c "import secrets; print(secrets.token_urlsafe(48))"
|
||||
SECRET_KEY=change-me-to-a-long-random-string
|
||||
POSTGRES_PASSWORD=change-me-db-password
|
||||
MINIO_ROOT_USER=aramesh
|
||||
MINIO_ROOT_PASSWORD=change-me-minio-password
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=change-me-admin-password
|
||||
|
||||
# --- اختیاری ---
|
||||
# پورت ورود Traefik در توسعه (پیشفرض 5173)
|
||||
TRAEFIK_HTTP_PORT=5173
|
||||
# نشانی عمومی سایت برای sitemap و آدرس مطلق OG — در تولید مثل https://aramesh.example
|
||||
SITE_URL=
|
||||
# آدرسهای مجاز CORS (لیست JSON) — پیشفرض فقط لوکالهاست
|
||||
# CORS_ORIGINS=["http://localhost:5173"]
|
||||
|
||||
# --- فقط برای docker-compose.prod.yml (حالت تولید با TLS) ---
|
||||
# DOMAIN=aramesh.example
|
||||
# ACME_EMAIL=admin@aramesh.example
|
||||
@@ -9,6 +9,9 @@ env/
|
||||
# Environment
|
||||
.env
|
||||
|
||||
# پشتیبانگیری محلی
|
||||
backups/
|
||||
|
||||
# Node / Vite
|
||||
node_modules/
|
||||
dist/
|
||||
|
||||
@@ -28,16 +28,20 @@ Traefik (پورت ۵۱۷۳ — تنها نقطه ورود)
|
||||
│ ├── Dockerfile # بیلد Node + سرو استاتیک با Caddy
|
||||
│ └── Caddyfile # فقط سرو فایلهای استاتیک (مسیریابی روی Traefik است)
|
||||
├── backend/ # بکاند FastAPI
|
||||
│ ├── app/ # کد منبع (آپلود تصویر: app/api/admin.py)
|
||||
│ ├── app/ # کد منبع (آپلود تصویر: app/api/admin.py — سئوی سرور: app/seo_pages.py)
|
||||
│ ├── tests/ # تستهای API (pytest)
|
||||
│ ├── requirements.txt
|
||||
│ ├── Dockerfile
|
||||
│ └── .env.example
|
||||
└── docker-compose.yml # اجرای کامل با یک دستور
|
||||
├── scripts/backup.sh # پشتیبانگیری دیتابیس و تصاویر
|
||||
├── docker-compose.yml # اجرای کامل با یک دستور
|
||||
└── docker-compose.prod.yml # لایه تولید: TLS با Let's Encrypt
|
||||
```
|
||||
|
||||
## اجرا با Docker (سادهترین راه)
|
||||
|
||||
```bash
|
||||
cp .env.example .env # مقادیر change-me را عوض کنید — بدون .env استک بالا نمیآید
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
@@ -49,6 +53,18 @@ docker compose up --build
|
||||
ارتقای نسخه دادههای فعلی را از بین نمیبرد
|
||||
- در اولین اجرا، سرویس `minio-setup` باکت `aramesh-media` را میسازد و خواندن عمومی را فعال میکند
|
||||
|
||||
## انتشار واقعی (TLS)
|
||||
|
||||
برای بالا آوردن با HTTPS روی دامنه واقعی، لایه `docker-compose.prod.yml` را هم سوار کنید:
|
||||
|
||||
```bash
|
||||
# در .env: DOMAIN=example.com و ACME_EMAIL=you@example.com
|
||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
- Traefik گواهی Let's Encrypt میگیرد و تمدید میکند؛ همه درخواستهای HTTP به HTTPS هدایت میشوند
|
||||
- در این حالت `SITE_URL=https://example.com` را هم در `.env` بگذارید تا sitemap و آدرسهای OG مطلق شوند
|
||||
|
||||
## اجرای محلی (توسعه)
|
||||
|
||||
### بکاند
|
||||
@@ -83,13 +99,17 @@ npm run dev
|
||||
- **فرم تماس** `POST /api/contact` و **خبرنامه** `POST /api/newsletter`
|
||||
- **پنل مدیریت** با JWT: ورود `POST /api/auth/login` (پیشفرض: `admin` / `aramesh123`)
|
||||
و سپس CRUD کامل همه محتواها در `/api/admin/...`
|
||||
- **تغییر رمز** `POST /api/auth/change-password` — از نوار مدیریت پایین سایت (دکمه «رمز») هم در دسترس است
|
||||
- **محدودسازی ورود**: بعد از ۵ ورود ناموفق در ۱۵ دقیقه از یک IP، درخواست با 429 رد میشود
|
||||
- **آپلود تصویر** `POST /api/admin/upload` (فقط توکن ادمین، حداکثر ۱۰MB) — فایل در MinIO
|
||||
ذخیره و URL نسبی `/media/...` برمیگردد
|
||||
- **ویرایش جزئی** `PATCH /api/admin/{resource}/{id}` — فقط فیلدهای ارسالشده عوض میشوند
|
||||
- **ترتیببندی** `POST /api/admin/{resource}/reorder` — با `{"ids": [...]}`
|
||||
- همه محتوا در یک درخواست: `GET /api/site`
|
||||
- همه محتوا در یک درخواست: `GET /api/site` (مقالههای پیشنویس در آن نمیآیند)
|
||||
- **تستها**: `cd backend && pip install -r requirements-dev.txt && pytest tests`
|
||||
|
||||
> در محیط واقعی حتماً `SECRET_KEY`، `ADMIN_PASSWORD` و `MINIO_ROOT_PASSWORD` را تغییر دهید.
|
||||
> در محیط واقعی حتماً `SECRET_KEY`، `ADMIN_PASSWORD` و `MINIO_ROOT_PASSWORD` را تغییر دهید
|
||||
> (از `.env`) و بعد از اولین ورود، رمز را از نوار مدیریت عوض کنید.
|
||||
|
||||
## ویرایش زنده سایت (دوقلوی دیجیتال)
|
||||
|
||||
@@ -119,6 +139,11 @@ npm run dev
|
||||
- راستبهچپ کامل با فونت و تم سایت
|
||||
- ذخیره خودکار: هر مکث کوتاه در تایپ، تغییرها را ذخیره میکند؛ بستن کشو هم ذخیره میکند
|
||||
|
||||
**پیشنویس و انتشار**: مقاله تازه همیشه **پیشنویس** شروع میشود — در سایت عمومی دیده
|
||||
نمیشود و تا انتشار، تغییرهای خودکار آن چیزی را خراب نمیکند. با دکمه «انتشار در سایت»
|
||||
عمومی میشود؛ دوباره میتوان آن را به پیشنویس برگرداند. پیشنویسها فقط برای ادمین
|
||||
(در صفحه وبلاگ، با برچسب «پیشنویس») دیده میشوند و در `GET /api/site` هرگز نمیآیند.
|
||||
|
||||
**فرمت ذخیره**: بدنه مقاله یک سند ساختاریافته TipTap (JSON) در PostgreSQL است — نه HTML خام.
|
||||
صفحه مقاله آن را با رندر استاتیک TipTap به HTML تبدیل میکند، پس فقط تگهای شناختهشده
|
||||
(پاراگراف، تیتر، لیست، نقلقول، تصویر، لینک) تولید میشوند. مقالات قدیمی (لیست پاراگراف)
|
||||
@@ -128,14 +153,40 @@ npm run dev
|
||||
|
||||
- **بررسی سئو در صفحه نوشتن**: فهرست سبز/نارنجی/قرمز به سبک یوست ولی سازگار با فارسی —
|
||||
طول عنوان، کلمه کلیدی هدف (در عنوان/پاراگراف اول/تیترها)، طول چکیده، تعداد واژه،
|
||||
تیتربندی H2، متن جایگزین تصاویر، پیوندها و نشانی (slug)
|
||||
تیتربندی H2، متن جایگزین تصاویر، پیوندها و نشانی (slug) — کلمه کلیدی هدف در دیتابیس
|
||||
ذخیره میشود (فیلد `seo_keyphrase` مقاله)
|
||||
- **سئوی هر صفحه**: در حالت ویرایش، دکمه سئو روی هیرو هر صفحه (و کنار هیرو صفحه اصلی) —
|
||||
عنوان تب و توضیح متای هر صفحه از دیتابیس خوانده و اعمال میشود
|
||||
عنوان تب و توضیح متای هر صفحه از دیتابیس خوانده و اعمال میشود؛ اگر ردیف سئوی صفحهای
|
||||
هنوز ساخته نشده باشد، دکمه + همانجا آن را میسازد
|
||||
- صفحه مقاله و پروژه بهطور خودکار عنوان و توضیح متا را از محتوای خودشان میگیرند
|
||||
- **تگهای OG و توییتر و canonical** برای همه صفحات از `src/seo.ts` ست میشود و
|
||||
مقالهها اسکیمای JSON-LD نوع Article میگیرند
|
||||
- **robots.txt و sitemap.xml** از بکاند سرو میشوند و مقاله/پروژههای منتشرشده را
|
||||
شامل میشوند (پیشنویسها نمیآیند) — برای آدرس مطلق، `SITE_URL` را در `.env` بگذارید
|
||||
- **متا برای خزندههای بدون جاوااسکریپت** (تلگرام، واتساپ و...): بکاند صفحات
|
||||
`/blog`، `/blog/{slug}`، `/projects` و `/projects/{slug}` را با همان `index.html`
|
||||
فرانتاند سرو میکند و پیش از ارسال، title / description / OG / JSON-LD را از
|
||||
دیتابیس داخل آن تزریق میکند (`app/seo_pages.py`) — بازدیدکنندههای عادی همان SPA
|
||||
همیشگی را میبینند
|
||||
|
||||
## پشتیبانگیری
|
||||
|
||||
همه محتوای سایت در PostgreSQL و تصاویر در MinIO است — هر دو را با یک دستور پشتیبان بگیرید:
|
||||
|
||||
```bash
|
||||
bash scripts/backup.sh # در backups/ — نگهداری پیشفرض ۱۴ روز
|
||||
```
|
||||
|
||||
- خروجی: `backups/db-<زمان>.sql.gz` (فشرده) و `backups/images-<زمان>/` (آینه باکت)
|
||||
- زمانبندی با cron یا Task Scheduler: نمونه دستور داخل خود اسکریپت هست
|
||||
- بازیابی دیتابیس: `gunzip -c backups/db-<زمان>.sql.gz | docker compose exec -T db psql -U aramesh arameshplaza`
|
||||
|
||||
## نکات امنیتی
|
||||
|
||||
- فایل `.env` هرگز commit نمیشود (در `.gitignore` است)
|
||||
- رمز عبور ادمین با bcrypt هش میشود
|
||||
- مقادیر محرمانه در `.env` ریشه هستند (در `.gitignore`) و در `docker-compose.yml`
|
||||
با سینتکس `${VAR:?}` اجباری شدهاند — بدون `.env` استک بالا نمیآید
|
||||
- رمز عبور ادمین با bcrypt هش میشود و از نوار مدیریت پایین سایت (دکمه «رمز») قابل تعویض است
|
||||
- ورود با محدودیت تلاش محافظت میشود: ۵ ورود ناموفق در ۱۵ دقیقه از هر IP → خطای 429
|
||||
- توکنهای JWT امضا دارند و مدت اعتبارشان قابل تنظیم است
|
||||
- تصاویر باکت MinIO عمومی (فقط خواندن) هستند؛ آپلود فقط با توکن ادمین ممکن است
|
||||
- مقالههای پیشنویس در هیچ انتهای عمومی نمیآیند و در HTML تزریقشده سئو هم افشا نمیشوند
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# لایه تولید — TLS با Let's Encrypt و انتشار روی پورتهای استاندارد ۸۰/۴۴۳.
|
||||
# پیشنیاز: متغیرهای DOMAIN و ACME_EMAIL در .env؛ بهعلاوه رکورد DNS دامنه به این سرور.
|
||||
#
|
||||
# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
|
||||
#
|
||||
# همه درخواستهای HTTP به HTTPS هدایت میشوند و مسیریابیها فقط روی دامنه DOMAIN پاسخ میدهند.
|
||||
|
||||
services:
|
||||
traefik:
|
||||
command: !override
|
||||
- --providers.docker=true
|
||||
- --providers.docker.exposedbydefault=false
|
||||
- --entrypoints.web.address=:80
|
||||
- --entrypoints.web.http.redirections.entrypoint.to=websecure
|
||||
- --entrypoints.web.http.redirections.entrypoint.scheme=https
|
||||
- --entrypoints.websecure.address=:443
|
||||
- --certificatesresolvers.le.acme.email=${ACME_EMAIL:?ACME_EMAIL را در .env تنظیم کنید}
|
||||
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
|
||||
- --certificatesresolvers.le.acme.httpchallenge=true
|
||||
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
|
||||
ports: !override
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes: !override
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- letsencrypt_data:/letsencrypt
|
||||
|
||||
backend:
|
||||
environment:
|
||||
# سایت هممبدأ است — CORS لازم نیست
|
||||
CORS_ORIGINS: '[]'
|
||||
SITE_URL: https://${DOMAIN:?DOMAIN را در .env تنظیم کنید}
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.api.rule=Host(`${DOMAIN}`) && PathPrefix(`/api`)
|
||||
- traefik.http.routers.api.entrypoints=websecure
|
||||
- traefik.http.routers.api.tls.certresolver=le
|
||||
- traefik.http.routers.seo.rule=Host(`${DOMAIN}`) && (PathPrefix(`/blog/`) || Path(`/blog`) || PathPrefix(`/projects/`) || Path(`/projects`) || PathPrefix(`/robots.txt`) || PathPrefix(`/sitemap.xml`))
|
||||
- traefik.http.routers.seo.entrypoints=websecure
|
||||
- traefik.http.routers.seo.tls.certresolver=le
|
||||
|
||||
minio:
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.media.rule=Host(`${DOMAIN}`) && PathPrefix(`/media`)
|
||||
- traefik.http.routers.media.entrypoints=websecure
|
||||
- traefik.http.routers.media.tls.certresolver=le
|
||||
|
||||
frontend:
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.web.rule=Host(`${DOMAIN}`) && PathPrefix(`/`)
|
||||
- traefik.http.routers.web.entrypoints=websecure
|
||||
- traefik.http.routers.web.tls.certresolver=le
|
||||
|
||||
volumes:
|
||||
letsencrypt_data:
|
||||
+51
-14
@@ -1,3 +1,7 @@
|
||||
# مقادیر محرمانه از فایل .env ریشه خوانده میشوند — از .env.example کپی کنید:
|
||||
# cp .env.example .env
|
||||
# برای انتشار با TLS، روی این فایل docker-compose.prod.yml را هم سوار کنید (README).
|
||||
|
||||
services:
|
||||
# نقطه ورود واحد — مسیریابی / (سایت استاتیک)، /api (بکاند) و /media (MinIO)
|
||||
traefik:
|
||||
@@ -8,7 +12,7 @@ services:
|
||||
- --providers.docker.exposedbydefault=false
|
||||
- --entrypoints.web.address=:80
|
||||
ports:
|
||||
- "5173:80"
|
||||
- "${TRAEFIK_HTTP_PORT:-5173}:80"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
|
||||
@@ -17,7 +21,7 @@ services:
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: aramesh
|
||||
POSTGRES_PASSWORD: aramesh
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD را در .env تنظیم کنید}
|
||||
POSTGRES_DB: arameshplaza
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
@@ -32,8 +36,8 @@ services:
|
||||
restart: unless-stopped
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: aramesh
|
||||
MINIO_ROOT_PASSWORD: aramesh-minio-secret
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-aramesh}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD را در .env تنظیم کنید}
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
healthcheck:
|
||||
@@ -42,7 +46,7 @@ services:
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
ports:
|
||||
- "9001:9001" # کنسول MinIO
|
||||
- "9001:9001" # کنسول MinIO — در محیط واقعی با فایروال ببندید
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.media.rule=PathPrefix(`/media`)
|
||||
@@ -60,8 +64,8 @@ services:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
MINIO_ROOT_USER: aramesh
|
||||
MINIO_ROOT_PASSWORD: aramesh-minio-secret
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-aramesh}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD را در .env تنظیم کنید}
|
||||
MINIO_BUCKET: aramesh-media
|
||||
restart: "no"
|
||||
entrypoint: >
|
||||
@@ -76,27 +80,44 @@ services:
|
||||
build: ./backend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DATABASE_URL: postgresql+psycopg://aramesh:aramesh@db:5432/arameshplaza
|
||||
SECRET_KEY: change-me-to-a-long-random-string
|
||||
CORS_ORIGINS: '["http://localhost:5173","http://127.0.0.1:5173"]'
|
||||
ADMIN_USERNAME: admin
|
||||
ADMIN_PASSWORD: aramesh123
|
||||
DATABASE_URL: postgresql+psycopg://aramesh:${POSTGRES_PASSWORD}@db:5432/arameshplaza
|
||||
SECRET_KEY: ${SECRET_KEY:?SECRET_KEY را در .env تنظیم کنید}
|
||||
CORS_ORIGINS: ${CORS_ORIGINS:-'["http://localhost:5173","http://127.0.0.1:5173"]'}
|
||||
ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}
|
||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ADMIN_PASSWORD را در .env تنظیم کنید}
|
||||
S3_ENDPOINT: http://minio:9000
|
||||
S3_ACCESS_KEY: aramesh
|
||||
S3_SECRET_KEY: aramesh-minio-secret
|
||||
S3_ACCESS_KEY: ${MINIO_ROOT_USER:-aramesh}
|
||||
S3_SECRET_KEY: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD را در .env تنظیم کنید}
|
||||
S3_BUCKET: aramesh-media
|
||||
MEDIA_PUBLIC_BASE: /media
|
||||
# نشانی عمومی سایت — برای sitemap و آدرس مطلق OG (مثل https://aramesh.example)
|
||||
SITE_URL: ${SITE_URL:-}
|
||||
# بیلد فرانتاند برای تزریق متای سئو در صفحات /blog و /projects
|
||||
FRONTEND_DIST: /srv/frontend
|
||||
volumes:
|
||||
- ./frontend/dist:/srv/frontend:ro
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.api.rule=PathPrefix(`/api`)
|
||||
- traefik.http.routers.api.entrypoints=web
|
||||
- traefik.http.routers.api.priority=100
|
||||
- traefik.http.services.api.loadbalancer.server.port=8000
|
||||
# robots.txt و sitemap.xml و صفحات وبلاگ/پروژه با متای سئو — اولویت بالاتر از SPA
|
||||
- traefik.http.routers.seo.rule=PathPrefix(`/blog/`) || Path(`/blog`) || PathPrefix(`/projects/`) || Path(`/projects`) || PathPrefix(`/robots.txt`) || PathPrefix(`/sitemap.xml`)
|
||||
- traefik.http.routers.seo.entrypoints=web
|
||||
- traefik.http.routers.seo.priority=50
|
||||
- traefik.http.routers.seo.service=api
|
||||
|
||||
# فقط سرو فایلهای استاتیک SPA — بیلد روی هاست (npm run build) انجام شده است
|
||||
frontend:
|
||||
@@ -105,6 +126,11 @@ services:
|
||||
depends_on:
|
||||
- backend
|
||||
- minio
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost/"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.web.rule=PathPrefix(`/`)
|
||||
@@ -112,6 +138,17 @@ services:
|
||||
- traefik.http.routers.web.priority=1
|
||||
- traefik.http.services.web.loadbalancer.server.port=80
|
||||
|
||||
# ابزار پشتیبانگیری — فقط با پروفایل tools اجرا میشود (scripts/backup.sh این کار را میکند)
|
||||
mc:
|
||||
image: minio/mc:latest
|
||||
profiles: ["tools"]
|
||||
depends_on:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-aramesh}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD را در .env تنظیم کنید}
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
minio_data:
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# پشتیبانگیری روزانه — دیتابیس PostgreSQL (همه محتوای سایت) و تصاویر MinIO.
|
||||
#
|
||||
# اجرا: bash scripts/backup.sh
|
||||
# زمانبندی: cron (لینوکس) یا Task Scheduler (ویندوز)، مثلا هر شب ساعت ۳:
|
||||
# 0 3 * * * cd /path/to/aramesh-backend && bash scripts/backup.sh >> backups/backup.log 2>&1
|
||||
#
|
||||
# نگهداری: فایلهای قدیمیتر از RETENTION_DAYS (پیشفرض ۱۴) حذف میشوند.
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
RETENTION_DAYS="${RETENTION_DAYS:-14}"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p backups
|
||||
|
||||
# متغیرهای .env (برای MINIO_ROOT_*) در دسترس قرار میگیرند
|
||||
if [ -f .env ]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
. ./.env
|
||||
set +a
|
||||
fi
|
||||
|
||||
echo "[backup] $STAMP"
|
||||
|
||||
# ۱) دیتابیس — همه متنها، مقالهها و تنظیمات سایت
|
||||
DB_FILE="backups/db-${STAMP}.sql.gz"
|
||||
docker compose exec -T db pg_dump -U aramesh arameshplaza | gzip > "$DB_FILE"
|
||||
echo "[backup] db: $DB_FILE ($(du -h "$DB_FILE" | cut -f1))"
|
||||
|
||||
# ۲) تصاویر MinIO — باکت aramesh-media
|
||||
docker compose run --rm -v "$(pwd)/backups:/backups" --entrypoint sh mc -c "
|
||||
mc alias set local http://minio:9000 \"\$MINIO_ROOT_USER\" \"\$MINIO_ROOT_PASSWORD\" >/dev/null &&
|
||||
mc mirror --overwrite local/aramesh-media /backups/images-${STAMP}
|
||||
"
|
||||
echo "[backup] images: backups/images-${STAMP}"
|
||||
|
||||
# ۳) پاکسازی نسخههای قدیمیتر
|
||||
find backups -name 'db-*.sql.gz' -mtime "+${RETENTION_DAYS}" -delete
|
||||
find backups -maxdepth 1 -type d -name 'images-*' -mtime "+${RETENTION_DAYS}" -exec rm -rf {} +
|
||||
echo "[backup] پاکسازی نسخههای قدیمیتر از ${RETENTION_DAYS} روز انجام شد"
|
||||
|
||||
echo "[backup] ✔ تمام شد"
|
||||
Reference in New Issue
Block a user