# Battani kurulum betiği (Windows + Docker Desktop) — https://battani.dev/install.ps1 # # Tek komutla kurulum (PowerShell): # irm https://battani.dev/install.ps1 | iex # # Seçenekli kullanım: # & ([scriptblock]::Create((irm https://battani.dev/install.ps1))) -Port 4000 -Version 1.2.3 # # Parametreler (ortam değişkeni karşılığı parantez içinde): # -Version X.Y.Z Kurulacak sürüm; varsayılan en yeni sürüm (BATTANI_VERSION) # -Dir YOL Kurulum klasörü; varsayılan %USERPROFILE%\Battani (BATTANI_DIR) # -Port N Web arayüzü portu; varsayılan 4000 (BATTANI_PORT) # -Url URL Dışarıdan erişilen adres, ör. https://k8s.firma.com (BATTANI_URL) # -NoRegistry Gömülü Docker Registry'yi kurma (BATTANI_REGISTRY=0) # -Yes Soru sorma (BATTANI_YES=1) # # Docker Desktop (Linux container modu) gerekir. Betik docker-compose.yml ve # rastgele parolalarla .env yazar, imajı çeker, servisleri başlatır ve uygulama # sağlıklı olana kadar bekler. Veritabanı compose içindeki PostgreSQL'dir. # Gövde kendi kapsamında çalışır: `irm | iex` ile de kullanıcının oturumuna # değişken ya da ErrorActionPreference sızmaz. @args parametreleri iletir. & { param( [string]$Version = $env:BATTANI_VERSION, [string]$Dir = $(if ($env:BATTANI_DIR) { $env:BATTANI_DIR } else { Join-Path $HOME 'Battani' }), [string]$Port = $(if ($env:BATTANI_PORT) { $env:BATTANI_PORT } else { '4000' }), [string]$Url = $env:BATTANI_URL, [switch]$NoRegistry = ($env:BATTANI_REGISTRY -eq '0'), [switch]$Yes = ($env:BATTANI_YES -eq '1') ) $ErrorActionPreference = 'Stop' $PortalUrl = if ($env:BATTANI_PORTAL_URL) { $env:BATTANI_PORTAL_URL } else { 'https://battani.dev' } $Image = if ($env:BATTANI_IMAGE) { $env:BATTANI_IMAGE } else { 'ghcr.io/buraksumer/battani' } $RegistryPort = if ($env:BATTANI_REGISTRY_PORT) { $env:BATTANI_REGISTRY_PORT } else { '5000' } $SkipPull = $env:BATTANI_SKIP_PULL -eq '1' [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 function Say([string]$m) { Write-Host '==> ' -ForegroundColor Green -NoNewline; Write-Host $m } function Warn([string]$m) { Write-Host "!! $m" -ForegroundColor Yellow } function Fail([string]$m) { Write-Host "HATA: $m" -ForegroundColor Red; throw $m } function Confirm-Step([string]$q) { if ($Yes) { return $true } $a = Read-Host "$q [E/h]" return -not ($a -match '^(h|n|hayir|hayır|no)$') } # Docker'ı çalıştırır; çıktıyı ekrana yazar ve çıkış kodunu döner. # (PowerShell 5.1 stderr'i hata sayıp betiği durdurmasın diye.) function Invoke-Docker([string[]]$DockerArgs, [switch]$Quiet) { $old = $ErrorActionPreference; $ErrorActionPreference = 'Continue' try { if ($Quiet) { & docker @DockerArgs *> $null } else { & docker @DockerArgs 2>&1 | ForEach-Object { "$_" } | Out-Host } return $LASTEXITCODE } finally { $ErrorActionPreference = $old } } function Get-DockerOutput([string[]]$DockerArgs) { $old = $ErrorActionPreference; $ErrorActionPreference = 'Continue' try { $out = & docker @DockerArgs 2>$null if ($LASTEXITCODE -ne 0) { return $null } return ($out | Out-String).Trim() } finally { $ErrorActionPreference = $old } } function New-Secret([int]$Length) { $chars = [char[]]'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' $bytes = New-Object byte[] ($Length * 2) $rng = New-Object System.Security.Cryptography.RNGCryptoServiceProvider $rng.GetBytes($bytes); $rng.Dispose() $sb = New-Object System.Text.StringBuilder foreach ($b in $bytes) { if ($b -lt 248) { [void]$sb.Append($chars[$b % 62]) } # 248 = 62*4: sapmasız if ($sb.Length -eq $Length) { break } } while ($sb.Length -lt $Length) { [void]$sb.Append($chars[(Get-Random -Maximum 62)]) } return $sb.ToString() } # UTF-8 (BOM'suz) ve LF: Compose .env'in ilk satırındaki BOM'u anahtar sanar. function Write-Utf8File([string]$Path, [string]$Text) { [System.IO.File]::WriteAllText($Path, ($Text -replace "`r`n", "`n"), (New-Object System.Text.UTF8Encoding $false)) } function Get-LatestVersion { if (-not $Image.StartsWith('ghcr.io/')) { return $null } $repo = $Image.Substring('ghcr.io/'.Length) try { $token = (Invoke-RestMethod -UseBasicParsing "https://ghcr.io/token?scope=repository:${repo}:pull").token $tags = (Invoke-RestMethod -UseBasicParsing -Headers @{ Authorization = "Bearer $token" } "https://ghcr.io/v2/$repo/tags/list?n=10000").tags } catch { return $null } $semver = @($tags | Where-Object { $_ -match '^\d+\.\d+\.\d+$' }) if ($semver.Count -eq 0) { return $null } return ($semver | Sort-Object { [version]$_ } | Select-Object -Last 1) } function Wait-Healthy([string]$Container, [int]$Seconds) { $elapsed = 0 while ($elapsed -lt $Seconds) { $info = Get-DockerOutput @('inspect', '-f', '{{.State.Status}} {{.RestartCount}} {{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}', $Container) if (-not $info) { return $false } $state, $restarts, $health = $info -split ' ' if ($health -eq 'healthy') { return $true } if ($state -in @('exited', 'dead')) { return $false } if ([int]$restarts -ge 3) { return $false } Start-Sleep -Seconds 5 $elapsed += 5 } return $false } Write-Host '' Write-Host 'Battani kurulumu' -ForegroundColor White Write-Host '' # ------------------------------------------------------------------ ön kontroller if ($Port -notmatch '^\d+$') { Fail "Geçersiz port: $Port" } if ($env:PROCESSOR_ARCHITECTURE -ne 'AMD64') { Warn "Battani imajı x86_64 (amd64) için yayınlanıyor; bu makine: $($env:PROCESSOR_ARCHITECTURE). Docker Desktop emülasyonla çalıştırmayı dener." } if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { Warn 'Docker Desktop kurulu değil.' if ((Get-Command winget -ErrorAction SilentlyContinue) -and (Confirm-Step "Docker Desktop'ı winget ile kurayım mı?")) { winget install -e --id Docker.DockerDesktop --accept-package-agreements --accept-source-agreements Fail 'Docker Desktop kuruldu. Bilgisayarı yeniden başlatıp Docker Desktop''ı açın, sonra bu komutu tekrar çalıştırın.' } Fail 'Docker Desktop gerekli: https://www.docker.com/products/docker-desktop/' } if ((Invoke-Docker @('info') -Quiet) -ne 0) { Fail 'Docker çalışmıyor. Docker Desktop''ı başlatıp tekrar deneyin.' } if ((Get-DockerOutput @('info', '--format', '{{.OSType}}')) -ne 'linux') { Fail 'Docker Desktop Windows container modunda. Tepsi simgesinden "Switch to Linux containers" seçin.' } if ((Invoke-Docker @('compose', 'version') -Quiet) -ne 0) { Fail 'Docker Compose bulunamadı; Docker Desktop''ı güncelleyin.' } if (Test-Path (Join-Path $Dir '.env')) { Fail "$Dir içinde zaten bir kurulum var. Güncellemek için: irm $PortalUrl/update.ps1 | iex" } $Version = "$Version".TrimStart('v') if (-not $Version) { Say 'En yeni sürüm aranıyor…' $Version = Get-LatestVersion if (-not $Version) { Fail 'En yeni sürüm bulunamadı; -Version ile belirtin.' } } Say "Kurulacak sürüm: $Version" if (-not $Url) { $Url = "http://localhost:$Port" } $Url = $Url.TrimEnd('/') $publicHost = ([uri]$Url).Host # ------------------------------------------------------------------------ dosyalar Say "Dosyalar yazılıyor: $Dir" New-Item -ItemType Directory -Force -Path (Join-Path $Dir 'backups') | Out-Null $adminPassword = New-Secret 20 # Lisans kurulum kimliği bu bilgisayarın kimliğine (MachineGuid) bağlanır. $hostIdFile = Join-Path $Dir 'host-id' if (-not (Test-Path $hostIdFile)) { $machineGuid = $null try { $machineGuid = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Cryptography' -Name MachineGuid -ErrorAction Stop).MachineGuid } catch { } if (-not $machineGuid) { $machineGuid = [guid]::NewGuid().ToString() } [System.IO.File]::WriteAllText($hostIdFile, $machineGuid, (New-Object System.Text.UTF8Encoding $false)) } $profiles = if ($NoRegistry) { '' } else { 'registry' } $now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm') + ' UTC' Write-Utf8File (Join-Path $Dir '.env') @" # Battani — install.ps1 tarafından $now tarihinde üretildi. # Parolalar içerir: kimseyle paylaşmayın, silmeyin (veritabanı parolası buradadır). # Sürüm — update.ps1 bu satırı günceller. BATTANI_IMAGE=$Image BATTANI_IMAGE_TAG=$Version # Erişim BATTANI_HTTP_PORT=$Port BATTANI_REGISTRY_PORT=$RegistryPort FRONTEND_URL=$Url # Gömülü PostgreSQL DB_NAME=battani DB_USER=battani DB_PASSWORD=$(New-Secret 32) # Uygulama sırları JWT_SECRET=$(New-Secret 64) REGISTRY_SECRETS_KEY=$(New-Secret 64) # Veri servisi bağlantılarına elle girilen kimlik bilgilerini şifreler; sonradan değiştirmeyin. INTEGRATION_SECRETS_KEY=$(New-Secret 64) # İlk yönetici (yalnız ilk açılışta oluşturulur; giriş yaptıktan sonra parolayı değiştirin) INITIAL_ADMIN_USERNAME=admin INITIAL_ADMIN_EMAIL=admin@local INITIAL_ADMIN_PASSWORD=$adminPassword # Docker Registry (registry profili). Adres Docker istemcilerinden erişilebilir olmalı. REGISTRY_TOKEN_REALM=$Url/api/docker-registry/token REGISTRY_PUBLIC_HOST=${publicHost}:$RegistryPort COMPOSE_PROFILES=$profiles # Lisans kurulum kimliğinin bağlandığı bilgisayar kimliği (değiştirmeyin). BATTANI_HOST_ID_FILE=./host-id "@ # .env yalnız bu kullanıcı ve yöneticiler tarafından okunabilsin. try { $envPath = Join-Path $Dir '.env' $acl = Get-Acl $envPath $acl.SetAccessRuleProtection($true, $false) foreach ($id in @([System.Security.Principal.WindowsIdentity]::GetCurrent().Name, 'BUILTIN\Administrators', 'NT AUTHORITY\SYSTEM')) { $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($id, 'FullControl', 'Allow'))) } Set-Acl $envPath $acl } catch { Warn ".env izinleri kısıtlanamadı: $($_.Exception.Message)" } Write-Utf8File (Join-Path $Dir 'docker-compose.yml') @' # Battani — install.ps1 tarafından üretildi. Değerler .env dosyasından gelir. # Yönetim: docker compose ps | docker compose logs -f app | docker compose restart app # Güncelleme: irm https://battani.dev/update.ps1 | iex name: battani services: postgres: image: postgres:16-alpine container_name: battani-postgres environment: POSTGRES_DB: ${DB_NAME} POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - postgres-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 10s timeout: 3s retries: 10 start_period: 10s restart: unless-stopped networks: - battani app: image: ${BATTANI_IMAGE}:${BATTANI_IMAGE_TAG} container_name: battani-app environment: NODE_ENV: production PORT: "4000" FRONTEND_URL: ${FRONTEND_URL} DB_PROVIDER: postgres DB_SERVER: postgres DB_PORT: "5432" DB_NAME: ${DB_NAME} DB_USER: ${DB_USER} DB_PASSWORD: ${DB_PASSWORD} DB_SSL: "false" JWT_SECRET: ${JWT_SECRET} INITIAL_ADMIN_USERNAME: ${INITIAL_ADMIN_USERNAME} INITIAL_ADMIN_EMAIL: ${INITIAL_ADMIN_EMAIL} INITIAL_ADMIN_PASSWORD: ${INITIAL_ADMIN_PASSWORD} REGISTRY_SECRETS_KEY: ${REGISTRY_SECRETS_KEY} INTEGRATION_SECRETS_KEY: ${INTEGRATION_SECRETS_KEY} REGISTRY_TOKEN_BUNDLE_PATH: /app/config/registry-token-bundle.pem REGISTRY_PUBLIC_HOST: ${REGISTRY_PUBLIC_HOST} REGISTRY_PUBLIC_REALM_URL: ${REGISTRY_TOKEN_REALM} BATTANI_UPDATE_IMAGE: ${BATTANI_IMAGE} BATTANI_IMAGE_TAG: ${BATTANI_IMAGE_TAG} ports: - "${BATTANI_HTTP_PORT}:4000" volumes: - app-config:/app/config # Host identity for the license installation ID (read-only). - ${BATTANI_HOST_ID_FILE}:/etc/host-machine-id:ro depends_on: postgres: condition: service_healthy security_opt: - no-new-privileges:true cap_drop: - ALL restart: unless-stopped networks: - battani registry: image: registry:3 container_name: battani-registry profiles: ["registry"] environment: REGISTRY_AUTH: token REGISTRY_AUTH_TOKEN_REALM: ${REGISTRY_TOKEN_REALM} REGISTRY_AUTH_TOKEN_SERVICE: battani-registry REGISTRY_AUTH_TOKEN_ISSUER: battani-registry REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: /auth/registry-token-bundle.pem REGISTRY_STORAGE_DELETE_ENABLED: "true" ports: - "${BATTANI_REGISTRY_PORT}:5000" volumes: - registry-data:/var/lib/registry - app-config:/auth:ro depends_on: app: condition: service_healthy restart: unless-stopped networks: - battani networks: battani: volumes: postgres-data: registry-data: app-config: '@ # -------------------------------------------------------------------------- başlat $composeArgs = @('compose', '--project-directory', $Dir) if (-not $SkipPull) { Say 'İmajlar indiriliyor (ilk seferde birkaç dakika sürebilir)…' if ((Invoke-Docker ($composeArgs + @('pull', '--quiet'))) -ne 0) { Fail 'İmajlar indirilemedi.' } } Say 'Servisler başlatılıyor…' if ((Invoke-Docker ($composeArgs + @('up', '-d'))) -ne 0) { Warn 'docker compose up hata verdi; uygulamanın durumu kontrol ediliyor…' } Say 'Uygulamanın açılması bekleniyor (veritabanı hazırlanıyor)…' if (-not (Wait-Healthy 'battani-app' 300)) { Warn 'Uygulama 5 dakika içinde sağlıklı hale gelmedi. Son loglar:' Invoke-Docker @('logs', '--tail', '40', 'battani-app') | Out-Null Fail "Kurulum tamamlanamadı. Loglar: docker compose --project-directory `"$Dir`" logs -f app" } Write-Host '' Write-Host "Battani $Version kuruldu." -ForegroundColor Green Write-Host '' Write-Host " Adres : $Url" Write-Host ' Kullanıcı : admin' Write-Host " Parola : $adminPassword (ilk girişten sonra değiştirin)" Write-Host " Lisans : Açılışta şirket adınızı girin; oluşan Installation ID ile $PortalUrl üzerinden lisans alın" Write-Host " Klasör : $Dir (.env dosyasını yedekleyin)" Write-Host " Loglar : docker compose --project-directory `"$Dir`" logs -f app" Write-Host " Güncelleme : irm $PortalUrl/update.ps1 | iex" Write-Host '' } @args