Deployment script updates

This commit is contained in:
2026-05-02 12:39:55 +12:00
parent 3587ba7f26
commit b0bb692972
8 changed files with 695 additions and 35 deletions
+130 -34
View File
@@ -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.
+15
View File
@@ -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
View File
@@ -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
}
}
+61
View File
@@ -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
View File
@@ -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",
+84
View File
@@ -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";
}
}
+142
View File
@@ -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
View File
@@ -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