Files
SkinbaseNova/docs/deployment.md

7.8 KiB

Deployment

This repository uses a Bash-based production deploy flow.

Normal deploy

Run the existing entrypoint:

bash sync.sh

If you launch bash sync.sh from WSL against this Windows checkout, the script will automatically run the frontend build with npm.cmd on Windows so Rollup/Vite use the correct optional native package set.

This will:

  • build frontend assets locally with npm run build
  • stage the new code into a versioned release directory on the production server
  • run composer install --no-dev inside that staged release before switching traffic
  • keep the fixed production app path pointed at the active release through a server-side current symlink
  • bring the app down only for the short critical section that switches the active release and runs php artisan migrate --force, php artisan optimize:clear, and php artisan optimize
  • bring the app back up immediately after that critical section finishes
  • warm the guest homepage cache with php artisan homepage:warm-guest-cache
  • warm the post trending cache with php artisan posts:warm-trending
  • restart queue workers with php artisan queue:restart

This is now the low-downtime default path for normal code and feature deploys.

Each deploy generates a release ID automatically from UTC time and the local Git revision. Releases are retained under REMOTE_RELEASE_ROOT/releases/<release-id>, and production switches between them on the server by updating REMOTE_RELEASE_ROOT/current. The public/runtime path stays fixed at REMOTE_FOLDER, which is now treated as a stable symlink to the active release.

On the first deploy with this layout, the existing live folder is adopted into the release archive automatically and REMOTE_FOLDER is converted into that stable symlink path. After that, switching back to an older release does not require any local re-upload.

Full upgrade

Use a full upgrade when the release also needs broad Meilisearch work or non-code service operations.

bash sync.sh --full-upgrade

Full-upgrade mode:

  • keeps the normal deploy steps
  • forces a full Meilisearch import for all searchable models unless you explicitly pass --skip-meilisearch
  • allows optional remote upgrade hooks for service-level work

Example with service hooks:

bash sync.sh --full-upgrade \
	--upgrade-pre-hook='sudo systemctl stop reverb' \
	--upgrade-post-hook='sudo systemctl restart reverb meilisearch'

You can also provide those hooks through environment variables instead of CLI flags:

FULL_UPGRADE_PRE_HOOK='sudo systemctl stop reverb' \
FULL_UPGRADE_POST_HOOK='sudo systemctl restart reverb meilisearch' \
bash sync.sh --full-upgrade

Deploy options

bash sync.sh --skip-build
bash sync.sh --skip-migrate
bash sync.sh --no-maintenance
bash sync.sh --mode=full-upgrade
bash sync.sh --keep-releases=8
bash sync.sh --release-id=release-2026-04-25

Environment overrides:

REMOTE_SERVER=user@example.com REMOTE_FOLDER=/var/www/app bash sync.sh
REMOTE_RELEASE_ROOT=/var/www/app.releases RELEASE_RETENTION=8 bash sync.sh

You can also override the local build command explicitly:

LOCAL_BUILD_COMMAND='npm run build' bash sync.sh
LOCAL_BUILD_COMMAND='pnpm build' bash sync.sh

Upgrade hooks can also be supplied via environment variables:

FULL_UPGRADE_PRE_HOOK='sudo systemctl stop reverb' bash sync.sh --full-upgrade
FULL_UPGRADE_POST_HOOK='sudo systemctl restart reverb meilisearch' bash sync.sh --full-upgrade

Rollback and release history

List retained releases on production:

bash scripts/rollback-production.sh --list

Switch production to the previous retained release:

bash scripts/rollback-production.sh --previous

Switch production to a specific retained release:

bash scripts/rollback-production.sh --release-id=20260425-132455-a1b2c3d

Preview a release switch without changing the server:

bash scripts/rollback-production.sh --previous --dry-run

Operational notes:

  • Rollback now means switching the active server release, not re-syncing files from local.
  • Each retained release already contains its own code and vendor tree, so rollback is primarily a symlink switch plus cache refresh and queue:restart.
  • Rollback does not reverse database migrations. If a release includes incompatible schema changes, handle the database separately.
  • Release retention defaults to 5 releases and can be changed with --keep-releases or RELEASE_RETENTION.
  • Release data lives outside the active app path by default at REMOTE_FOLDER.releases, so switching releases happens entirely on the production server.

Replace production database from local

This is intentionally separate from a normal deploy because it overwrites production data.

bash scripts/push-db-to-prod.sh --force

Or combine it with deploy:

bash sync.sh --with-db-from=local

When run interactively, the deploy script will ask you to confirm the exact remote server and type a confirmation phrase before replacing production data.

For non-interactive use, pass both confirmations explicitly:

bash sync.sh --with-db-from=local \
	--confirm-db-sync-target=klevze@server3.klevze.si \
	--confirm-db-sync-phrase='replace production db from local'

Legacy compatibility still exists for:

bash sync.sh --with-db --force-db-sync

But the safer --with-db-from=local flow should be preferred.

The database sync script will:

  • read local DB credentials from the local .env
  • create a local mysqldump export
  • upload the dump to the production server
  • create a backup of the current production database under storage/app/deploy-backups
  • import the local dump into the production database
  • run php artisan migrate --force unless --skip-migrate is passed

If you run the deploy from WSL while your local MySQL server is running on Windows with DB_HOST=127.0.0.1 or localhost, the DB sync script will automatically use Windows mysqldump.exe so it can still reach the local database.

You can override the dump command explicitly if needed:

LOCAL_MYSQLDUMP_COMMAND='mysqldump --host=10.0.0.5 --port=3306 --user=app dbname' bash scripts/push-db-to-prod.sh --force

Safety notes

  • Normal deployments should use bash sync.sh without --with-db.
  • Use bash sync.sh --full-upgrade only when the release also includes Meilisearch-wide refreshes or remote service changes.
  • Use database replacement only for first-time bootstrap, staging, or an intentional full production reset.
  • Use bash scripts/rollback-production.sh --previous for a fast server-side release switch when the last deploy needs to be reverted.
  • Route caching now runs through php artisan optimize in deploy automation; if that starts failing again, fix the route definitions instead of dropping route caching from deploy.

Nginx upstream error pages

Laravel now owns the Nova-style HTML error pages for normal application responses, but true upstream failures such as 502 Bad Gateway and 504 Gateway Timeout still have to be handled by nginx because PHP/FPM is unavailable in that state.

The repo includes a ready-to-include snippet at deploy/nginx/upstream-error-pages.conf and the matching static page at public/errors/upstream-gateway.html.

To enable it on production:

  1. Include the snippet inside the Skinbase server {} block.
  2. Add fastcgi_intercept_errors on; to every FastCGI location that should use the static fallback.
  3. Keep the static file available in the live public path so nginx can serve it without Laravel.

On the current skinbase.top vhost, the required FastCGI locations are:

  • location ^~ /api/uploads/
  • location = /index.php

This intentionally intercepts only 502 and 504, so Laravel remains responsible for normal 404, 419, 429, 500, and 503 rendering when the application is actually running.