| 1 | # Configures and starts a self-hosted Ghost on Windows. Safe to re-run: existing |
| 2 | # answers in .env become the defaults, and generated secrets are never regenerated. |
| 3 | # powershell -ExecutionPolicy Bypass -File docker\setup.ps1 |
| 4 | $ErrorActionPreference = 'Stop |
| 5 | Set-Location $PSScriptRoot |
| 6 | $EnvFile = Join-Path |
| 7 | |
| 8 | function Die($msg |
| 9 | function Say($msg |
| 10 | |
| 11 | if (-not (Get-Command docker |
| 12 | docker compose version *> $null; if |
| 13 | docker info *> $null; if |
| 14 | |
| 15 | $c = @{} |
| 16 | if (Test-Path $EnvFile |
| 17 | foreach ($line |
| 18 | if ($line |
| 19 | } |
| 20 | $keep = Read-Host |
| 21 | if ($keep |
| 22 | } |
| 23 | |
| 24 | function Ask($name |
| 25 | $cur = if |
| 26 | $suffix = if |
| 27 | $ans = Read-Host |
| 28 | $c[$name |
| 29 | } |
| 30 | function AskSecret($name |
| 31 | $suffix = if |
| 32 | $s = Read-Host |
| 33 | $ans = [ |
| 34 | if ($ans) |
| 35 | } |
| 36 | function AskYesNo($question |
| 37 | $ans = Read-Host |
| 38 | if (-not $ans |
| 39 | return $ans -match |
| 40 | } |
| 41 | function RandomBytes($n |
| 42 | $b = New-Object |
| 43 | [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes( |
| 44 | return ,$b |
| 45 | } |
| 46 | function Random64($n |
| 47 | function Hex($n |
| 48 | |
| 49 | Say 'Where will Ghost live?' |
| 50 | Write-Host 'A domain (git.example.com) gets HTTPS on ports 80/443, with api. and docs. subdomains.' |
| 51 | Write-Host 'An IP address or localhost serves plain HTTP on ports 3000 (web), 3001 (api), 3003 (docs).' |
| 52 | $lastHost = if |
| 53 | Ask HOST 'Domain or IP' $lastHost |
| 54 | $h = ($ |
| 55 | Ask GIT_SSH_PORT 'SSH port for git clone/push' '1031' |
| 56 | |
| 57 | if ($h -eq |
| 58 | $c.DOMAIN = |
| 59 | $c.WEB_APP_URL = |
| 60 | } else { |
| 61 | $c.DOMAIN = |
| 62 | $c.WEB_APP_URL = |
| 63 | Write-Host "Point DNS A/AAAA records for $h |
| 64 | } |
| 65 | $c.SSH_CLONE_HOST = |
| 66 | |
| 67 | Say 'Email (required: verification, password resets, notifications)' |
| 68 | Write-Host ' 1) SMTP server' |
| 69 | Write-Host ' 2) HTTP email relay (EMAIL_PROXY), also lets apps/delivery retry emails' |
| 70 | Ask MAIL_KIND 'Choose 1 or 2' $(if ( |
| 71 | if ($c.MAIL_KIND |
| 72 | $c.MAIL_HOST = |
| 73 | do { Ask EMAIL_PROXY 'Relay URL' } |
| 74 | AskSecret EMAIL_PROXY_SECRET 'Relay secret (empty for none)' |
| 75 | $c.DELIVERY_EMAIL = |
| 76 | } else { |
| 77 | $c.EMAIL_PROXY = |
| 78 | do { Ask MAIL_HOST 'SMTP host' } |
| 79 | Ask MAIL_PORT 'SMTP port' '587' |
| 80 | Ask MAIL_USER 'SMTP username' |
| 81 | do { AskSecret MAIL_PASSWORD 'SMTP password' } |
| 82 | $c.MAIL_SECURE = |
| 83 | } |
| 84 | Ask EMAIL_SENDER 'Send email as' "Ghost <noreply@$(if |
| 85 | $c.EMAIL_VERIFICATION_ENABLED = |
| 86 | |
| 87 | Say 'Storage' |
| 88 | if (AskYesNo 'Use the bundled S3 storage (RustFS)? Say no for R2, Tigris, MinIO...') |
| 89 | $c.COMPOSE_PROFILES = |
| 90 | if ($c.S3_ENDPOINT |
| 91 | $c.S3_ENDPOINT = |
| 92 | if (-not $c |
| 93 | if (-not $c |
| 94 | } else { |
| 95 | if ($c.S3_ENDPOINT |
| 96 | Ask S3_ENDPOINT 'S3 endpoint URL' |
| 97 | Ask S3_BUCKET 'Bucket' 'ghost' |
| 98 | Ask S3_ACCESS_KEY_ID 'Access key ID' |
| 99 | AskSecret S3_SECRET_ACCESS_KEY 'Secret access key' |
| 100 | } |
| 101 | |
| 102 | Say 'GitHub imports (optional)' |
| 103 | Write-Host 'Lets people import a GitHub repository with its code, releases, issues and pull requests.' |
| 104 | Write-Host 'Needs a GitHub OAuth app: https://github.com/settings/applications/new' |
| 105 | Write-Host " Homepage URL: $($ |
| 106 | Write-Host " Callback URL: $($ |
| 107 | if (AskYesNo 'Turn on GitHub imports?' ([bool |
| 108 | $c.COMPOSE_PROFILES = |
| 109 | do { Ask GITHUB_CLIENT_ID 'OAuth app client ID' } |
| 110 | do { AskSecret GITHUB_CLIENT_SECRET 'OAuth app client secret' } |
| 111 | $c.IMPORTER_URL = |
| 112 | if (-not $c |
| 113 | } else { |
| 114 | $c.GITHUB_CLIENT_ID = |
| 115 | } |
| 116 | |
| 117 | Say 'Secrets (press enter to generate)' |
| 118 | # Existing ones are kept: a new auth secret signs everyone out, a new webhook key |
| 119 | # makes every stored webhook secret unreadable, a new host key warns every client. |
| 120 | if (-not $c |
| 121 | if (-not $c |
| 122 | if (-not $c |
| 123 | if (-not $c |
| 124 | if (-not $c |
| 125 | if (-not $c |
| 126 | if (-not $c |
| 127 | Write-Host 'Generating an SSH host key...' |
| 128 | # No double quotes in the script: Windows PowerShell mangles them on the way to docker. |
| 129 | $c.GIT_SSH_HOST_KEY = |
| 130 | if ($LASTEXITCODE -or -not |
| 131 | } |
| 132 | |
| 133 | foreach ($v in |
| 134 | # ponytail: .env values are single-quoted so $ and spaces are literal; a quote would need an escape compose lacks. |
| 135 | if ("$($ |
| 136 | } |
| 137 | |
| 138 | $c.DATABASE_URL = |
| 139 | $c.BETTER_AUTH_URL = |
| 140 | $c.AUTH_TRUSTED_ORIGINS = |
| 141 | $c.ZOEKT_URL = |
| 142 | $keys = 'COMPOSE_PROFILES |
| 143 | 'POSTGRES_PASSWORD', 'BETTER_AUTH_SECRET', |
| 144 | 'GIT_SSH_HOST_KEY', 'GIT_SSH_PORT', |
| 145 | 'EMAIL_SENDER', 'EMAIL_VERIFICATION_ENABLED', |
| 146 | 'MAIL_HOST', 'MAIL_PORT', |
| 147 | 'GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET', |
| 148 | 'DATABASE_URL', 'BETTER_AUTH_URL', |
| 149 | $lines = @( |
| 150 | # Empty values are left out: the API reads `EMAIL_PROXY=''` as set. |
| 151 | $lines += $ |
| 152 | # No BOM and LF endings: compose reads the file byte for byte. |
| 153 | [IO.File]::WriteAllText($EnvFile |
| 154 | |
| 155 | Say 'Building and starting Ghost. The first build takes several minutes.' |
| 156 | docker compose up -d --build |
| 157 | if ($LASTEXITCODE) { Die ' |
| 158 | |
| 159 | Say 'Ghost is up.' |
| 160 | Write-Host " Web $($ |
| 161 | Write-Host " API $($ |
| 162 | Write-Host " Docs $($ |
| 163 | Write-Host " SSH ssh://git@$($ |
| 164 | Write-Host "`nConfiguration is in $EnvFile |