From b0bb6929722af560fbc9d4806c1ee35e682beea1 Mon Sep 17 00:00:00 2001 From: ponzischeme89 Date: Sat, 2 May 2026 12:39:55 +1200 Subject: [PATCH] Deployment script updates --- DEPLOYMENT.md | 164 +++++++++++---- deploy.env.template | 15 ++ deploy.ps1 | 241 +++++++++++++++++++++++ docker-compose.prod.yml | 61 ++++++ mail-api/main.py | 2 +- nginx/goodwalk.co.nz.svelte.conf.example | 84 ++++++++ scripts/deploy-remote.sh | 142 +++++++++++++ ssh-config | 21 ++ 8 files changed, 695 insertions(+), 35 deletions(-) create mode 100644 deploy.env.template create mode 100644 deploy.ps1 create mode 100644 docker-compose.prod.yml create mode 100644 nginx/goodwalk.co.nz.svelte.conf.example create mode 100644 scripts/deploy-remote.sh create mode 100644 ssh-config diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index be8e72b..cf411c9 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -1,45 +1,141 @@ # Deployment -## What the scripts do +## Server layout confirmed -- `scripts/migrate-wordpress.ps1` - - Dumps the existing WordPress MySQL database to `migration-backups//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//uploads` +The production server currently runs multiple separate Docker Compose projects: -- `scripts/deploy.ps1` - - Optionally runs the migration step first - - Optionally shuts down the legacy compose stack - - Validates the new compose file - - Builds and starts the new stack - - Waits for `http://localhost/api/health` to return success +- Main public site WordPress stack: + - project: `goodwalkconz` + - path: `/docker/wordpress/goodwalk.co.nz` +- Onboarding WordPress stack: + - project: `onboardinggoodwalkconz` + - 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` -2. Make sure the legacy WordPress stack is still running -3. Identify: - - 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 +- remote path: `/docker/goodwalk-svelte` +- compose file: `docker-compose.prod.yml` +- docker project: `goodwalk-svelte` -## Example +This leaves the onboarding site, shared nginx, shared mysql, and other unrelated +containers untouched. -```powershell -powershell -ExecutionPolicy Bypass -File .\scripts\deploy.ps1 ` - -RunMigration ` - -LegacyComposeFile C:\deploy\wordpress\docker-compose.yml ` - -LegacyProjectName goodwalk-wordpress ` - -LegacyWordPressContainer goodwalk-wordpress-1 ` - -LegacyDatabaseContainer goodwalk-db-1 ` - -MySqlDatabase wordpress ` - -MySqlUser wordpress ` - -MySqlPassword 'replace-me' +## Files involved + +- [deploy.ps1](deploy.ps1) + - Windows entrypoint for packaging the repo, uploading it, and running the + remote deployment helper over SSH. +- [scripts/deploy-remote.sh](scripts/deploy-remote.sh) + - Server-side helper that updates only the `goodwalk-svelte` compose project. +- [docker-compose.prod.yml](docker-compose.prod.yml) + - Production compose file for the new Svelte app, mail API, and Postgres. +- [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. -- The deployment script does not destroy the legacy database dump. It writes a fresh backup on every migration run. -- If you want to keep the legacy stack running while testing, omit `-LegacyComposeFile` or add `-SkipLegacyShutdown`. +```bash +/docker/goodwalk-svelte/.env +``` + +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 +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. diff --git a/deploy.env.template b/deploy.env.template new file mode 100644 index 0000000..307ae19 --- /dev/null +++ b/deploy.env.template @@ -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 +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 \ No newline at end of file diff --git a/deploy.ps1 b/deploy.ps1 new file mode 100644 index 0000000..9b1d59c --- /dev/null +++ b/deploy.ps1 @@ -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 + } +} diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..dc38737 --- /dev/null +++ b/docker-compose.prod.yml @@ -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 } + 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 diff --git a/mail-api/main.py b/mail-api/main.py index 72a0f3d..3058575 100644 --- a/mail-api/main.py +++ b/mail-api/main.py @@ -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_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( "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", diff --git a/nginx/goodwalk.co.nz.svelte.conf.example b/nginx/goodwalk.co.nz.svelte.conf.example new file mode 100644 index 0000000..2aecc13 --- /dev/null +++ b/nginx/goodwalk.co.nz.svelte.conf.example @@ -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"; + } +} diff --git a/scripts/deploy-remote.sh b/scripts/deploy-remote.sh new file mode 100644 index 0000000..d5f777d --- /dev/null +++ b/scripts/deploy-remote.sh @@ -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 --deploy-path --compose-file --project-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" diff --git a/ssh-config b/ssh-config new file mode 100644 index 0000000..57c25ac --- /dev/null +++ b/ssh-config @@ -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