DEV Community

Cover image for Deployment Hooks Explained: Running Custom Scripts During Every Deploy
Deploynix
Deploynix

Posted on • Originally published at deploynix.io

Deployment Hooks Explained: Running Custom Scripts During Every Deploy

Every deployment has the standard tasks — install dependencies, build assets, run migrations, cache configuration, restart queue workers. Deploynix handles all of these automatically. But your application might need more. Maybe you need to seed specific data, terminate Horizon, invalidate a CDN, or ping Slack when a deployment completes.

Deploynix's additional deploy commands let you define custom scripts that run alongside every deployment. They're integrated into the deployment pipeline, logged in real-time, and work with both traditional and zero-downtime deployments. Here's how they work.

What the Default Deploy Script Already Handles

Before adding custom commands, it's important to know what Deploynix does automatically. The default deploy script runs on every deployment and handles:

  1. Install Composer dependencies — composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
  2. Install npm dependencies and build assets — npm install and npm run build (if a build script exists in package.json)
  3. Laravel setup — Creates .env from .env.example on first deploy, generates APP_KEY if missing, creates storage symlink
  4. Clear and rebuild caches — optimize:clear, then config:cache, route:cache, and view:cache
  5. Run database migrations — php artisan migrate --force
  6. Restart queue workers — php artisan queue:restart

You don't need to add any of these as custom commands — they run every time, automatically.

How Additional Deploy Commands Fit Into the Pipeline

Your custom commands run after the default deploy script completes but before the deployment is finalized. Here's where they fit in both deployment modes:

Zero-Downtime Deployment Timeline

  1. Create new release directory — A fresh timestamped directory is created
  2. Clone repository — Your code is checked out into the new release directory
  3. Link shared resources — Storage directory and .env file are symlinked from the shared directory
  4. Sync environment variables — Database credentials and env vars are written to .env
  5. Run default deploy script — Dependencies, assets, caches, migrations, queue restart
  6. Run your additional deploy commands — Your custom scripts execute here
  7. Activate release — The current symlink atomically switches to the new release
  8. Reload PHP-FPM — Workers gracefully reload with the new code
  9. Clean up old releases — Releases beyond the retention limit are removed

The key point: your custom commands run in the new release directory, before the symlink switches. If your commands fail, the deployment is aborted — users never see the broken release.

Traditional Deployment Timeline

  1. Pull latest changes — git pull in the site directory
  2. Sync environment variables — Env vars are written to .env
  3. Run default deploy script — Dependencies, assets, caches, migrations, queue restart
  4. Run your additional deploy commands — Your custom scripts execute here
  5. Reload PHP-FPM — Workers reload with the new code

In traditional deployments, your commands run after the default script completes. If they fail, the deployment is marked as failed and the error is logged.

Adding Custom Commands

Navigate to your site's settings and find the Additional Deploy Commands tab. You'll see a text editor where you can write bash commands. These commands execute as a single script with set -e (exit on first error), and the working directory is automatically set to your site or release directory.

Here's an example:

# Terminate Horizon so supervisor restarts it with new code
php artisan horizon:terminate

# Seed specific data
php artisan db:seed --class=PermissionsSeeder --force

# Notify Slack
curl -s -X POST -H 'Content-type: application/json' \
  --data '{"text":"Deployment completed successfully on production"}' \
  https://hooks.slack.com/services/YOUR/WEBHOOK/URL || true
Enter fullscreen mode Exit fullscreen mode

Useful Custom Commands

Since the default script already handles the essentials, your custom commands are for anything specific to your application.

Horizon Restart

If you use Laravel Horizon for queue management, you'll want to terminate it so the supervisor process restarts workers with the new code:

php artisan horizon:terminate
Enter fullscreen mode Exit fullscreen mode

This is different from queue:restart (which the default script already runs). Horizon's terminate command signals the master supervisor to shut down gracefully. The process manager (like Supervisor) then restarts it automatically.

Database Seeders

If you have seeders that should run on each deploy (permissions, default settings, etc.):

php artisan db:seed --class=PermissionsSeeder --force
Enter fullscreen mode Exit fullscreen mode

The --force flag is required because seeders ask for confirmation in production.

Slack Notification

curl -s -X POST -H 'Content-type: application/json' \
  --data '{"text":"Deployment completed on production"}' \
  https://hooks.slack.com/services/YOUR/WEBHOOK/URL || true
Enter fullscreen mode Exit fullscreen mode

The || true at the end ensures the deployment isn't marked as failed if Slack is unreachable. Use this pattern for any non-critical notification.

Sentry Release Tracking

If you use Sentry for error tracking, creating a release during deployment lets Sentry associate errors with specific deployments:

export SENTRY_ORG="your-org"
export SENTRY_PROJECT="your-project"
export SENTRY_AUTH_TOKEN="your-token"
sentry-cli releases new "$(git rev-parse HEAD)"
sentry-cli releases set-commits "$(git rev-parse HEAD)" --auto
sentry-cli releases finalize "$(git rev-parse HEAD)"
Enter fullscreen mode Exit fullscreen mode

CDN Cache Invalidation

If you use a CDN like CloudFront for assets:

aws cloudfront create-invalidation \
  --distribution-id YOUR_DISTRIBUTION_ID \
  --paths "/build/*"
Enter fullscreen mode Exit fullscreen mode

Health Check

Verify the application is responding after the deploy script runs:

sleep 3
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/health)
if [ "$HTTP_STATUS" != "200" ]; then
  echo "Health check failed with status $HTTP_STATUS"
  exit 1
fi
Enter fullscreen mode Exit fullscreen mode

If the health check fails, the deployment is aborted (zero-downtime) or marked as failed (traditional), and you're notified in the real-time deployment log.

Conditional Commands

Run different commands based on the environment:

# Only run seeders on staging
if grep -q "^APP_ENV=staging" .env; then
  php artisan db:seed --force
fi
Enter fullscreen mode Exit fullscreen mode

A Complete Example

Here's a realistic set of additional deploy commands for a production Laravel application using Horizon and Slack notifications:

# Restart Horizon (supervisor will bring it back with new code)
php artisan horizon:terminate

# Sync permissions
php artisan db:seed --class=PermissionsSeeder --force

# Clear specific application caches
php artisan cache:forget dashboard-stats
php artisan cache:forget featured-products

# Notify Slack (non-critical, so || true)
curl -s -X POST -H 'Content-type: application/json' \
  --data '{"text":"✅ Production deployment completed"}' \
  https://hooks.slack.com/services/YOUR/WEBHOOK/URL || true
Enter fullscreen mode Exit fullscreen mode

Debugging Failed Commands

When a custom command fails, Deploynix logs the full output including the error message and which command failed. This information appears in the real-time deployment log via WebSocket streaming.

Common Failure Causes

Permission errors. Commands run as the site's system user. If a command requires root access, it will fail. Ensure file permissions allow the deploy user to execute the necessary commands.

Missing dependencies. If a command relies on a CLI tool (like sentry-cli or aws), that tool must be installed on the server. Install dependencies before relying on them in deploy commands.

Network issues. Commands that make external HTTP requests (Slack, APIs) can fail if the external service is down. For non-critical notifications, add || true to prevent the failure from blocking the deployment.

Timeout. Long-running commands can timeout. If you have an operation that takes several minutes, consider running it as a queued job instead of a deploy command.

Testing Commands

Before adding commands to your deployment, test them on the server via SSH. Run each command manually to verify it works with the correct permissions, in the correct directory, with access to the required tools.

Commands and Rollback

When you trigger a rollback on Deploynix (available for zero-downtime sites), the current symlink switches to a previous release and PHP-FPM reloads. Rollback is instant — it does not re-clone, re-install, or re-run any deploy scripts.

A few things to keep in mind:

  • Database migrations are not reversed during a rollback. If you need to reverse a migration, run php artisan migrate:rollback manually via SSH.
  • Queue workers and Horizon should be restarted after a rollback so they load the rolled-back code. Run php artisan queue:restart or php artisan horizon:terminate manually.
  • Cache may need clearing if the rolled-back code expects different cached data.

These manual steps are the trade-off for instant rollback speed. An automated rollback that tries to reverse migrations and clear caches would take minutes instead of seconds.

Best Practices

  1. Don't duplicate the default script. Migrations, cache commands, dependency installs, and queue restarts already run automatically. Adding them as custom commands would run them twice.
  2. Use || true for non-critical commands. A Slack notification failure shouldn't block your deployment. Mark non-critical commands as soft failures so they don't abort the deploy.
  3. Keep commands idempotent. If a deployment is retried, your commands run again. Ensure running them twice doesn't cause problems.
  4. Log meaningful output. Command output appears in the deployment log. Use echo statements to indicate progress:
echo "Terminating Horizon workers..."
php artisan horizon:terminate
echo "Horizon terminated, supervisor will restart."
Enter fullscreen mode Exit fullscreen mode
  1. Order matters. Commands execute top to bottom. If one command depends on another's result, put it later in the script. A failing command (with set -e active) stops execution of all subsequent commands.
  2. Move heavy operations to queued jobs. If an operation takes more than a few seconds (cache warming, data processing), dispatch it as a queued job from your application code rather than blocking the deployment.

Conclusion

Deploynix handles the standard deployment tasks automatically — dependencies, assets, caches, migrations, and queue restarts. Additional deploy commands are for everything else: Horizon restarts, seeders, notifications, CDN invalidation, health checks, and any other task specific to your application.

Your commands are integrated into the deployment pipeline, logged in real-time, and in zero-downtime mode they run before the symlink switches — so if something fails, users never see the broken release.

Define your commands once. They run on every deployment, consistently and predictably.

Top comments (0)