Add Gitea CI/CD for hamkadr.ir (Nexus build + self-hosted compose deploy)
- .gitea/workflows/ci-cd.yml: dotnet build via mirror.soroushasadi.com; self-hosted deploy with pg_dump backup, rollback tag, scoped recreate, /healthz wait, prune - Dockerfile (sdk/aspnet 10 via Nexus) + nuget.docker.config + .dockerignore - docker-compose.prod.yml: app on 127.0.0.1:APP_PORT, Postgres internal-only + named volume - deploy/nginx-hamkadr.ir.conf + DEPLOY.md (ports: 22/80/443 only; DB never exposed) - Prod seeds reference data only (no demo listings); ForwardedHeaders for nginx TLS; /healthz endpoint Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
**/bin/
|
||||||
|
**/obj/
|
||||||
|
.git/
|
||||||
|
.gitea/
|
||||||
|
.vs/
|
||||||
|
.idea/
|
||||||
|
.claude/
|
||||||
|
*.md
|
||||||
|
docker-compose*.yml
|
||||||
|
deploy/
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
name: CI/CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push: { branches: [main] }
|
||||||
|
pull_request: { branches: [main] }
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: hamkadr-cicd-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ---------------------------------------------------------------- CI
|
||||||
|
build:
|
||||||
|
name: "CI — dotnet build (Release)"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: mirror.soroushasadi.com/dotnet/sdk:10.0
|
||||||
|
options: --add-host=gitea:host-gateway
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ github.token }}
|
||||||
|
REF: ${{ github.ref }}
|
||||||
|
run: |
|
||||||
|
git init
|
||||||
|
git remote add origin "${{ github.server_url }}/${{ github.repository }}.git"
|
||||||
|
git config http.extraheader "Authorization: Bearer ${TOKEN}"
|
||||||
|
git fetch --depth=1 origin "${REF}"
|
||||||
|
git checkout FETCH_HEAD
|
||||||
|
|
||||||
|
- name: Write NuGet config (Nexus)
|
||||||
|
run: |
|
||||||
|
cat > /tmp/nuget.ci.config << 'EOF'
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
<add key="nexus"
|
||||||
|
value="https://mirror.soroushasadi.com/repository/nuget-group/index.json"
|
||||||
|
protocolVersion="3" />
|
||||||
|
</packageSources>
|
||||||
|
</configuration>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Restore
|
||||||
|
run: dotnet restore src/JobsMedical.Web/JobsMedical.Web.csproj --configfile /tmp/nuget.ci.config
|
||||||
|
env: { DOTNET_CLI_TELEMETRY_OPTOUT: 1 }
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: dotnet build src/JobsMedical.Web/JobsMedical.Web.csproj --no-restore -c Release
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- Deploy
|
||||||
|
deploy:
|
||||||
|
name: "Deploy — hamkadr (compose)"
|
||||||
|
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
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ github.token }}
|
||||||
|
REF: ${{ github.ref }}
|
||||||
|
run: |
|
||||||
|
git init
|
||||||
|
git remote add origin "${{ github.server_url }}/${{ github.repository }}.git"
|
||||||
|
git config http.extraheader "Authorization: Bearer ${TOKEN}"
|
||||||
|
git fetch --depth=1 origin "${REF}"
|
||||||
|
git checkout FETCH_HEAD
|
||||||
|
|
||||||
|
- name: Write .env
|
||||||
|
run: printf '%s' "$ENV_FILE" > .env
|
||||||
|
env: { ENV_FILE: ${{ secrets.ENV_FILE }} }
|
||||||
|
|
||||||
|
- 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)"
|
||||||
|
else
|
||||||
|
echo "No existing db container — 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: Start database
|
||||||
|
run: docker compose -f docker-compose.prod.yml up -d --no-deps db
|
||||||
|
|
||||||
|
- name: Recreate 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
|
||||||
|
|
||||||
|
- name: Wait for app 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
|
||||||
|
fi
|
||||||
|
echo " [$i/24] not ready yet…"
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
echo "TIMEOUT — dumping logs"; docker logs --tail=60 hamkadr-app; exit 1
|
||||||
|
|
||||||
|
- name: Prune dangling images
|
||||||
|
if: success()
|
||||||
|
run: docker image prune -f
|
||||||
@@ -12,6 +12,7 @@ artifacts/
|
|||||||
|
|
||||||
## App
|
## App
|
||||||
appsettings.*.local.json
|
appsettings.*.local.json
|
||||||
|
.env
|
||||||
|
|
||||||
## OS
|
## OS
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# Deploying همکادر / hamkadr.ir
|
||||||
|
|
||||||
|
CI/CD via the **soroush method**: push to Gitea → Gitea Actions builds (through the Nexus mirror)
|
||||||
|
and the self-hosted runner deploys with Docker Compose. nginx (already on the server) terminates
|
||||||
|
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)
|
||||||
|
│ internal docker net
|
||||||
|
▼
|
||||||
|
hamkadr-db (postgres, no host port)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Port | Open? | Purpose |
|
||||||
|
|------|-------|---------|
|
||||||
|
| 22 | ✅ (ideally IP-restricted) | SSH |
|
||||||
|
| 80 | ✅ | HTTP → 443 redirect + Let's Encrypt ACME |
|
||||||
|
| 443 | ✅ | HTTPS `hamkadr.ir` |
|
||||||
|
| 8090 | ❌ host-localhost only | app, reached only by nginx |
|
||||||
|
| 5432 | ❌ internal docker net only | Postgres — never published |
|
||||||
|
|
||||||
|
`ufw` should be exactly: `allow 22, 80, 443`. Nothing else. (80/443 are already open since nginx
|
||||||
|
serves git./mirror. — no firewall change needed.)
|
||||||
|
|
||||||
|
## Files in this repo
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|------|------|
|
||||||
|
| `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 |
|
||||||
|
| `.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 |
|
||||||
|
|
||||||
|
## One-time setup
|
||||||
|
|
||||||
|
### 1. DNS
|
||||||
|
A records → server IP:
|
||||||
|
```
|
||||||
|
hamkadr.ir A <server-ip>
|
||||||
|
www.hamkadr.ir A <server-ip>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Gitea runner
|
||||||
|
Confirm the `act_runner` on this server has the **`self-hosted:host`** label (the deploy job needs it)
|
||||||
|
and its user is in the `docker` group. (Already true if other soroush projects deploy here.)
|
||||||
|
|
||||||
|
### 3. ENV_FILE secret
|
||||||
|
Set at `https://git.soroushasadi.com/soroushdes/hamkadr/settings/secrets` → key **`ENV_FILE`**:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
ASPNETCORE_URLS=http://+:8080
|
||||||
|
|
||||||
|
# host port nginx proxies to (must match deploy/nginx-hamkadr.ir.conf)
|
||||||
|
APP_PORT=8090
|
||||||
|
|
||||||
|
# Postgres (container) — 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 (the phone that gets the Admin role on login)
|
||||||
|
Auth__AdminPhone=09XXXXXXXXX
|
||||||
|
|
||||||
|
# Future: Kavenegar / SMS.ir keys for real OTP delivery
|
||||||
|
```
|
||||||
|
> `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.
|
||||||
|
|
||||||
|
### 4. nginx vhost + TLS
|
||||||
|
```bash
|
||||||
|
sudo cp deploy/nginx-hamkadr.ir.conf /etc/nginx/sites-available/hamkadr.ir
|
||||||
|
sudo ln -s /etc/nginx/sites-available/hamkadr.ir /etc/nginx/sites-enabled/
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
sudo certbot --nginx -d hamkadr.ir -d www.hamkadr.ir
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. First deploy
|
||||||
|
```bash
|
||||||
|
git push gitea main # add the gitea remote first if needed
|
||||||
|
```
|
||||||
|
Watch `https://git.soroushasadi.com/soroushdes/hamkadr/actions`. The app auto-applies EF migrations
|
||||||
|
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:
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
- **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`
|
||||||
|
|
||||||
|
## Safety (never do these)
|
||||||
|
- ❌ `docker compose down -v` — deletes the database volume.
|
||||||
|
- ❌ bare `docker compose down` / `restart` — would stop other projects on the shared host. The
|
||||||
|
workflow always uses `--no-deps <service>` and explicit `stop`/`rm`.
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
# Hamkadr (همکادر) — .NET 10 Razor Pages. Images + NuGet pulled through Nexus (mirror.soroushasadi.com).
|
||||||
|
FROM mirror.soroushasadi.com/dotnet/sdk:10.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY nuget.docker.config /tmp/nuget.config
|
||||||
|
COPY src/ ./src/
|
||||||
|
RUN dotnet restore src/JobsMedical.Web/JobsMedical.Web.csproj --configfile /tmp/nuget.config
|
||||||
|
RUN dotnet publish src/JobsMedical.Web/JobsMedical.Web.csproj -c Release -o /out --no-restore \
|
||||||
|
/p:UseAppHost=false
|
||||||
|
|
||||||
|
FROM mirror.soroushasadi.com/dotnet/aspnet:10.0
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /out ./
|
||||||
|
EXPOSE 8080
|
||||||
|
ENV ASPNETCORE_URLS=http://+:8080 \
|
||||||
|
DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||||
|
ENTRYPOINT ["dotnet", "JobsMedical.Web.dll"]
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# hamkadr.ir reverse-proxy vhost for the EXISTING nginx on the server.
|
||||||
|
# Install:
|
||||||
|
# sudo cp deploy/nginx-hamkadr.ir.conf /etc/nginx/sites-available/hamkadr.ir
|
||||||
|
# sudo ln -s /etc/nginx/sites-available/hamkadr.ir /etc/nginx/sites-enabled/
|
||||||
|
# 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).
|
||||||
|
|
||||||
|
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.
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8090;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme; # app's ForwardedHeaders reads this → knows it's HTTPS
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
<add key="nexus"
|
||||||
|
value="https://mirror.soroushasadi.com/repository/nuget-group/index.json"
|
||||||
|
protocolVersion="3" />
|
||||||
|
</packageSources>
|
||||||
|
<config>
|
||||||
|
<add key="http_retry_count" value="8" />
|
||||||
|
<add key="http_retry_delay_milliseconds" value="1000" />
|
||||||
|
</config>
|
||||||
|
</configuration>
|
||||||
@@ -4,12 +4,14 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
namespace JobsMedical.Web.Data;
|
namespace JobsMedical.Web.Data;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Seeds a believable Tehran-focused board so the marketplace doesn't look empty on first run
|
/// Seeds reference data (cities, roles, districts) always, and a believable Tehran demo board
|
||||||
/// (the cold-start problem). Idempotent: only seeds when the DB is empty.
|
/// (facilities/shifts/jobs/raw listings) only when <paramref name="includeDemo"/> is true.
|
||||||
|
/// In production we pass false so real employers populate listings — no fake data goes public.
|
||||||
|
/// Idempotent: reference seeds only when empty; demo seeds only when no facilities exist.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SeedData
|
public static class SeedData
|
||||||
{
|
{
|
||||||
public static async Task EnsureSeededAsync(AppDbContext db)
|
public static async Task EnsureSeededAsync(AppDbContext db, bool includeDemo = true)
|
||||||
{
|
{
|
||||||
if (await db.Cities.AnyAsync()) return;
|
if (await db.Cities.AnyAsync()) return;
|
||||||
|
|
||||||
@@ -49,6 +51,9 @@ public static class SeedData
|
|||||||
new District { Name = "تجریش", CityId = tehran.Id });
|
new District { Name = "تجریش", CityId = tehran.Id });
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
// ----- Demo data (Tehran sample board): development only -----
|
||||||
|
if (!includeDemo) return;
|
||||||
|
|
||||||
var facilities = new[]
|
var facilities = new[]
|
||||||
{
|
{
|
||||||
new Facility { Name = "بیمارستان میلاد", Type = FacilityType.Hospital, CityId = tehran.Id,
|
new Facility { Name = "بیمارستان میلاد", Type = FacilityType.Hospital, CityId = tehran.Id,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Text.Unicode;
|
|||||||
using JobsMedical.Web.Data;
|
using JobsMedical.Web.Data;
|
||||||
using JobsMedical.Web.Services;
|
using JobsMedical.Web.Services;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@@ -45,7 +46,8 @@ using (var scope = app.Services.CreateScope())
|
|||||||
{
|
{
|
||||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
db.Database.Migrate();
|
db.Database.Migrate();
|
||||||
await SeedData.EnsureSeededAsync(db);
|
// Production seeds reference data only (no demo facilities/shifts); dev seeds the full board.
|
||||||
|
await SeedData.EnsureSeededAsync(db, app.Environment.IsDevelopment());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
@@ -56,6 +58,16 @@ if (!app.Environment.IsDevelopment())
|
|||||||
app.UseHsts();
|
app.UseHsts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Behind nginx (TLS terminated upstream): trust X-Forwarded-Proto/For so the app knows it's
|
||||||
|
// HTTPS — required for correct secure cookies and to avoid HTTPS-redirect loops.
|
||||||
|
var forwardedOptions = new ForwardedHeadersOptions
|
||||||
|
{
|
||||||
|
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
|
||||||
|
};
|
||||||
|
forwardedOptions.KnownIPNetworks.Clear(); // only nginx can reach the container's bound port
|
||||||
|
forwardedOptions.KnownProxies.Clear();
|
||||||
|
app.UseForwardedHeaders(forwardedOptions);
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
@@ -70,4 +82,7 @@ app.MapStaticAssets();
|
|||||||
app.MapRazorPages()
|
app.MapRazorPages()
|
||||||
.WithStaticAssets();
|
.WithStaticAssets();
|
||||||
|
|
||||||
|
// Lightweight liveness probe for the deploy health-wait loop (and uptime checks).
|
||||||
|
app.MapGet("/healthz", () => Results.Text("ok"));
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
Reference in New Issue
Block a user