Személyes portfólió oldal és a hozzá tartozó saját CMS. Két ASP.NET Core host, két Vue 3 SPA, Postgres és Redis; egy VPS-en, Docker Compose-zal, kifelé cloudflared tunnelen.
A rendszer szándékosan két hostra van osztva:
| Host | Mit ad | Hogyan érhető el |
|---|---|---|
web (publikus) | a portfólió oldal, olvasó API, kapcsolatfelvétel, látogatás-beacon | cloudflared tunnel → 127.0.0.1:8080 |
admin (dashboard) | minden írás (CRUD), csatolmánykezelés, statisztika, contact inbox | tunnel + Cloudflare Access → 127.0.0.1:8090 |
A lényeg nem a kényelem, hanem hogy a publikus originon nem léteznek író endpointok — nem le
vannak tiltva, hanem az a controller-készlet nincs betöltve. Az admin host a védelmét nem a
portkötésre bízza: a Cloudflare Access assertionjét maga is ellenőrzi
(CloudflareAccessMiddleware).
PortfolioCMS.Core közös mag: EF Core modell + migrációk, olvasó controllerek,
middleware-ek (CSP, bot, Access), ContentCache, SEO (sitemap,
útvonal-index, oldalankénti meta), indítási kiterjesztések
PortfolioCMS.Server publikus host: Core + kapcsolat, analytics beacon, sitemap/robots,
SPA fallback (valódi 404 + szerveroldali meta)
PortfolioCMS.Admin admin host: Core + CRUD és statisztika controllerek, Cloudflare Access
PortfolioCMS.Tests xUnit: unit tesztek, TestServer-es pipeline tesztek, API tesztek
eldobható Postgres konténerben (Testcontainers)
portfoliocms.client publikus SPA (Vue 3 + Vite)
portfoliocms.admin.client dashboard SPA (Vue 3 + Vite)
portfoliocms.shared a két SPA közös kódja: API kliens, content store, i18n, formázók.
Saját package.json / tsconfig / vitest — önmagában lintelhető,
típusellenőrizhető és tesztelhető. A két appba nem npm
függőségként, hanem a `@shared` aliasból épül be.
deploy/ üzemeltetés: runbook, mentés/visszaállítás, smoke teszt,
cloudflared példakonfiguráció, systemd unitok
Adattárolás:
- Postgres — tartalom (
Works,Skills,Experiences,About), csatolmányokbytea-ban, látogatás-események, kapcsolati üzenetek. A migrációk aCore-ban vannak, és mindkét host induláskor migrál (MigrateAndSeedAsync). - Redis — olvasási cache (
ContentCache). Szándékosan fail-open: ha nem érhető el, minden kérés az adatbázisra megy, de az oldal áll. Minden admin írás explicit invalidál, a TTL csak biztonsági háló.
Kiszolgálás: mindkét host a saját wwwroot-jából adja a SPA-t. A /api alatti nem létező
útvonal 404, nem index.html. A publikus hoston a
MapSpaFallback dönt a status kódról
(a nem létező slug valódi 404-et kap) és írja a kért oldal saját meta/OG tagjeit a kiszolgált
HTML-be — a SPA ugyanezt megteszi futásidőben, de a szkriptet nem futtató crawler csak ezt látja.
Kell hozzá: .NET 10 SDK, Node 22, Docker Desktop.
cp .env.example .envAmit ki kell tölteni (a többinek van használható defaultja): POSTGRES_PASSWORD,
ANALYTICS_HASH_SECRET, TURNSTILE_*, CF_ACCESS_*. A fájl minden változónál leírja, mire jó.
docker compose up --build- publikus oldal: http://localhost:8080
- admin: http://localhost:8090 — konténerben
Productionkörnyezet fut, tehát az Access assertion ellenőrzés éles. Helyi admin munkához a hostot IDE-ből futtasd (lásd lentebb):Development-ben,CloudflareAccessértékek nélkül a middleware nem kerül be a pipeline-ba.
Ez a szokásos fejlesztői kör. A PortfolioCMS.slnx mind a hat projektet tartalmazza, a két host
Microsoft.AspNetCore.SpaProxy-val indul, tehát a npm run dev-et maga hívja.
Csak az adatbázist és a cache-t hozd fel konténerben:
docker compose up -d db redisA db és a redis a compose-ban internal hálózaton van, port nélkül — a hoszt felől nem
látszanak. A publikálásukhoz kell egy docker-compose.override.yml a repó gyökerében (a compose
automatikusan betölti, és a .gitignore kihagyja, mert csak fejlesztéshez való):
services:
db:
ports:
- "127.0.0.1:5432:5432"redis:
ports:
- "127.0.0.1:6379:6379"A szerveren e nélkül kell deployolni: docker compose -f docker-compose.yml up -d.
Ezután a hostok konfigurációja appsettings.Development.json-ból jön (gitignorolt, minden
fejlesztő a sajátját írja):
{
"ConnectionStrings": {
"Default": "Host=localhost;Port=5432;Database=portfoliocms;Username=portfolio;Password=<a .env-beli jelszó>",
"Redis": "localhost:6379"
},
"Turnstile": {
"SiteKey": "1x00000000000000000000AA",
"SecretKey": "1x0000000000000000000000000000000AA"
},
"SeedMode": "dev"
}A Turnstile értékek a Cloudflare mindig-átmenő teszt kulcsai. A SeedMode: dev demo tartalmat
seedel; prod üres adatbázissal indul.
Indítás:
dotnet run --project PortfolioCMS.Server --launch-profile https- publikus API: https://localhost:7261, SPA dev server: https://localhost:53774
- admin:
dotnet run --project PortfolioCMS.Admin --launch-profile https→ https://localhost:7262, dashboard dev server: https://localhost:53775
A Vite konfiguráció a dev tanúsítványt magától exportálja (dotnet dev-certs https), az első
futás előtt viszont egyszer meg kell bízni benne:
dotnet dev-certs https --trustKét környezeti változó a dev serverre:
DEV_SERVER_HTTP=1— sima HTTP-t szolgál ki, ha valami nem fogadja el az önaláírt tanúsítványtDEV_SERVER_PORT=<port>— a Vite port felülírása (alap: 53774 / 53775)
dotnet test PortfolioCMS.Tests/PortfolioCMS.Tests.csprojA séma jsonb kolumnákat és FILTER aggregátumot használ, ezért az API tesztek egy eldobható
Postgres konténert indítanak. Docker nélküli gépen ezek a tesztek kimaradnak (nem hasalnak el),
a CI-ban futnak.
Frontend, mindkét app könyvtárában:
npm ci
npm run type-check
npx oxlint .&& npx eslint .
npm testA lock fájlok Linuxon készültek — a natív csomagok wasm fallback ágát a Windowson futó npm
kihagyja a lockból, és akkor a CI npm ci "not in sync" hibára fut.
A portfoliocms.shared ugyanez, csak npm install-lal (a saját lockja Windowson készült):
cd portfoliocms.shared
npm install
npm run type-check && npx oxlint .&& npx eslint .&& npm testA három package.json közös csomagjainak verziója nem csúszhat el — semmi nem köti őket
egymáshoz, ezért a CI külön ellenőrzi:
node scripts/check-dep-sync.mjsA négy .NET projekt feloldott csomaggráfja packages.lock.json-ban van kikötve, és az image build
--locked-mode-ban restore-ol — tehát egy elavult lock a buildet buktatja, nem csendben old fel
mást. PackageReference változás után a lock fájlt is commitolni kell:
dotnet restore PortfolioCMS.slnx /p:RestoreForceEvaluate=trueA hitelesnek tekintendő lista a kommentezett .env.example. Amelyik nélkül a
host el sem indul:
| Változó | Mi hasalna el nélküle |
|---|---|
POSTGRES_PASSWORD | a compose (:?), tehát mindkét host |
ANALYTICS_HASH_SECRET (min. 16 byte) | a publikus host — a látogató-hash sózása |
TURNSTILE_SITE_KEY, TURNSTILE_SECRET_KEY | a publikus host — üres secret csendben kikapcsolná a captchát |
CF_ACCESS_TEAM_DOMAIN, CF_ACCESS_AUD | az admin host — enélkül hitelesítés nélkül írható lenne |
A többi (PUBLIC_SITE_URL, ALLOWED_HOSTS, SEED_MODE, retention és mentés beállítások)
defaultolódik. Development-ben a captcha és az Access értékek elhagyhatók.
A master-re való push indítja a deploy.yml-t:
- ci — a
ci.ymlmeghívása: backend tesztek, mindkét frontend típusellenőrzése, lintje,npm audit-ja és tesztjei, a shared csomag ugyanez, valamint a NuGet sebezhetőség-ellenőrzés. Egy közvetlen master push sem deployol tesztek nélkül. Az image build (és vele a Trivy szkennelés) itt kimarad: azt a következő job végzi. - build-and-push — a két image (
portfoliocms,portfoliocms-admin) buildje és pusholása GHCR-be,latestéssha-<commit>taggel. - deploy —
docker-compose.ymlés adeploy/szkriptek másolása a VPS/opt/portfoliocmskönyvtárába, majd SSH-n:.envírása a repository secretekből, GHCR login,pull, deploy előtti adatbázis dump (deploy/backup.sh),up -d,deploy/smoke-test.sh, végül a hétnél régebbi image-ek takarítása.
A dump azért van a séma hozzáérése előtt, mert a hostok induláskor migrálnak, és egy régebbi image nem tudja lebontani az újabb sémát — a visszalépés dump nélkül nem lenne elég.
Ehhez szükséges repository secretek:
- SSH:
SSH_HOST,SSH_USER,SSH_KEY,SSH_PORT - GHCR (a VPS oldali pullhoz):
GHCR_USER,GHCR_TOKEN - alkalmazás:
POSTGRES_DB,POSTGRES_USER,POSTGRES_PASSWORD,ANALYTICS_HASH_SECRET,ANALYTICS_RETENTION_DAYS,ATTACHMENT_ORPHAN_GRACE_DAYS,CONTACT_RETENTION_DAYS,CONTACT_KEEP_UNREAD,PUBLIC_SITE_URL,SEED_MODE,ALLOWED_HOSTS,TURNSTILE_SITE_KEY,TURNSTILE_SECRET_KEY,CF_ACCESS_TEAM_DOMAIN,CF_ACCESS_AUD - mentés:
BACKUP_RETENTION_DAYS,BACKUP_AGE_RECIPIENT,BACKUP_RCLONE_REMOTE
A rollback.yml kézzel indítható (workflow_dispatch). SHA
megadása nélkül az eggyel korábbi image párra lép vissza: a deploy minden alkalommal kimenti a
futó image referenciákat a VPS .env.previous fájljába. A séma miatt a visszalépés általában
visszaállítást is igényel — a menete a runbookban van.
A .github/dependabot.yml hetente négy ökoszisztémát néz: nuget,
npm (a három frontend manifest), github-actions és docker (a két Dockerfile és a
docker-compose.yml base image-ei). A PR-ek csoportosítva jönnek, minor/patch és major külön —
egy Microsoft.* patch kiadás enélkül nyolc külön PR-t nyitna.
A három frontend manifest szándékosan egy PR-be esik: a
scripts/check-dep-sync.mjs elhasal, ha ugyanaz a csomag máshol
más ranget kap, tehát csak együtt léptethetők.
A CI oldali ellenőrzés ehhez: npm audit --audit-level=high mindkét frontendre, a
dotnet list package --vulnerable --include-transitive a négy .NET projektre, és Trivy
(HIGH,CRITICAL, ignore-unfixed) a PR-ben buildelt image-ekre.
Amit a kódból nem lehet látni, mert a hoszton él, a deploy/RUNBOOK.md-ban
van: a forgalom útja, a cloudflared konfiguráció
(példa), a Cloudflare Access alkalmazás és AUD, az
ellenőrzőlista, a deploy és a visszalépés menete, az adatbázis mentése (titkosítás, offsite
tároló, systemd timer) és visszaállítása, a csatolmányok életciklusa, valamint az
erőforrás-korlátok és a logrotáció.
A 2026-08-11-i biztonsági és optimalizálási átvizsgálás (TODO.md) tételei vagy meg vannak
oldva, vagy GitHub issue-ként vannak nyitva — a fájl ezért nem maradt a repóban. Ami a döntések
miértjét rögzíti, az a kód mellett van: a docker-compose.yml, a két Program.cs és a
Core/Services XML kommentjei szándékosan bőbeszédűek.