Deployment script updates
This commit is contained in:
+130
-34
@@ -1,45 +1,141 @@
|
|||||||
# Deployment
|
# Deployment
|
||||||
|
|
||||||
## What the scripts do
|
## Server layout confirmed
|
||||||
|
|
||||||
- `scripts/migrate-wordpress.ps1`
|
The production server currently runs multiple separate Docker Compose projects:
|
||||||
- Dumps the existing WordPress MySQL database to `migration-backups/<timestamp>/wordpress.sql`
|
|
||||||
- Copies `wp-content/uploads` out of the legacy WordPress container into `static/wp-content/uploads`
|
|
||||||
- Keeps an archive copy of the uploads in `migration-backups/<timestamp>/uploads`
|
|
||||||
|
|
||||||
- `scripts/deploy.ps1`
|
- Main public site WordPress stack:
|
||||||
- Optionally runs the migration step first
|
- project: `goodwalkconz`
|
||||||
- Optionally shuts down the legacy compose stack
|
- path: `/docker/wordpress/goodwalk.co.nz`
|
||||||
- Validates the new compose file
|
- Onboarding WordPress stack:
|
||||||
- Builds and starts the new stack
|
- project: `onboardinggoodwalkconz`
|
||||||
- Waits for `http://localhost/api/health` to return success
|
- path: `/docker/wordpress/onboarding.goodwalk.co.nz`
|
||||||
|
- Shared nginx:
|
||||||
|
- project: `nginx`
|
||||||
|
- path: `/docker/nginx`
|
||||||
|
- Shared mysql:
|
||||||
|
- project: `mysql`
|
||||||
|
- path: `/docker/mysql`
|
||||||
|
|
||||||
## Before cutover
|
The deployment scripts in this repo are set up to deploy the new Svelte site as a
|
||||||
|
separate stack at:
|
||||||
|
|
||||||
1. Fill in `.env` from `.env.example`
|
- remote path: `/docker/goodwalk-svelte`
|
||||||
2. Make sure the legacy WordPress stack is still running
|
- compose file: `docker-compose.prod.yml`
|
||||||
3. Identify:
|
- docker project: `goodwalk-svelte`
|
||||||
- the legacy WordPress container name
|
|
||||||
- the legacy MySQL container name
|
|
||||||
- the legacy compose file path if you want the deploy script to shut it down for you
|
|
||||||
- the WordPress MySQL database name, user, and password
|
|
||||||
|
|
||||||
## Example
|
This leaves the onboarding site, shared nginx, shared mysql, and other unrelated
|
||||||
|
containers untouched.
|
||||||
|
|
||||||
```powershell
|
## Files involved
|
||||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy.ps1 `
|
|
||||||
-RunMigration `
|
- [deploy.ps1](deploy.ps1)
|
||||||
-LegacyComposeFile C:\deploy\wordpress\docker-compose.yml `
|
- Windows entrypoint for packaging the repo, uploading it, and running the
|
||||||
-LegacyProjectName goodwalk-wordpress `
|
remote deployment helper over SSH.
|
||||||
-LegacyWordPressContainer goodwalk-wordpress-1 `
|
- [scripts/deploy-remote.sh](scripts/deploy-remote.sh)
|
||||||
-LegacyDatabaseContainer goodwalk-db-1 `
|
- Server-side helper that updates only the `goodwalk-svelte` compose project.
|
||||||
-MySqlDatabase wordpress `
|
- [docker-compose.prod.yml](docker-compose.prod.yml)
|
||||||
-MySqlUser wordpress `
|
- Production compose file for the new Svelte app, mail API, and Postgres.
|
||||||
-MySqlPassword 'replace-me'
|
- [ssh-config](ssh-config)
|
||||||
|
- Repo-local SSH config used by the deployment script.
|
||||||
|
- [nginx/goodwalk.co.nz.svelte.conf.example](nginx/goodwalk.co.nz.svelte.conf.example)
|
||||||
|
- Example shared-nginx config for routing the main public site to the new
|
||||||
|
Svelte app and mail API.
|
||||||
|
|
||||||
|
## First-time server preparation
|
||||||
|
|
||||||
|
1. Fill in [ssh-config](ssh-config) with the real host details.
|
||||||
|
|
||||||
|
2. Create the deployment directory on the server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /docker/goodwalk-svelte
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notes
|
3. The first deployment will auto-create the production env file on the server at:
|
||||||
|
|
||||||
- The new app now uses root-relative `/wp-content/uploads/...` paths, so the copied uploads are served by the SvelteKit stack after cutover.
|
```bash
|
||||||
- The deployment script does not destroy the legacy database dump. It writes a fresh backup on every migration run.
|
/docker/goodwalk-svelte/.env
|
||||||
- If you want to keep the legacy stack running while testing, omit `-LegacyComposeFile` or add `-SkipLegacyShutdown`.
|
```
|
||||||
|
|
||||||
|
It is created from [deploy.env.template](deploy.env.template). Current template contents:
|
||||||
|
|
||||||
|
```env
|
||||||
|
POSTGRES_DB=goodwalk
|
||||||
|
POSTGRES_USER=goodwalk
|
||||||
|
POSTGRES_PASSWORD=gw_Pg_7Jm9!Qx4#Ld2@Vr8
|
||||||
|
|
||||||
|
RESEND_API_KEY=replace-me
|
||||||
|
OWNER_EMAIL=replace-me
|
||||||
|
FROM_EMAIL=GoodWalk <bookings@goodwalk.co.nz>
|
||||||
|
REPLY_TO=aless@goodwalk.co.nz
|
||||||
|
|
||||||
|
FORM_MIN_SECONDS=4
|
||||||
|
FORM_MAX_SECONDS=7200
|
||||||
|
RATE_LIMIT_WINDOW_SECONDS=900
|
||||||
|
RATE_LIMIT_MAX_PER_IP=5
|
||||||
|
RATE_LIMIT_MAX_PER_EMAIL=3
|
||||||
|
RATE_LIMIT_MIN_INTERVAL_SECONDS=20
|
||||||
|
```
|
||||||
|
|
||||||
|
After the first deploy, edit `/docker/goodwalk-svelte/.env` on the server and replace:
|
||||||
|
|
||||||
|
- `RESEND_API_KEY=replace-me`
|
||||||
|
- `OWNER_EMAIL=replace-me`
|
||||||
|
|
||||||
|
4. Confirm the shared Docker network already exists:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network ls | grep webnet
|
||||||
|
```
|
||||||
|
|
||||||
|
Your server already uses `webnet`, so this should already be present.
|
||||||
|
|
||||||
|
## First deploy
|
||||||
|
|
||||||
|
From Windows PowerShell in the repo root:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\deploy.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Or skip the confirmation prompt:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\deploy.ps1 -Force
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cutover nginx
|
||||||
|
|
||||||
|
After the new Svelte stack is up and healthy, update the shared nginx config on
|
||||||
|
the server for the main site.
|
||||||
|
|
||||||
|
Current live file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/docker/nginx/conf.d/goodwalk.co.nz.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the repo example as the new target config:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nginx/goodwalk.co.nz.svelte.conf.example
|
||||||
|
```
|
||||||
|
|
||||||
|
Then reload nginx:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -p nginx -f /docker/nginx/docker-compose.yml exec nginx nginx -t
|
||||||
|
docker compose -p nginx -f /docker/nginx/docker-compose.yml exec nginx nginx -s reload
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important notes
|
||||||
|
|
||||||
|
- Do not deploy the top-level `docker-compose.yml` to this server for production.
|
||||||
|
It includes its own nginx service and does not match the shared nginx setup on
|
||||||
|
the host.
|
||||||
|
- The deployment scripts do not stop or remove the onboarding WordPress stack.
|
||||||
|
- The deployment scripts do not touch the shared mysql compose project.
|
||||||
|
- The deployment scripts preserve the remote `.env` file.
|
||||||
|
- The site check in `deploy.ps1` targets `https://www.goodwalk.co.nz/api/health`.
|
||||||
|
Before nginx cutover, use `-SkipSiteCheck` or expect that check to fail.
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
POSTGRES_DB=goodwalk
|
||||||
|
POSTGRES_USER=goodwalk
|
||||||
|
POSTGRES_PASSWORD=gw_Pg_7Jm9!Qx4#Ld2@Vr8
|
||||||
|
|
||||||
|
RESEND_API_KEY=re_hcDByLp8_HEBW93wDirr7o9g16FgCeYNF
|
||||||
|
OWNER_EMAIL=mattcohen0@gmail.com
|
||||||
|
FROM_EMAIL=GoodWalk <info@goodwalk.co.nz>
|
||||||
|
REPLY_TO=mattcohen0@gmail.com
|
||||||
|
|
||||||
|
FORM_MIN_SECONDS=4
|
||||||
|
FORM_MAX_SECONDS=7200
|
||||||
|
RATE_LIMIT_WINDOW_SECONDS=900
|
||||||
|
RATE_LIMIT_MAX_PER_IP=5
|
||||||
|
RATE_LIMIT_MAX_PER_EMAIL=3
|
||||||
|
RATE_LIMIT_MIN_INTERVAL_SECONDS=20
|
||||||
+241
@@ -0,0 +1,241 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[switch]$Force,
|
||||||
|
[switch]$SkipSiteCheck
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Goodwalk production deployment settings
|
||||||
|
# Update these values before the first real deployment.
|
||||||
|
# This script targets the main Svelte Goodwalk stack that uses the top-level
|
||||||
|
# docker-compose.yml. It does not touch the legacy WordPress compose files.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
$SshUser = 'root'
|
||||||
|
$ServerHost = 'gw-prod'
|
||||||
|
# Leave blank to use interactive password prompts instead of an SSH key.
|
||||||
|
$SshKeyPath = ''
|
||||||
|
$LocalProjectPath = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$SshConfigPath = Join-Path $LocalProjectPath 'ssh-config'
|
||||||
|
$RemoteDeploymentPath = '/docker/goodwalk-svelte'
|
||||||
|
$ComposeFileName = 'docker-compose.prod.yml'
|
||||||
|
$DockerProjectName = 'goodwalk-svelte'
|
||||||
|
|
||||||
|
# Optional deployment settings.
|
||||||
|
$VerifyUrl = 'https://www.goodwalk.co.nz/api/health'
|
||||||
|
$RemoteArchivePath = '/tmp/goodwalk-deploy.tgz'
|
||||||
|
$RemoteHelperPath = '/tmp/goodwalk-deploy-remote.sh'
|
||||||
|
$LocalRemoteHelperPath = Join-Path $LocalProjectPath 'scripts\deploy-remote.sh'
|
||||||
|
|
||||||
|
function Assert-NotBlank {
|
||||||
|
param(
|
||||||
|
[string]$Name,
|
||||||
|
[string]$Value
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Value)) {
|
||||||
|
throw "Required setting '$Name' is blank. Update deploy.ps1 before running it."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-Command {
|
||||||
|
param([string]$Name)
|
||||||
|
|
||||||
|
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
||||||
|
throw "Required command '$Name' is not available in PATH."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-External {
|
||||||
|
param(
|
||||||
|
[string]$FilePath,
|
||||||
|
[string[]]$Arguments
|
||||||
|
)
|
||||||
|
|
||||||
|
& $FilePath @Arguments
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Command failed: $FilePath $($Arguments -join ' ')"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SshArgumentList {
|
||||||
|
$args = @()
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($SshConfigPath)) {
|
||||||
|
$args += @('-F', $SshConfigPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($SshKeyPath)) {
|
||||||
|
$args += @('-i', $SshKeyPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $args
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-DeployArchive {
|
||||||
|
param(
|
||||||
|
[string]$ProjectPath
|
||||||
|
)
|
||||||
|
|
||||||
|
$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||||
|
$archivePath = Join-Path ([System.IO.Path]::GetTempPath()) "goodwalk-deploy-$timestamp.tgz"
|
||||||
|
$excludeArgs = @(
|
||||||
|
'--exclude=.git',
|
||||||
|
'--exclude=node_modules',
|
||||||
|
'--exclude=.svelte-kit',
|
||||||
|
'--exclude=build',
|
||||||
|
'--exclude=.env',
|
||||||
|
'--exclude=.env.*',
|
||||||
|
'--exclude=logs',
|
||||||
|
'--exclude=mail-api/__pycache__',
|
||||||
|
'--exclude=mail-api/*.pyc',
|
||||||
|
'--exclude=migration-backups'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Test-Path $archivePath) {
|
||||||
|
Remove-Item -LiteralPath $archivePath -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Push-Location $ProjectPath
|
||||||
|
try {
|
||||||
|
$tarArguments = @('-czf', $archivePath) + $excludeArgs + @('.')
|
||||||
|
Invoke-External -FilePath 'tar' -Arguments $tarArguments
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
|
||||||
|
return $archivePath
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-SiteCheck {
|
||||||
|
param([string]$Url)
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host "[deploy] Checking production site: $Url"
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = Invoke-WebRequest -Uri $Url -MaximumRedirection 5 -TimeoutSec 30
|
||||||
|
Write-Host "[deploy] Site responded with HTTP $($response.StatusCode)"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Production site check failed: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-Command ssh
|
||||||
|
Assert-Command scp
|
||||||
|
Assert-Command tar
|
||||||
|
|
||||||
|
Assert-NotBlank -Name 'SshUser' -Value $SshUser
|
||||||
|
Assert-NotBlank -Name 'ServerHost' -Value $ServerHost
|
||||||
|
Assert-NotBlank -Name 'LocalProjectPath' -Value $LocalProjectPath
|
||||||
|
Assert-NotBlank -Name 'RemoteDeploymentPath' -Value $RemoteDeploymentPath
|
||||||
|
Assert-NotBlank -Name 'ComposeFileName' -Value $ComposeFileName
|
||||||
|
Assert-NotBlank -Name 'DockerProjectName' -Value $DockerProjectName
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($SshConfigPath) -and -not (Test-Path -LiteralPath $SshConfigPath)) {
|
||||||
|
throw "SSH config file not found: $SshConfigPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($SshKeyPath) -and -not (Test-Path -LiteralPath $SshKeyPath)) {
|
||||||
|
throw "SSH key not found: $SshKeyPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $LocalProjectPath)) {
|
||||||
|
throw "Local project path not found: $LocalProjectPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath (Join-Path $LocalProjectPath $ComposeFileName))) {
|
||||||
|
throw "Compose file not found in local project path: $(Join-Path $LocalProjectPath $ComposeFileName)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $LocalRemoteHelperPath)) {
|
||||||
|
throw "Remote deployment helper not found: $LocalRemoteHelperPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$sshTarget = '{0}@{1}' -f $SshUser, $ServerHost
|
||||||
|
$scpArchiveTarget = '{0}:{1}' -f $sshTarget, $RemoteArchivePath
|
||||||
|
$scpHelperTarget = '{0}:{1}' -f $sshTarget, $RemoteHelperPath
|
||||||
|
$sshArgs = Get-SshArgumentList
|
||||||
|
|
||||||
|
Write-Host '[deploy] Main Goodwalk website deployment'
|
||||||
|
Write-Host "[deploy] Local project path: $LocalProjectPath"
|
||||||
|
Write-Host "[deploy] Remote deployment path: $RemoteDeploymentPath"
|
||||||
|
Write-Host "[deploy] Remote compose file: $ComposeFileName"
|
||||||
|
Write-Host "[deploy] Docker project name: $DockerProjectName"
|
||||||
|
Write-Host "[deploy] SSH target: $sshTarget"
|
||||||
|
Write-Host "[deploy] SSH config: $SshConfigPath"
|
||||||
|
if ([string]::IsNullOrWhiteSpace($SshKeyPath)) {
|
||||||
|
Write-Host '[deploy] SSH auth: interactive password prompt'
|
||||||
|
} else {
|
||||||
|
Write-Host "[deploy] SSH auth: key file $SshKeyPath"
|
||||||
|
}
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host '[deploy] Safety notes:'
|
||||||
|
Write-Host ' - Only the top-level Goodwalk compose project will be updated.'
|
||||||
|
Write-Host ' - Legacy WordPress/onboarding compose files are not used.'
|
||||||
|
Write-Host ' - Remote .env files are preserved because they are not uploaded.'
|
||||||
|
Write-Host ' - No global Docker prune/stop/delete commands are used.'
|
||||||
|
|
||||||
|
if (-not $Force) {
|
||||||
|
$confirmation = Read-Host "Type DEPLOY to continue with the remote path '$RemoteDeploymentPath'"
|
||||||
|
if ($confirmation -ne 'DEPLOY') {
|
||||||
|
throw 'Deployment cancelled.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$archivePath = $null
|
||||||
|
|
||||||
|
try {
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host '[deploy] Creating deployment archive'
|
||||||
|
$archivePath = New-DeployArchive -ProjectPath $LocalProjectPath
|
||||||
|
Write-Host "[deploy] Archive ready: $archivePath"
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host '[deploy] Uploading remote helper'
|
||||||
|
Invoke-External -FilePath 'scp' -Arguments ($sshArgs + @($LocalRemoteHelperPath, $scpHelperTarget))
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host '[deploy] Uploading application archive'
|
||||||
|
Invoke-External -FilePath 'scp' -Arguments ($sshArgs + @($archivePath, $scpArchiveTarget))
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host '[deploy] Running remote deployment'
|
||||||
|
Invoke-External -FilePath 'ssh' -Arguments ($sshArgs + @(
|
||||||
|
$sshTarget,
|
||||||
|
'bash',
|
||||||
|
$RemoteHelperPath,
|
||||||
|
'--archive',
|
||||||
|
$RemoteArchivePath,
|
||||||
|
'--deploy-path',
|
||||||
|
$RemoteDeploymentPath,
|
||||||
|
'--compose-file',
|
||||||
|
$ComposeFileName,
|
||||||
|
'--project-name',
|
||||||
|
$DockerProjectName
|
||||||
|
))
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host '[deploy] Cleaning remote temporary files'
|
||||||
|
Invoke-External -FilePath 'ssh' -Arguments ($sshArgs + @(
|
||||||
|
$sshTarget,
|
||||||
|
'rm',
|
||||||
|
'-f',
|
||||||
|
$RemoteArchivePath,
|
||||||
|
$RemoteHelperPath
|
||||||
|
))
|
||||||
|
|
||||||
|
if (-not $SkipSiteCheck) {
|
||||||
|
Invoke-SiteCheck -Url $VerifyUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host '[deploy] Deployment completed successfully'
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if ($archivePath -and (Test-Path -LiteralPath $archivePath)) {
|
||||||
|
Remove-Item -LiteralPath $archivePath -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
container_name: goodwalk_svelte_app
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://${POSTGRES_USER:-goodwalk}:${POSTGRES_PASSWORD:-goodwalk}@db:5432/${POSTGRES_DB:-goodwalk}
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: 3000
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
expose:
|
||||||
|
- '3000'
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- webnet
|
||||||
|
|
||||||
|
mail-api:
|
||||||
|
build:
|
||||||
|
context: ./mail-api
|
||||||
|
container_name: goodwalk_svelte_mail_api
|
||||||
|
environment:
|
||||||
|
RESEND_API_KEY: ${RESEND_API_KEY}
|
||||||
|
OWNER_EMAIL: ${OWNER_EMAIL}
|
||||||
|
FROM_EMAIL: ${FROM_EMAIL:-GoodWalk <bookings@goodwalk.co.nz>}
|
||||||
|
REPLY_TO: ${REPLY_TO:-aless@goodwalk.co.nz}
|
||||||
|
FORM_MIN_SECONDS: ${FORM_MIN_SECONDS:-4}
|
||||||
|
FORM_MAX_SECONDS: ${FORM_MAX_SECONDS:-7200}
|
||||||
|
RATE_LIMIT_WINDOW_SECONDS: ${RATE_LIMIT_WINDOW_SECONDS:-900}
|
||||||
|
RATE_LIMIT_MAX_PER_IP: ${RATE_LIMIT_MAX_PER_IP:-5}
|
||||||
|
RATE_LIMIT_MAX_PER_EMAIL: ${RATE_LIMIT_MAX_PER_EMAIL:-3}
|
||||||
|
RATE_LIMIT_MIN_INTERVAL_SECONDS: ${RATE_LIMIT_MIN_INTERVAL_SECONDS:-20}
|
||||||
|
PYTHONUNBUFFERED: '1'
|
||||||
|
expose:
|
||||||
|
- '8000'
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- webnet
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: goodwalk_svelte_db
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-goodwalk}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-goodwalk}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-goodwalk}
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./docker/postgres/init:/docker-entrypoint-initdb.d:ro
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
webnet:
|
||||||
|
external: true
|
||||||
+1
-1
@@ -112,7 +112,7 @@ RATE_LIMIT_MAX_PER_IP = _config["rate_limit_max_per_ip"]
|
|||||||
RATE_LIMIT_MAX_PER_EMAIL = _config["rate_limit_max_per_email"]
|
RATE_LIMIT_MAX_PER_EMAIL = _config["rate_limit_max_per_email"]
|
||||||
RATE_LIMIT_MIN_INTERVAL_SECONDS = _config["rate_limit_min_interval_seconds"]
|
RATE_LIMIT_MIN_INTERVAL_SECONDS = _config["rate_limit_min_interval_seconds"]
|
||||||
|
|
||||||
LOGO_URL = "https://www.goodwalk.co.nz/static/images/goodwalk-auckland-dog-walking-logo.png"
|
LOGO_URL = "https://www.goodwalk.co.nz/images/goodwalk-auckland-dog-walking-logo.png"
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Mail API config: from=%r reply_to=%r owner=%r max_attempts=%d form_min=%ss form_max=%ss rate_window=%ss per_ip=%d per_email=%d min_interval=%ss",
|
"Mail API config: from=%r reply_to=%r owner=%r max_attempts=%d form_min=%ss form_max=%ss rate_window=%ss per_ip=%d per_email=%d min_interval=%ss",
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
limit_req_zone $binary_remote_addr zone=goodwalk_limit:10m rate=20r/s;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name goodwalk.co.nz www.goodwalk.co.nz;
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 301 https://www.goodwalk.co.nz$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name goodwalk.co.nz;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/goodwalk.co.nz/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/goodwalk.co.nz/privkey.pem;
|
||||||
|
|
||||||
|
return 301 https://www.goodwalk.co.nz$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name www.goodwalk.co.nz;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/goodwalk.co.nz/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/goodwalk.co.nz/privkey.pem;
|
||||||
|
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
ssl_session_tickets off;
|
||||||
|
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
add_header X-Frame-Options SAMEORIGIN always;
|
||||||
|
add_header X-Content-Type-Options nosniff always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss image/svg+xml;
|
||||||
|
|
||||||
|
location ~* /\.(git|env|htaccess) {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /xmlrpc.php {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /wp-login.php {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/submit {
|
||||||
|
limit_req zone=goodwalk_limit burst=10 nodelay;
|
||||||
|
proxy_pass http://goodwalk_svelte_mail_api:8000/submit;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://goodwalk_svelte_app:3000;
|
||||||
|
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;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
ARCHIVE_PATH=""
|
||||||
|
DEPLOY_PATH=""
|
||||||
|
COMPOSE_FILE=""
|
||||||
|
PROJECT_NAME=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
deploy-remote.sh --archive <path> --deploy-path <path> --compose-file <name> --project-name <name>
|
||||||
|
|
||||||
|
This script only updates the main Goodwalk compose project at the specified
|
||||||
|
deployment path. It does not touch unrelated Docker projects or global Docker
|
||||||
|
state.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "[deploy-remote] ERROR: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--archive)
|
||||||
|
ARCHIVE_PATH="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--deploy-path)
|
||||||
|
DEPLOY_PATH="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--compose-file)
|
||||||
|
COMPOSE_FILE="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--project-name)
|
||||||
|
PROJECT_NAME="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
fail "Unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$ARCHIVE_PATH" ]] || fail "--archive is required"
|
||||||
|
[[ -n "$DEPLOY_PATH" ]] || fail "--deploy-path is required"
|
||||||
|
[[ -n "$COMPOSE_FILE" ]] || fail "--compose-file is required"
|
||||||
|
[[ -n "$PROJECT_NAME" ]] || fail "--project-name is required"
|
||||||
|
[[ "$DEPLOY_PATH" != "/" ]] || fail "Refusing to deploy to /"
|
||||||
|
[[ -f "$ARCHIVE_PATH" ]] || fail "Archive not found: $ARCHIVE_PATH"
|
||||||
|
|
||||||
|
if docker compose version >/dev/null 2>&1; then
|
||||||
|
COMPOSE_CMD=(docker compose)
|
||||||
|
elif command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
COMPOSE_CMD=(docker-compose)
|
||||||
|
else
|
||||||
|
fail "Docker Compose is not installed on the server"
|
||||||
|
fi
|
||||||
|
|
||||||
|
STAGING_DIR="$(mktemp -d "${TMPDIR:-/tmp}/goodwalk-deploy.XXXXXX")"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$STAGING_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo "[deploy-remote] Deploying main Goodwalk stack"
|
||||||
|
echo "[deploy-remote] Target deployment path: $DEPLOY_PATH"
|
||||||
|
echo "[deploy-remote] Compose file: $COMPOSE_FILE"
|
||||||
|
echo "[deploy-remote] Docker project: $PROJECT_NAME"
|
||||||
|
echo "[deploy-remote] Staging archive in: $STAGING_DIR"
|
||||||
|
|
||||||
|
mkdir -p "$DEPLOY_PATH"
|
||||||
|
tar -xzf "$ARCHIVE_PATH" -C "$STAGING_DIR"
|
||||||
|
|
||||||
|
[[ -f "$STAGING_DIR/$COMPOSE_FILE" ]] || fail "Compose file missing from uploaded archive: $COMPOSE_FILE"
|
||||||
|
|
||||||
|
if [[ -f "$DEPLOY_PATH/.env" ]]; then
|
||||||
|
echo "[deploy-remote] Preserving existing $DEPLOY_PATH/.env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[deploy-remote] Copying application files into $DEPLOY_PATH"
|
||||||
|
if command -v rsync >/dev/null 2>&1; then
|
||||||
|
rsync -a \
|
||||||
|
--exclude '.env' \
|
||||||
|
--exclude '.env.*' \
|
||||||
|
"$STAGING_DIR"/ "$DEPLOY_PATH"/
|
||||||
|
else
|
||||||
|
while IFS= read -r -d '' item; do
|
||||||
|
relative_path="${item#"$STAGING_DIR"/}"
|
||||||
|
|
||||||
|
if [[ "$relative_path" == ".env" || "$relative_path" == .env.* ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
destination="$DEPLOY_PATH/$relative_path"
|
||||||
|
|
||||||
|
if [[ -d "$item" ]]; then
|
||||||
|
mkdir -p "$destination"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$destination")"
|
||||||
|
cp -f "$item" "$destination"
|
||||||
|
done < <(find "$STAGING_DIR" -mindepth 1 -print0)
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -f "$DEPLOY_PATH/$COMPOSE_FILE" ]] || fail "Compose file missing after copy: $DEPLOY_PATH/$COMPOSE_FILE"
|
||||||
|
|
||||||
|
if [[ ! -f "$DEPLOY_PATH/.env" ]]; then
|
||||||
|
if [[ -f "$DEPLOY_PATH/deploy.env.template" ]]; then
|
||||||
|
echo "[deploy-remote] No remote .env found. Creating $DEPLOY_PATH/.env from deploy.env.template"
|
||||||
|
cp "$DEPLOY_PATH/deploy.env.template" "$DEPLOY_PATH/.env"
|
||||||
|
else
|
||||||
|
fail "Remote .env is missing and deploy.env.template was not uploaded"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$DEPLOY_PATH"
|
||||||
|
|
||||||
|
echo "[deploy-remote] Validating compose configuration"
|
||||||
|
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" config >/dev/null
|
||||||
|
|
||||||
|
echo "[deploy-remote] Stopping only the Goodwalk project containers"
|
||||||
|
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" stop || true
|
||||||
|
|
||||||
|
echo "[deploy-remote] Rebuilding and starting only the Goodwalk project containers"
|
||||||
|
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" up -d --build --remove-orphans
|
||||||
|
|
||||||
|
echo "[deploy-remote] Current Goodwalk container status"
|
||||||
|
"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" ps
|
||||||
|
|
||||||
|
echo "[deploy-remote] Remote deployment finished"
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# Repo-local SSH config for Goodwalk deployments.
|
||||||
|
# Fill in the HostName, User, and optional IdentityFile values to match your server.
|
||||||
|
# The deploy.ps1 script will automatically use this file via: ssh/scp -F ./ssh-config
|
||||||
|
|
||||||
|
Host gw-prod
|
||||||
|
HostName 170.64.216.55
|
||||||
|
User root
|
||||||
|
Port 22
|
||||||
|
|
||||||
|
# Uncomment if you switch to key-based auth later.
|
||||||
|
# IdentityFile C:/Users/your-user/.ssh/goodwalk-prod
|
||||||
|
|
||||||
|
# Keep interactive password auth available for now.
|
||||||
|
PreferredAuthentications password,keyboard-interactive,publickey
|
||||||
|
PubkeyAuthentication yes
|
||||||
|
|
||||||
|
# Safe defaults for deployments.
|
||||||
|
ServerAliveInterval 30
|
||||||
|
ServerAliveCountMax 4
|
||||||
|
TCPKeepAlive yes
|
||||||
|
StrictHostKeyChecking ask
|
||||||
Reference in New Issue
Block a user