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-devinside that staged release before switching traffic - keep the fixed production app path pointed at the active release through a server-side
currentsymlink - 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, andphp 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-releasesorRELEASE_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
mysqldumpexport - 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 --forceunless--skip-migrateis 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.shwithout--with-db. - Use
bash sync.sh --full-upgradeonly 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 --previousfor a fast server-side release switch when the last deploy needs to be reverted. - Route caching now runs through
php artisan optimizein 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:
- Include the snippet inside the Skinbase
server {}block. - Add
fastcgi_intercept_errors on;to every FastCGI location that should use the static fallback. - 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.