Align CI/CD with soroush method (DrSousan single-app pattern)
Audited against working Meezi/DrSousan pipelines. Fixes:
- Single docker-compose.yml is the production stack (api + internal db); folded in docker-compose.prod.yml; dev Postgres → docker-compose.dev.yml
- Dockerfile HEALTHCHECK (bash /dev/tcp) so deploy's docker-inspect Health.Status wait works
- Naming to convention: service api, container hamkadr_api/hamkadr_db, image mirror.soroushasadi.com/hamkadr/api:${API_TAG}
- Workflow rewritten to DrSousan pattern: ci build + deploy (rollback-tag before build, pg_dump backup, stop/rm/up, docker-inspect health-wait with crash detection, scoped image prune)
- environment: block with ${VAR:-default} substitution (no hard-failing env_file); HOST_PORT; .env excluded from image context
- nginx vhost + DEPLOY.md updated to match
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
**/bin/
|
||||
**/obj/
|
||||
.env
|
||||
.env.*
|
||||
.git/
|
||||
.gitea/
|
||||
.vs/
|
||||
|
||||
+80
-44
@@ -1,17 +1,28 @@
|
||||
name: CI/CD
|
||||
|
||||
on:
|
||||
push: { branches: [main] }
|
||||
pull_request: { branches: [main] }
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: hamkadr-cicd-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Runner labels (act_runner):
|
||||
# ubuntu-latest:docker://node:20-alpine ← CI jobs run in real Docker containers
|
||||
# self-hosted:host ← deploy runs directly on the server
|
||||
# All images/packages via Nexus at mirror.soroushasadi.com.
|
||||
# Required Gitea secret: ENV_FILE (contents of .env)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
jobs:
|
||||
# ---------------------------------------------------------------- CI
|
||||
build:
|
||||
name: "CI — dotnet build (Release)"
|
||||
|
||||
# ── CI: compile-check (every push / PR) ──────────────────────────────────────
|
||||
ci:
|
||||
name: "CI · dotnet build"
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mirror.soroushasadi.com/dotnet/sdk:10.0
|
||||
@@ -44,23 +55,23 @@ jobs:
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore src/JobsMedical.Web/JobsMedical.Web.csproj --configfile /tmp/nuget.ci.config
|
||||
env: { DOTNET_CLI_TELEMETRY_OPTOUT: 1 }
|
||||
env:
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||
|
||||
- name: Build
|
||||
run: dotnet build src/JobsMedical.Web/JobsMedical.Web.csproj --no-restore -c Release
|
||||
|
||||
# ---------------------------------------------------------------- Deploy
|
||||
# ── CD: build image → deploy on the server (push to main only) ────────────────
|
||||
deploy:
|
||||
name: "Deploy — hamkadr (compose)"
|
||||
name: "Deploy · hamkadr"
|
||||
runs-on: self-hosted
|
||||
needs: [build]
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
# act host runner starts with a minimal PATH — extend so docker/snap resolve.
|
||||
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
|
||||
DOCKER_BUILDKIT: 1
|
||||
COMPOSE_DOCKER_CLI_BUILD: 1
|
||||
needs: [ci]
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
env:
|
||||
@@ -75,52 +86,77 @@ jobs:
|
||||
|
||||
- name: Write .env
|
||||
run: printf '%s' "$ENV_FILE" > .env
|
||||
env: { ENV_FILE: ${{ secrets.ENV_FILE }} }
|
||||
env:
|
||||
ENV_FILE: ${{ secrets.ENV_FILE }}
|
||||
|
||||
- name: Tag current image for rollback (before rebuild)
|
||||
run: |
|
||||
if docker image inspect mirror.soroushasadi.com/hamkadr/api:latest >/dev/null 2>&1; then
|
||||
docker tag mirror.soroushasadi.com/hamkadr/api:latest mirror.soroushasadi.com/hamkadr/api:rollback
|
||||
echo "Tagged previous image → :rollback"
|
||||
else
|
||||
echo "No previous image — first deploy."
|
||||
fi
|
||||
|
||||
- name: Back up database (if running)
|
||||
run: |
|
||||
set -a; . ./.env; set +a
|
||||
if docker ps -a --format '{{.Names}}' | grep -q '^hamkadr-db$'; then
|
||||
mkdir -p /opt/hamkadr-backups
|
||||
TS=$(date +%Y%m%d-%H%M%S)
|
||||
echo "Backing up DB → /opt/hamkadr-backups/hamkadr-$TS.sql"
|
||||
docker exec hamkadr-db pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" \
|
||||
> "/opt/hamkadr-backups/hamkadr-$TS.sql" || echo "WARN: backup skipped (db not ready yet)"
|
||||
if docker ps -q --filter name='^hamkadr_db$' | grep -q .; then
|
||||
BACKUP_DIR="/opt/hamkadr-backups"; mkdir -p "$BACKUP_DIR"
|
||||
STAMP=$(date +%Y%m%d-%H%M%S)
|
||||
docker exec hamkadr_db pg_dump -U "${POSTGRES_USER:-hamkadr}" "${POSTGRES_DB:-hamkadr}" \
|
||||
> "$BACKUP_DIR/hamkadr-$STAMP.sql" \
|
||||
&& echo "✅ DB backed up → $BACKUP_DIR/hamkadr-$STAMP.sql" \
|
||||
|| echo "⚠️ backup failed (non-fatal)"
|
||||
ls -t "$BACKUP_DIR"/*.sql 2>/dev/null | tail -n +11 | xargs -r rm
|
||||
else
|
||||
echo "No existing db container — first deploy, nothing to back up."
|
||||
echo "ℹ️ hamkadr_db not running — first deploy, nothing to back up."
|
||||
fi
|
||||
|
||||
- name: Tag current image for rollback
|
||||
run: |
|
||||
if docker image inspect hamkadr-app:latest >/dev/null 2>&1; then
|
||||
docker tag hamkadr-app:latest hamkadr-app:rollback
|
||||
echo "Tagged hamkadr-app:rollback"
|
||||
fi
|
||||
|
||||
- name: Build app image
|
||||
run: docker compose -f docker-compose.prod.yml build app
|
||||
- name: Build image
|
||||
run: docker compose build api
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
COMPOSE_DOCKER_CLI_BUILD: 1
|
||||
|
||||
- name: Start database
|
||||
run: docker compose -f docker-compose.prod.yml up -d --no-deps db
|
||||
run: docker compose up -d --no-deps db
|
||||
|
||||
- name: Recreate app (stop + rm + up — reliable across docker versions)
|
||||
- name: Deploy app (stop + rm + up — reliable across docker versions)
|
||||
run: |
|
||||
docker stop hamkadr-app 2>/dev/null || true
|
||||
docker rm hamkadr-app 2>/dev/null || true
|
||||
docker compose -f docker-compose.prod.yml up -d --no-deps app
|
||||
docker stop hamkadr_api 2>/dev/null || true
|
||||
docker rm hamkadr_api 2>/dev/null || true
|
||||
docker compose up -d --no-deps api
|
||||
|
||||
- name: Wait for app healthy
|
||||
- name: Wait for healthy
|
||||
run: |
|
||||
set -a; . ./.env; set +a
|
||||
for i in $(seq 1 24); do
|
||||
if curl -fsS "http://127.0.0.1:${APP_PORT}/healthz" >/dev/null 2>&1; then
|
||||
echo "OK — hamkadr-app healthy on 127.0.0.1:${APP_PORT}"; exit 0
|
||||
echo "Waiting for hamkadr_api (up to 3 min)..."
|
||||
for i in $(seq 1 36); do
|
||||
HEALTH=$(docker inspect --format='{{.State.Health.Status}}' hamkadr_api 2>/dev/null || echo "missing")
|
||||
STATE=$(docker inspect --format='{{.State.Status}}' hamkadr_api 2>/dev/null || echo "missing")
|
||||
RESTARTS=$(docker inspect --format='{{.RestartCount}}' hamkadr_api 2>/dev/null || echo "0")
|
||||
echo " [$i/36] state=$STATE health=$HEALTH restarts=$RESTARTS"
|
||||
[ "$HEALTH" = "healthy" ] && echo "✅ hamkadr_api healthy" && exit 0
|
||||
if [ "$STATE" = "exited" ] || [ "$STATE" = "dead" ]; then
|
||||
echo "❌ hamkadr_api crashed — logs:"; docker logs hamkadr_api 2>&1 | tail -120; exit 1
|
||||
fi
|
||||
echo " [$i/24] not ready yet…"
|
||||
if [ "$RESTARTS" -gt 1 ]; then
|
||||
echo "❌ hamkadr_api crash-loop — logs:"; docker logs hamkadr_api 2>&1 | tail -120; exit 1
|
||||
fi
|
||||
[ "$i" = "36" ] && echo "❌ timeout" && docker logs hamkadr_api 2>&1 | tail -80 && exit 1
|
||||
sleep 5
|
||||
done
|
||||
echo "TIMEOUT — dumping logs"; docker logs --tail=60 hamkadr-app; exit 1
|
||||
|
||||
- name: Prune dangling images
|
||||
- name: Show containers
|
||||
if: always()
|
||||
run: docker compose ps
|
||||
|
||||
- name: Prune old hamkadr images
|
||||
if: success()
|
||||
run: docker image prune -f
|
||||
# Only untagged (<none>) hamkadr images — never touches other projects.
|
||||
run: |
|
||||
docker images --format '{{.Repository}}:{{.Tag}} {{.ID}}' \
|
||||
| grep '^mirror\.soroushasadi\.com/hamkadr/' \
|
||||
| grep '<none>' \
|
||||
| awk '{print $2}' \
|
||||
| xargs -r docker rmi || true
|
||||
|
||||
@@ -7,10 +7,10 @@ TLS for `hamkadr.ir` and reverse-proxies to the app.
|
||||
## Architecture & open ports
|
||||
|
||||
```
|
||||
Internet ──443/80──► nginx (host, existing) ──► 127.0.0.1:8090 ──► hamkadr-app (container :8080)
|
||||
Internet ──443/80──► nginx (host, existing) ──► 127.0.0.1:8090 ──► hamkadr_api (container :8080)
|
||||
│ internal docker net
|
||||
▼
|
||||
hamkadr-db (postgres, no host port)
|
||||
hamkadr_db (postgres, no host port)
|
||||
```
|
||||
|
||||
| Port | Open? | Purpose |
|
||||
@@ -30,7 +30,8 @@ serves git./mirror. — no firewall change needed.)
|
||||
|------|------|
|
||||
| `Dockerfile` | multi-stage build, images + NuGet via `mirror.soroushasadi.com` |
|
||||
| `nuget.docker.config` | NuGet → Nexus `nuget-group` |
|
||||
| `docker-compose.prod.yml` | `app` (127.0.0.1:${APP_PORT}) + `db` (internal) + named volume |
|
||||
| `docker-compose.yml` | production stack: `api` (127.0.0.1:${HOST_PORT}) + `db` (internal) + named volume |
|
||||
| `docker-compose.dev.yml` | local-dev Postgres only (host 5433) for `dotnet run` |
|
||||
| `.gitea/workflows/ci-cd.yml` | build job + self-hosted deploy (backup → rollback tag → recreate → health-wait) |
|
||||
| `deploy/nginx-hamkadr.ir.conf` | nginx vhost for hamkadr.ir |
|
||||
|
||||
@@ -50,48 +51,37 @@ and its user is in the `docker` group. (Already true if other soroush projects d
|
||||
### 3. ENV_FILE secret
|
||||
Set at `https://git.soroushasadi.com/soroushdes/hamkadr/settings/secrets` → key **`ENV_FILE`**:
|
||||
|
||||
`docker-compose.yml` substitutes these into the `api`/`db` services (it derives
|
||||
`ConnectionStrings__Default` and `Auth__AdminPhone` for you), so the secret is just:
|
||||
|
||||
```dotenv
|
||||
ASPNETCORE_ENVIRONMENT=Production
|
||||
ASPNETCORE_URLS=http://+:8080
|
||||
|
||||
# host port nginx proxies to (must match deploy/nginx-hamkadr.ir.conf)
|
||||
APP_PORT=8090
|
||||
HOST_PORT=8090
|
||||
|
||||
# Postgres (container) — generate a strong password: openssl rand -hex 24
|
||||
# Postgres — generate a strong password: openssl rand -hex 24
|
||||
POSTGRES_DB=hamkadr
|
||||
POSTGRES_USER=hamkadr
|
||||
POSTGRES_PASSWORD=__CHANGE_ME__
|
||||
|
||||
# EF Core connection string (host = compose service name "db")
|
||||
ConnectionStrings__Default=Host=db;Port=5432;Database=hamkadr;Username=hamkadr;Password=__CHANGE_ME__
|
||||
# Platform admin phone (gets the Admin role on login)
|
||||
ADMIN_PHONE=09XXXXXXXXX
|
||||
|
||||
# Platform admin (the phone that gets the Admin role on login)
|
||||
Auth__AdminPhone=09XXXXXXXXX
|
||||
|
||||
# Future: Kavenegar / SMS.ir keys for real OTP delivery
|
||||
|
||||
# --- Channel scraping (optional; off by default) ---
|
||||
# Enable the background worker and the sources you want, then their fetch runs on a timer.
|
||||
# Ingestion__Enabled=true
|
||||
# Ingestion__IntervalMinutes=30
|
||||
# Telegram (public channels via t.me/s — no token needed):
|
||||
# Ingestion__Telegram__Enabled=true
|
||||
# Ingestion__Telegram__Channels__0=shift_channel_username
|
||||
# Ingestion__Telegram__Channels__1=another_channel
|
||||
# Bale (bot must be a member of the channel; Telegram-style Bot API):
|
||||
# Ingestion__Bale__Enabled=true
|
||||
# Ingestion__Bale__BotToken=__BALE_BOT_TOKEN__
|
||||
# Divar (best-effort web-search):
|
||||
# Ingestion__Divar__Enabled=true
|
||||
# Ingestion__Divar__Queries__0=استخدام پزشک
|
||||
# Ingestion__Divar__Queries__1=پرستار
|
||||
# --- Channel scraping (optional; off by default) — toggles ---
|
||||
# INGESTION_ENABLED=true
|
||||
# INGESTION_INTERVAL_MINUTES=30
|
||||
# TELEGRAM_ENABLED=true
|
||||
# TELEGRAM_BOT_TOKEN=__TELEGRAM_BOT_TOKEN__
|
||||
# BALE_ENABLED=true
|
||||
# BALE_BOT_TOKEN=__BALE_BOT_TOKEN__
|
||||
# DIVAR_ENABLED=true
|
||||
```
|
||||
> Channel **lists** (`Telegram.Channels`, `Divar.Queries`) live in `appsettings.json` (or add
|
||||
> `Ingestion__Telegram__Channels__0=...` keys). The toggles above gate each source on/off.
|
||||
> The **AI audit layer** is configured at runtime in the admin panel (`/Admin/Settings`) — endpoint,
|
||||
> model, API key, prompt/framework, and auto-approve — not via env. Default: AI off, mode = Manual,
|
||||
> model, API key, prompt/framework, auto-approve — not via env. Default: AI off, mode = Manual,
|
||||
> so every ingested listing waits in the review queue until an admin publishes it.
|
||||
> `POSTGRES_PASSWORD` and the password in `ConnectionStrings__Default` must be identical.
|
||||
> `ASPNETCORE_ENVIRONMENT=Production` ⇒ only **reference data** (roles/cities/districts) is seeded —
|
||||
> no demo facilities/shifts. Real employers add listings via the employer panel.
|
||||
> `ASPNETCORE_ENVIRONMENT=Production` is set by the compose file ⇒ only **reference data**
|
||||
> (roles/cities/districts) is seeded — no demo facilities/shifts.
|
||||
|
||||
### 4. nginx vhost + TLS
|
||||
```bash
|
||||
@@ -111,15 +101,14 @@ on startup and seeds reference data; nginx already proxies hamkadr.ir to it.
|
||||
## Operations
|
||||
|
||||
- **Backups:** every deploy runs `pg_dump` → `/opt/hamkadr-backups/hamkadr-<timestamp>.sql` before touching containers.
|
||||
- **Rollback:** the previous image is tagged `hamkadr-app:rollback` each deploy:
|
||||
- **Rollback:** the previous image is tagged `mirror.soroushasadi.com/hamkadr/api:rollback` each deploy:
|
||||
```bash
|
||||
docker stop hamkadr-app && docker rm hamkadr-app
|
||||
docker run -d --name hamkadr-app --env-file .env --network hamkadr_default \
|
||||
-p 127.0.0.1:8090:8080 hamkadr-app:rollback
|
||||
docker stop hamkadr_api && docker rm hamkadr_api
|
||||
API_TAG=rollback docker compose up -d --no-deps api
|
||||
```
|
||||
- **Rotate a secret:** edit `ENV_FILE` in Gitea, push any commit to redeploy.
|
||||
- **Logs:** `docker logs -f hamkadr-app`
|
||||
- **Restore a backup:** `cat /opt/hamkadr-backups/<file>.sql | docker exec -i hamkadr-db psql -U hamkadr -d hamkadr`
|
||||
- **Logs:** `docker logs -f hamkadr_api`
|
||||
- **Restore a backup:** `cat /opt/hamkadr-backups/<file>.sql | docker exec -i hamkadr_db psql -U hamkadr -d hamkadr`
|
||||
|
||||
## Safety (never do these)
|
||||
- ❌ `docker compose down -v` — deletes the database volume.
|
||||
|
||||
@@ -12,5 +12,12 @@ WORKDIR /app
|
||||
COPY --from=build /out ./
|
||||
EXPOSE 8080
|
||||
ENV ASPNETCORE_URLS=http://+:8080 \
|
||||
ASPNETCORE_ENVIRONMENT=Production \
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
|
||||
# Liveness probe (TCP connect to Kestrel) — lets the deploy job wait on
|
||||
# `docker inspect Health.Status`. bash ships in the Debian-based aspnet image.
|
||||
HEALTHCHECK --interval=15s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD bash -c 'echo > /dev/tcp/localhost/8080' || exit 1
|
||||
|
||||
ENTRYPOINT ["dotnet", "JobsMedical.Web.dll"]
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
# sudo nginx -t && sudo systemctl reload nginx
|
||||
# sudo certbot --nginx -d hamkadr.ir -d www.hamkadr.ir # adds the :443 server + HTTP→HTTPS redirect
|
||||
#
|
||||
# APP_PORT below MUST match APP_PORT in the Gitea ENV_FILE secret (default 8090).
|
||||
# The port below MUST match HOST_PORT in the Gitea ENV_FILE secret (default 8090).
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name hamkadr.ir www.hamkadr.ir;
|
||||
|
||||
# The app binds 127.0.0.1:8090 (docker-compose.prod.yml) — never exposed publicly.
|
||||
# The app binds 127.0.0.1:8090 (docker-compose.yml, service "api") — never exposed publicly.
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8090;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Local development DB only. Devs run `docker compose -f docker-compose.dev.yml up -d`
|
||||
# and `dotnet run` against it. Production uses docker-compose.yml (api + db).
|
||||
name: jobsmedical-dev
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:17-alpine
|
||||
container_name: jobsmedical-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: jobsmedical
|
||||
POSTGRES_USER: jobsmedical
|
||||
POSTGRES_PASSWORD: jobsmedical_dev
|
||||
ports:
|
||||
- "5433:5432" # host 5433 to avoid clashing with a local Postgres on 5432
|
||||
volumes:
|
||||
- jobsmedical-pgdata:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
jobsmedical-pgdata:
|
||||
@@ -1,39 +0,0 @@
|
||||
# Production stack for hamkadr.ir — used by the Gitea deploy job (docker compose -f docker-compose.prod.yml).
|
||||
# nginx (on the host) terminates TLS for hamkadr.ir and reverse-proxies to 127.0.0.1:${APP_PORT}.
|
||||
name: hamkadr # pinned so redeploys reuse the same named volume (never creates orphaned data)
|
||||
|
||||
services:
|
||||
db:
|
||||
image: mirror.soroushasadi.com/postgres:16-alpine
|
||||
container_name: hamkadr-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- hamkadr_db_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
# NOTE: no `ports:` — Postgres is reachable only by the app on the internal network.
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: hamkadr-app:latest
|
||||
container_name: hamkadr-app
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "127.0.0.1:${APP_PORT}:8080" # localhost-only; nginx proxies hamkadr.ir → here
|
||||
|
||||
volumes:
|
||||
hamkadr_db_data:
|
||||
name: hamkadr_db_data
|
||||
+49
-9
@@ -1,16 +1,56 @@
|
||||
# Production compose for hamkadr.ir — the Gitea deploy job uses THIS file directly
|
||||
# (docker compose build api / up -d --no-deps api). Local dev DB → docker-compose.dev.yml.
|
||||
# nginx (host) terminates TLS for hamkadr.ir and reverse-proxies to 127.0.0.1:${HOST_PORT}.
|
||||
name: hamkadr # locked so redeploys reuse the same named volume (no orphaned data)
|
||||
|
||||
services:
|
||||
|
||||
# ── .NET 10 Razor Pages app ──────────────────────────────────────────────────
|
||||
api:
|
||||
image: mirror.soroushasadi.com/hamkadr/api:${API_TAG:-latest}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: hamkadr_api
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "127.0.0.1:${HOST_PORT:-8090}:8080" # localhost-only; nginx proxies hamkadr.ir → here
|
||||
environment:
|
||||
ASPNETCORE_ENVIRONMENT: "Production"
|
||||
ASPNETCORE_URLS: "http://+:8080"
|
||||
ConnectionStrings__Default: "Host=db;Port=5432;Database=${POSTGRES_DB:-hamkadr};Username=${POSTGRES_USER:-hamkadr};Password=${POSTGRES_PASSWORD}"
|
||||
Auth__AdminPhone: "${ADMIN_PHONE:-}"
|
||||
# Channel scraping (optional; enable + configure via ENV_FILE)
|
||||
Ingestion__Enabled: "${INGESTION_ENABLED:-false}"
|
||||
Ingestion__IntervalMinutes: "${INGESTION_INTERVAL_MINUTES:-30}"
|
||||
Ingestion__Telegram__Enabled: "${TELEGRAM_ENABLED:-false}"
|
||||
Ingestion__Telegram__BotToken: "${TELEGRAM_BOT_TOKEN:-}"
|
||||
Ingestion__Bale__Enabled: "${BALE_ENABLED:-false}"
|
||||
Ingestion__Bale__BotToken: "${BALE_BOT_TOKEN:-}"
|
||||
Ingestion__Divar__Enabled: "${DIVAR_ENABLED:-false}"
|
||||
# healthcheck is defined in the Dockerfile (bash /dev/tcp probe) so the deploy
|
||||
# job's `docker inspect Health.Status` wait works.
|
||||
|
||||
# ── PostgreSQL (internal only — never published) ─────────────────────────────
|
||||
db:
|
||||
image: postgres:17-alpine
|
||||
container_name: jobsmedical-db
|
||||
image: mirror.soroushasadi.com/postgres:16-alpine
|
||||
container_name: hamkadr_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: jobsmedical
|
||||
POSTGRES_USER: jobsmedical
|
||||
POSTGRES_PASSWORD: jobsmedical_dev
|
||||
ports:
|
||||
- "5433:5432" # host 5433 to avoid clashing with a local Postgres on 5432
|
||||
POSTGRES_DB: ${POSTGRES_DB:-hamkadr}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-hamkadr}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- jobsmedical-pgdata:/var/lib/postgresql/data
|
||||
- hamkadr_db_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-hamkadr} -d ${POSTGRES_DB:-hamkadr}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
volumes:
|
||||
jobsmedical-pgdata:
|
||||
hamkadr_db_data:
|
||||
name: hamkadr_db_data
|
||||
|
||||
Reference in New Issue
Block a user