← Tüm yazılar

Render: Angular ve .NET uygulamasını Blueprint ile yayınlamak

Native Node ortamında Angular ve .NET API: build/start betikleri, portlar, ortam önceliği, health check, kalıcı veri ve hata incelemesi.

t.Turgay
Render: Angular ve .NET uygulamasını Blueprint ile yayınlamak

Bir portfolio için iki uygulama geliştirmek, iki ayrı sunucu kiralamayı gerektirmiyor. PortfolioBlog’da Angular/Node frontend ile .NET API’yi tek Docker imajında topladım. Render tek container çalıştırıyor; içerik Supabase PostgreSQL’de, yüklenen görseller Supabase Storage’da kalıyor. Bu rehber depodaki Dockerfile, render.yaml ve gerçek container testlerini anlatıyor. Yerel doğrulama, uzak Render dağıtımının tamamlandığı anlamına gelmiyor.

Tek adres, iki süreç

Dış isteği Node karşılıyor. Angular dosyaları, ilk HTML, canonical etiketleri ve sitemap frontend’in işi. /api, /uploads ve /health istekleri içerideki .NET API’ye aktarılıyor. Genel port PORT değişkeninden geliyor; varsayılan 8080. API yalnız 127.0.0.1:8081 dinliyor. Container’ın ağ adresinden bu API portuna bağlantı bile kabul edilmiyor.

Aynı origin, tarayıcı tarafında cookie ve CSRF akışını sadeleştiriyor. Güvenlik kararı yine API’de: Angular guard ekran geçişini düzenler, sunucudaki admin policy yerine geçmez. İki süreç aynı container’da diye frontend’e SQL bağlantısı veya Storage sunucu anahtarı vermiyorum.

Derleme aşamaları neden ayrı?

Angular derlenirken npm paketleri ve derleyici gerekir. API hazırlanırken .NET SDK gerekir. Bunların tamamını çalışan imajda taşımak gereksizdir. Aşağıdaki Dockerfile frontend, Node araç bağımlılıkları ve API için ayrı build aşamaları kullanıyor; son aşama yalnız çalıştırılacak dosyaları alıyor.

# syntax=docker/dockerfile:1

FROM node:24-bookworm-slim AS node

FROM node AS frontend-build
WORKDIR /src/frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --no-audit --no-fund
COPY frontend/ ./
RUN npm run build -- --configuration production

FROM node AS tooling
WORKDIR /src
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev --ignore-scripts --no-audit --no-fund

FROM mcr.microsoft.com/dotnet/sdk:10.0-noble AS api-build
WORKDIR /src
COPY Directory.Packages.props ./
COPY backend/PortfolioBlog.Core/PortfolioBlog.Core.csproj backend/PortfolioBlog.Core/
COPY backend/PortfolioBlog.Data/PortfolioBlog.Data.csproj backend/PortfolioBlog.Data/
COPY backend/PortfolioBlog.Business/PortfolioBlog.Business.csproj backend/PortfolioBlog.Business/
COPY backend/PortfolioBlog.WebApi/PortfolioBlog.WebApi.csproj backend/PortfolioBlog.WebApi/
RUN dotnet restore backend/PortfolioBlog.WebApi/PortfolioBlog.WebApi.csproj
COPY backend/ backend/
COPY config/supabase-ca.crt config/
RUN dotnet publish backend/PortfolioBlog.WebApi/PortfolioBlog.WebApi.csproj \
    --configuration Release --no-restore --no-self-contained \
    --output /out -p:UseAppHost=false

FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble AS runtime
# The official Node binary targets an older glibc and runs on Ubuntu Noble.
COPY --from=node /usr/local/bin/node /usr/local/bin/node
RUN apt-get update \
    && apt-get install -y --no-install-recommends libatomic1 libstdc++6 libgssapi-krb5-2 \
    && rm -rf /var/lib/apt/lists/* \
    && node --version
WORKDIR /app
ENV NODE_ENV=production \
    ASPNETCORE_ENVIRONMENT=Production \
    ASPNETCORE_HTTP_PORTS= \
    DOTNET_EnableDiagnostics=0 \
    PORT=8080 \
    START_API=1 \
    API_ORIGIN=http://127.0.0.1:8081 \
    API_DIRECTORY=/app/backend/PortfolioBlog.WebApi \
    API_EXECUTABLE=dotnet \
    API_ASSEMBLY=/app/backend/PortfolioBlog.WebApi/PortfolioBlog.WebApi.dll
COPY --from=api-build /out/ backend/PortfolioBlog.WebApi/
COPY --from=frontend-build /src/frontend/dist/portfolio-web/browser/ frontend/dist/portfolio-web/browser/
COPY --from=tooling /src/node_modules/ node_modules/
COPY package.json ./
COPY frontend/server/ frontend/server/
COPY frontend/shared/ frontend/shared/
COPY scripts/container-start.mjs scripts/
COPY scripts/lib/env.mjs scripts/lib/local-settings.mjs scripts/lib/database.mjs scripts/lib/setup-database.mjs scripts/lib/
COPY backend/PortfolioBlog.Data/db/ backend/PortfolioBlog.Data/db/
COPY content/site.json content/
COPY config/supabase-ca.crt config/
USER $APP_UID
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
    CMD node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/health',{signal:AbortSignal.timeout(4000)}).then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
ENTRYPOINT ["node", "scripts/container-start.mjs"]

Paket bildirimlerini kaynak koddan önce kopyalamak kasıtlı. Bir C# dosyası değiştiğinde NuGet paketlerini, bir Angular bileşeni değiştiğinde bütün npm paketlerini yeniden indirmek istemiyorum. npm ci lockfile’a uyar; publish yalnız WebApi projesini hedefler. WPF ve Prisma makale örnekleri bu imajın uygulamaları değildir.

API çıktısı framework-dependent: dotnet komutu son imajdaki ASP.NET 10 runtime’ını kullanır. SDK son imajda bulunmaz. Node 24’ün resmi Linux ikilisi aynı imaja alınır; libatomic1 ve libgssapi-krb5-2 dahil gereken sistem kütüphaneleri kurulur. Özellikle Npgsql bağlantısındaki eksik GSSAPI kütüphanesi uyarısını gerçek container çalıştırması ortaya çıkardı. “Bilgisayarımda çalışıyor” burada pek güçlü bir test raporu sayılmıyor.

USER $APP_UID ile uygulama root olmadan çalışır. EXPOSE 8080 yayınlanabilecek portu belgeler; API’yi gizleyen asıl karar onun loopback adresine bağlanmasıdır. EXPOSE satırı bir güvenlik duvarı değildir. Sağlık komutu da sabit port yerine süreçteki PORT değerini kullanır.

Build context ile runtime ayarını ayırmak

.dockerignore izin verilen kaynakları seçer; .env dosyaları, Git geçmişi, yedekler, node_modules ve eski build çıktıları context dışında kalır. Sonradan bir katmanda .env silmek yeterli olmaz: önceki katman hâlâ onu taşıyabilir. Burada sırlar hiçbir COPY veya ARG işlemine girmez.

Runtime ayarları şunlardır; değerler yalnız yer tutucudur:

ConnectionStrings__Portfolio=YOUR_SUPABASE_SESSION_POOLER_CONNECTION
Supabase__Url=https://YOUR_PROJECT_REF.supabase.co
Supabase__SecretKey=YOUR_SERVER_SECRET
Supabase__Bucket=portfolio-media
Admin__Username=YOUR_ADMIN_USERNAME
Admin__PasswordHash=YOUR_GENERATED_PBKDF2_HASH
Security__DataProtectionKeys=YOUR_GENERATED_KEY_RING
Database__Schema=public
# PUBLIC_ORIGIN=https://portfolio.example.com

SQL parolası, Storage sunucu anahtarı, PBKDF2 parola özeti ve cookie anahtar halkası farklı amaçlara hizmet eder. Security__DataProtectionKeys yeniden başlatmalarda aynı kalmalıdır. Her açılışta yeni anahtar üretirsem önceki admin oturumları ve CSRF token’ları çözülemez.

Render’da Environment → Add from .env ile üretim ayarları içe aktarılır. Depoda özel olarak tutulan .env.render imajın içine alınmaz ve kendi başına Render ortamını doldurmaz. Yerel Docker çalıştırmasında dosyayı salt okunur bağlayıp PORTFOLIO_ENV_FILE ile seçmek de mümkündür; komut kök README’dedir. Node dotenv ayrıştırıcısı tırnaklı değerleri okur. Süreç ortamı dosyadan önceliklidir.

Başlangıçta kurulum, sonra trafik

import path from "node:path";
import { fileURLToPath } from "node:url";
import { loadEnvironment } from "./lib/env.mjs";
import { validateProductionEnvironment } from "../frontend/server/configuration.mjs";
import { setupDatabase } from "./lib/setup-database.mjs";

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
loadEnvironment(root);
validateProductionEnvironment();
// Schema setup is explicit, transactional and preserves existing content.
console.log(JSON.stringify(await setupDatabase()));
await import("../frontend/server/start.mjs");

Önce eksik üretim ayarları kontrol edilir, sonra şema kurulum aracı çalışır. Altı içerik tablosu transaction ve advisory lock ile kurulur; seed yalnız boş profile tablosunda kullanılır. Mevcut yazı, yayın seçimi veya profil düzenlemesi tekrar başlangıçta ezilmez. API kendi başına tablo veya seed oluşturmaz.

Ardından Node, dotnet alt sürecini başlatır. API’nin /health adresini en fazla 60 saniye bekler; başarılı olmadan genel portu açmaz. API başlatılamazsa veya sonradan beklenmedik şekilde kapanırsa Node da başarısız çıkış koduyla sonlanır. Veri sunamayan bir uygulamanın yalnız vitrin ışıklarını açık bırakmasını istemiyorum.

SIGTERM geldiğinde yeni HTTP bağlantıları durdurulur ve API’ye kapanma sinyali iletilir. İşler takılırsa 15 saniyelik sınırdan sonra kalan bağlantılar ve çocuk süreç sonlandırılır. Yeniden başlatmayı Docker veya Render yönetir; uygulamanın içine ikinci bir sonsuz restart döngüsü koymuyorum.

Render Blueprint

# yaml-language-server: $schema=https://render.com/schema/render.yaml.json
services:
  - type: web
    name: portfolio-blog
    runtime: docker
    region: frankfurt
    plan: starter
    repo: https://github.com/turgaybudak/Porfolio
    branch: main
    autoDeployTrigger: commit
    dockerfilePath: ./Dockerfile
    dockerContext: .
    healthCheckPath: /health
    envVars:
      - key: ASPNETCORE_ENVIRONMENT
        value: Production
      - key: PORT
        value: "8080"
      - key: ConnectionStrings__Portfolio
        sync: false
      - key: Supabase__Url
        sync: false
      - key: Supabase__SecretKey
        sync: false
      - key: Admin__Username
        sync: false
      - key: Admin__PasswordHash
        sync: false
      - key: Security__DataProtectionKeys
        sync: false

runtime: docker, build kaynağının Dockerfile olduğunu söyler. dockerContext depo köküdür; frontend, backend ve ortak dosyalar aynı context içinde bulunur. buildCommand ve startCommand native ayarları kaldırılmıştır. Container’ın ENTRYPOINT tanımı kullanılır. sync: false alanlarına gerçek değerler Render Environment üzerinden verilir; YAML bunları içermez.

New → Blueprint ile depo ve main dalı seçilir. Mevcut servis native runtime kullanıyorsa Docker türünde servis oluşturulup alan adı ona taşınmalıdır. Eski build/start override’larını yeni servise kopyalamayın. Custom domain için PUBLIC_ORIGIN verilir; aksi hâlde Render’ın RENDER_EXTERNAL_URL değeri canonical kaynağıdır. İstekten gelen Host başlığı bu amaçla kullanılmaz.

Çalıştığını neyle doğruladım?

docker build -t portfolio-blog:local .
node scripts/verify-container.mjs portfolio-blog:local

# Read-only verification of an existing deployment (PowerShell):
$env:PUBLIC_ORIGIN = 'https://YOUR_SERVICE.onrender.com'
node scripts/check-deployment.mjs

verify-container gerçek imajı çalıştırır; kendine ait geçici Supabase şemasını ve geçici admin bilgilerini kullanır. Ana şemayı değiştirmez, Storage’a görsel yüklemez. 42 yazı ve 4 projenin görünmesini, iki dilde makale HTML’ini, sitemap’i ve altılı sayfalamayı denetler. Admin okuması girişsiz 401, CSRF’siz yazma 400, eski revizyonla kayıt 409 dönmelidir.

Test ayrıca bir profil alanını değiştirir, container’ı yeniden başlatır ve değişikliğin korunduğunu okur. Aynı cookie ve CSRF token’ıyla tekrar kayıt yapar. API süreci zorla öldürüldüğünde container hata koduyla kapanmalıdır; normal docker stop ise SIGKILL’e ihtiyaç duymadan tamamlanmalıdır. Root olmayan kullanıcı, SDK ve .env dosyalarının imajda bulunmaması, özel API portunun erişilememesi de kontrol edilir.

/health API’ye erişildiğini gösterir; her çağrıda veritabanı sorgulamaz. Sonradan kopan bir SQL bağlantısını sürekli sınayan readiness endpoint’i olarak yorumlamıyorum. Bu yüzden canlı kontrol betiği /api/site ve makale okumalarını da yapar. Yönetici cookie’si üretimde Secure ve HttpOnly’dur; tarayıcıyla admin girişi HTTPS üzerinden kullanılmalıdır.

Hata olduğunda bakılacak yer

Build hatasında ilk npm, Angular, restore veya publish hatasını okuyun. Başlangıç hatasında ayar adlarını, CA sertifikasını ve Supabase Session pooler bağlantısını kontrol edin. 6543 transaction pooler bu kalıcı backend için kullanılmaz. ConnectionStrings__Portfolio eksik mesajı, veritabanının kapalı olduğunu kanıtlamaz; önce ayarın sürece ulaşıp ulaşmadığına bakılır.

Yeni commit’i geri almak, veritabanını geri almak değildir. İçerik PostgreSQL’de, görseller Storage’da ve oturum anahtarları runtime ayarlarında yaşar. Sağlıklı bir geri dönüşte bu üçü birlikte değerlendirilir. Container imajını yeniden üretmek kolay olmalı; kullanıcı verisini yeniden üretmeye çalışmak ise yedekleme planı değildir.

Başvurular

✳ Bu yörüngenin sonuna geldin.
Diğer yazıları keşfet →
turgay.Biraz kod, biraz piksel, bolca merak.© 2026 · Dünya’dan sevgilerle ✳