12 KiB
Deployment
Hosts served by this stack
The Goodwalk Svelte stack serves three subdomains from the same SvelteKit app container, routed by Host header at nginx:
| Hostname | Purpose |
|---|---|
goodwalk.co.nz / www.… |
Public marketing site |
clients.goodwalk.co.nz |
New-client onboarding + contract portal |
cp.goodwalk.co.nz |
Owner admin dashboard (Aless only) |
The shared nginx container reads TLS material from its host bind mount at
/docker/certbot/conf, exposed inside the container as /etc/letsencrypt.
The deploy scripts now auto-bootstrap any missing certificates referenced by
the shared nginx config, including clients.goodwalk.co.nz,
cp.goodwalk.co.nz, and the legacy HTTPS redirect aliases
onboarding.goodwalk.co.nz and admin.goodwalk.co.nz. They do that by
temporarily loading an HTTP-only nginx config and running certbot/certbot
against the mounted ACME webroot. The only prerequisite is that each
hostname's DNS A record already points at the droplet and port 80 is
reachable.
The dashboard's data (client_profiles, allowed_emails, drafts) lives in
the shared postgres database alongside the marketing site content, in a single
admin_kv table created by docker/postgres/init/002-admin-kv.sql. The
mail-api connects with the same DATABASE_URL the SvelteKit app uses.
Seeding admin_kv from the old JSON files
Existing installs have admin data in client_profiles.json,
allowed_emails.json, and drafts.json on the mail-api Docker volume. To copy
that data into postgres on the next deploy, run:
./deploy.ps1 -SeedAdminData
That sets ADMIN_DATA_SEED_FROM_JSON=force for the mail-api container, which
overwrites admin_kv from the JSON files on the next boot. Subsequent deploys
default back to auto (seed only when admin_kv is empty), so they are no-ops
for the seed. Use -SeedAdminData again if you ever need to force a re-seed.
Server layout confirmed
The production server currently runs multiple separate Docker Compose projects:
- Main public site WordPress stack:
- project:
goodwalkconz - path:
/docker/wordpress/goodwalk.co.nz
- project:
- Legacy onboarding WordPress stack:
- project:
onboardinggoodwalkconz - path:
/docker/wordpress/onboarding.goodwalk.co.nz
- project:
- Shared nginx:
- project:
nginx - path:
/docker/nginx
- project:
- Shared mysql:
- project:
mysql - path:
/docker/mysql
- project:
The deployment scripts in this repo are set up to deploy the new Svelte site as a separate stack at:
- remote path:
/docker/goodwalk-svelte - compose file:
docker-compose.prod.yml - docker project:
goodwalk-svelte
This leaves the onboarding site, shared nginx, shared mysql, and other unrelated containers untouched.
Files involved
- deploy.ps1
- Windows entrypoint for packaging the repo, uploading it, and running the remote deployment helper over SSH.
- scripts/deploy.ps1
- Deprecated compatibility wrapper that forwards to the repo-root
deploy.ps1. Keep using the root script directly.
- Deprecated compatibility wrapper that forwards to the repo-root
- scripts/deploy-remote.sh
- Server-side helper that updates only the
goodwalk-sveltecompose project.
- Server-side helper that updates only the
- scripts/deploy-from-git.sh
- Standalone server-side entrypoint that pulls from Git, then runs the same compose/nginx deployment steps on the server.
- docker-compose.prod.yml
- Production compose file for the new Svelte app, mail API, and Postgres.
scripts/export-homepage-content.mjs- Local helper that exports the current
src/lib/content/homepage.tsinto a deployable JSON payload before each deployment.
- Local helper that exports the current
scripts/sync-homepage-content.mjs- Runtime helper that upserts the exported homepage content into PostgreSQL after deploys that affect the app/database.
- ssh-config
- Repo-local SSH config used by the deployment script.
- 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, including the
clientsandcpsubdomains.
- Example shared-nginx config for routing the main public site to the new
Svelte app and mail API, including the
First-time server preparation
-
Fill in ssh-config with the real host details.
-
Create the deployment directory on the server:
mkdir -p /docker/goodwalk-svelte
- The first deployment will auto-create the production env file on the server at:
/docker/goodwalk-svelte/.env
It is created from deploy.env.template. Current template contents:
APP_VERSION=4.0.1
ENABLE_GENERAL_ENQUIRIES=false
PUBLIC_ENABLE_MOBILE_CTA_BUTTON=false
PUBLIC_ENABLE_ENHANCED_CONTENT_IMAGES=false
TZ=Pacific/Auckland
POSTGRES_DB=goodwalk
POSTGRES_USER=goodwalk
POSTGRES_PASSWORD=gw_Pg_7Jm9!Qx4#Ld2@Vr8
POSTGRES_PASSWORD_URLENCODED=gw_Pg_7Jm9%21Qx4%23Ld2%40Vr8
RESEND_API_KEY=replace-me
OWNER_EMAIL=replace-me
SECONDARY_CP_EMAIL=
SECONDARY_CP_EMAILS=
FROM_EMAIL=GoodWalk <bookings@goodwalk.co.nz>
REPLY_TO=aless@goodwalk.co.nz
MAIL_API_DATA_DIR=/app/data
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
EMAIL_SEND_TIMEOUT_SECONDS=20
After the first deploy, edit /docker/goodwalk-svelte/.env on the server and replace:
RESEND_API_KEY=replace-meOWNER_EMAIL=replace-me
Optional CP dashboard admins:
SECONDARY_CP_EMAIL=person@example.comSECONDARY_CP_EMAILS=bob@smith.com;bobsmith2@smith.com
OWNER_EMAIL always keeps CP access. The secondary values are optional and may be
semicolon-, comma-, or whitespace-separated.
Frontend flags:
PUBLIC_ENABLE_MOBILE_CTA_BUTTON=falsekeeps the sticky mobile booking CTA hidden.- Set
PUBLIC_ENABLE_MOBILE_CTA_BUTTON=trueto show it again. PUBLIC_ENABLE_ENHANCED_CONTENT_IMAGES=falseskips eager@sveltejs/enhanced-imgprocessing for content images during production builds. Turn it on only if you intentionally want non-WebP images fromsrc/lib/imagesto go through the imagetools pipeline.
- Confirm the shared Docker network already exists:
docker network ls | grep webnet
Your server already uses webnet, so this should already be present.
- Confirm the shared nginx compose mounts still point at the host certbot paths expected by the deploy scripts:
- /docker/certbot/conf:/etc/letsencrypt:ro
- /docker/certbot/www:/var/www/certbot:ro
The scripts inspect the running nginx container to derive those host paths
before checking or issuing certificates.
First deploy
From Windows PowerShell in the repo root:
powershell -ExecutionPolicy Bypass -File .\deploy.ps1
This is the single supported deployment entrypoint. If you see
scripts/deploy.ps1, that file now just forwards to the root script so the
deployment logic only lives in one place.
Or skip the confirmation prompt:
powershell -ExecutionPolicy Bypass -File .\deploy.ps1 -Force
To rebuild and restart only one service, for example the mail API:
powershell -ExecutionPolicy Bypass -File .\deploy.ps1 -Force -Service mail-api
Remote Git deploy
If you want the production server to pull straight from Gitea instead of receiving an uploaded tarball from Windows, use scripts/deploy-from-git.sh on the server.
Recommended credential setup for a private HTTPS repo:
umask 077
cat > ~/.netrc <<'EOF'
machine g.sublogue.com
login YOUR_GITEA_USERNAME
password YOUR_READ_ONLY_TOKEN
EOF
chmod 600 ~/.netrc
Install the script on the server and make it executable:
install -m 0755 scripts/deploy-from-git.sh /usr/local/bin/goodwalk-deploy
The remote host must have git and docker. A host-level node install is
optional; if it is missing, the script will export homepage content using a
temporary node:22-alpine container instead.
Run a full deploy from the repo:
/usr/local/bin/goodwalk-deploy \
--repo-url https://g.sublogue.com/admin/gw-svelte.git \
--branch main \
--deploy-path /docker/goodwalk-svelte \
--compose-file docker-compose.prod.yml \
--project-name goodwalk-svelte \
--nginx-source nginx/goodwalk.co.nz.svelte.conf.example \
--nginx-target /docker/nginx/conf.d/goodwalk.co.nz.conf \
--nginx-compose-file /docker/nginx/docker-compose.yml \
--nginx-project-name nginx \
--maintenance-host-dir /docker/nginx/maintenance \
--maintenance-flag /docker/nginx/conf.d/maintenance.flag
Deploy a specific commit or tag:
/usr/local/bin/goodwalk-deploy \
--repo-url https://g.sublogue.com/admin/gw-svelte.git \
--branch main \
--ref <commit-or-tag> \
--deploy-path /docker/goodwalk-svelte \
--compose-file docker-compose.prod.yml \
--project-name goodwalk-svelte \
--nginx-source nginx/goodwalk.co.nz.svelte.conf.example \
--nginx-target /docker/nginx/conf.d/goodwalk.co.nz.conf \
--nginx-compose-file /docker/nginx/docker-compose.yml \
--nginx-project-name nginx \
--maintenance-host-dir /docker/nginx/maintenance \
--maintenance-flag /docker/nginx/conf.d/maintenance.flag
Homepage content sync
Local development can feel fresher than production because production reads the
homepage/shared content from PostgreSQL whenever DATABASE_URL is set.
The deployment flow now handles that automatically:
deploy.ps1exports the currentsrc/lib/content/homepage.tsintodeploy-data/homepage-content.json.- The deploy archive uploads that JSON payload with the app source.
- After the Goodwalk stack is updated, the remote helper runs a content sync inside the app container.
- That sync upserts the
homepagerow insite_content.
This means future deploys will carry your latest file-based homepage/navigation/ shared content changes into production PostgreSQL automatically.
Mail auth persistence
The mail API stores auth state in DATA_DIR, including:
allowed_emails.jsonclient_profiles.jsondrafts.json
Both compose files now mount a named Docker volume at MAIL_API_DATA_DIR
(default /app/data) so previously registered client emails and saved drafts
survive container rebuilds and redeploys.
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:
/docker/nginx/conf.d/goodwalk.co.nz.conf
Use the repo example as the new target config:
nginx/goodwalk.co.nz.svelte.conf.example
Important:
deploy.ps1now copies the repo nginx config to/docker/nginx/conf.d/goodwalk.co.nz.confand reloads the shared nginx container as part of deployment.- The repo nginx config uses Docker's internal resolver so future app/mail container rebuilds will not leave nginx pinned to stale upstream IPs.
- The same nginx config now also routes
clients.goodwalk.co.nzto the Svelte app and/api/onboarding-submitto the shared mail API. - The owner dashboard is now served on
cp.goodwalk.co.nz. onboarding.goodwalk.co.nzandadmin.goodwalk.co.nzshould be kept only as redirect aliases once their DNS and TLS are in place.- The deploy script will attempt to issue any missing certificates for
clients.goodwalk.co.nz,cp.goodwalk.co.nz, andonboarding.goodwalk.co.nzbefore the final nginx reload.
Manual nginx commands, if you ever need them:
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.ymlto 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
.envfile. - The site check in
deploy.ps1targetshttps://www.goodwalk.co.nz/api/health. Before nginx cutover, use-SkipSiteCheckor expect that check to fail.