symflip
All posts

Migrate from Envoyer to symflip (multi-server)

How to move an Envoyer project that deploys to several servers onto symflip: hooks, .env, shared storage, the primary server and a safe cutover.

If you run Laravel on more than one server, you have probably read this line on the Forge pricing page: Forge "only supports deployments with zero downtime on one server, unlike Envoyer which can deploy one project to multiple servers." And a little further down, if you deploy to multiple servers with Envoyer, "you'll want to keep your subscription for the time being."

Forge's own "Migrate to Forge" tool won't help you either. Its docs say the Envoyer project "must be connected to a single server." So if your project lives on two web servers behind a load balancer, you stay on Envoyer, or you move somewhere else. Laravel's wording ("still", "for the time being") suggests this could change, so check the Forge comparison for the current state.

This post is the "somewhere else" guide for symflip. It covers mapping your Envoyer hooks, moving your environment, shared storage, picking a primary server for migrations, and a cutover you can undo. All of the Laravel and Forge details were checked on 18 October 2026.

First, a straight answer on whether you should bother.

Should you move at all?

Envoyer is still sold, still maintained, and Laravel says it "will be maintained for the foreseeable future." If it does what you need, you don't have to leave. The symflip vs Envoyer comparison lays out where each one is the better pick.

Moving to symflip makes sense if one of these is true:

  • You want to self-host your deploy tool, or you want it to be open source. symflip is Apache-2.0 and runs as one Go binary plus Postgres.
  • You want every server to build the new release first, with none of them going live until all of them are ready.
  • You want a failed health check to roll the release back on its own, across every server (auto-rollback is a per-project switch).
  • You want approvals before production, scheduled deploys, or an audit log of who shipped what.

Stay on Envoyer, or keep it running next to symflip, if you depend on these, because symflip doesn't have them today:

  • Heartbeats (cron job monitoring).
  • A public API or CLI to trigger deploys from scripts.
  • Configurable linked folders. symflip shares .env and storage/ between releases and nothing else. There is a workaround below, but it is a workaround.
  • FreeBSD servers. symflip targets Linux.
  • Unlimited team members on a small plan. symflip caps seats per plan: 5 on Team, 15 on Business.

Also be clear on what symflip is not. It doesn't provision servers, install PHP or write your nginx config. Keep Forge, Ploi or Ansible for that. symflip replaces the deploy part, which is the part Envoyer did.

What you need before you start

  • A symflip account on Team or above. Deploying one project to more than one server starts on Team (EUR29/month), and so do the pre-activate and post-deploy hooks. The build command is on every plan. If you self-host symflip, run it however you like.
  • SSH access to every server, as the user that owns your app files. On a Forge box that's usually forge.
  • Your servers must be able to pull from your Git repository. symflip runs git on the server itself, so a private repo needs read access from each box, for example a deploy key added in GitHub or GitLab. If Envoyer was cloning for you, check this first.
  • A quiet window for the final switch. It's short, and it's reversible, but do it when you're watching.

Step 1: write down what Envoyer does today

Open your Envoyer project and copy out four things. You'll map each one to symflip in the next steps.

  1. Deployment hooks. Every command you added before or after an action, and which action it hangs off.
  2. Environment. The .env contents Envoyer manages for you.
  3. Linked folders. Anything beyond storage.
  4. Servers. Hostnames, SSH users, PHP versions, and which server you'd trust to run migrations.

Also note where the project lives on disk (something like /home/forge/example.com) and how your nginx root, cron entries and queue workers point into it. You'll need those paths at cutover.

Step 2: map your hooks to symflip's pipeline

symflip runs a fixed pipeline on every server. You fill in the commands; it decides the order. Here's how Envoyer's typical hooks land (more Laravel setup notes are on the zero-downtime Laravel deployment page):

What you do in Envoyer Where it goes in symflip Runs
Install Composer Dependencies, plus any npm build Build command In the new release, before go-live, on every server
php artisan migrate --force Migrate command Once, on the primary server, before go-live
config:cache, route:cache, view:cache, event:cache Pre-activate hook In the new release, before go-live, on every server
Reload PHP-FPM Reload command Right after the symlink flip
queue:restart, horizon:terminate Post-deploy hook After go-live, on every server
Ping a health endpoint Health check command After go-live, with retries and optional auto-rollback

A starting point for a typical Laravel app:

# Build command
composer install --no-dev --optimize-autoloader --no-interaction
npm ci && npm run build

# Migrate command
php artisan migrate --force

# Pre-activate hook
php artisan config:cache
php artisan route:cache
php artisan view:cache

# Reload command
sudo -n systemctl reload php8.3-fpm

# Post-deploy hook
php artisan queue:restart

# Health check command
curl -fsS http://127.0.0.1/up -H 'Host: example.com'

The build, migrate and hook fields run under set -e, so the first failing line stops the deploy. The reload and health check commands don't, so keep each of those to a single command.

The reload needs passwordless sudo for that one command. symflip's server setup command prints the sudoers line to add. On a Forge server, use the command Forge's own deploy script uses instead: sudo -S service php8.3-fpm reload. Either way, adjust the PHP-FPM service name to your version. The /up route ships with Laravel 11 and later; on older apps, point the check at any route that touches the database.

The important difference from a hand-built hook list: everything above the symlink flip is safe to fail. If Composer, the build, a migration or a pre-activate hook fails on any server, the deploy stops and every server keeps serving the release it had.

Step 3: move your environment into symflip secrets

In symflip, open the project's Secrets and add each line from your Envoyer environment as a key and value. symflip seals them at rest, and on each deploy writes them to shared/.env on every server, then links that file into the new release.

That means one source of truth for .env across the fleet. You stop copying the file by hand, and every deploy rewrites the same file on every server. The flip side is that symflip owns the file: edits made directly on a server get overwritten on the next deploy. Make changes in symflip.

If you want to know exactly how that storage works, I wrote it up in How symflip handles your SSH keys and secrets.

Step 4: shared storage and linked folders

symflip's layout on each server looks like this:

/home/forge/example.com-symflip/
  current -> releases/20261018T140200.000000Z
  releases/
  shared/
    .env
    storage/
  repo/

shared/storage is linked into every release as storage/, which covers Laravel's logs, cache, sessions and storage/app/public uploads.

Copy your existing storage across once, on each server, before the first deploy:

mkdir -p /home/forge/example.com-symflip/shared
rsync -a /home/forge/example.com/storage/ /home/forge/example.com-symflip/shared/storage/

Check the source path against your own server; it's wherever Envoyer's shared storage lives for your project.

If Envoyer links extra folders, for example public/uploads, symflip won't do it for you yet. Put the folder under shared/ yourself and link it in the pre-activate hook:

mkdir -p /home/forge/example.com-symflip/shared/uploads
ln -sfn /home/forge/example.com-symflip/shared/uploads public/uploads

The hook runs inside the new release before it goes live, so the link is in place the moment traffic arrives.

Step 5: add your servers and pick a primary

Create the project in symflip with your repository, branch and a new deploy path, like /home/forge/example.com-symflip. Don't reuse the Envoyer directory. A fresh path means both tools can sit side by side until you switch, and switching back is trivial.

Then add each server. symflip generates a deploy key and a one-time setup command for it; run that command on the box and click test. Mark one server as primary. That's where the migrate command runs, once per deploy, before any server goes live. Pick a server that can reach the database and isn't special in any other way.

Step 6: a dry run, while Envoyer still serves traffic

Nginx still points at the Envoyer directory, so a symflip release isn't serving anyone yet. Before you click deploy, know that some fields act on the live app even now:

  • Migrate runs against the production database. If a migration is pending, deploy the same commit that's already live, or leave the field empty.
  • Post-deploy commands like php artisan queue:restart signal through the shared cache, so they restart the live Envoyer workers. Leave the field empty.
  • Reload reloads the live PHP-FPM. That's harmless but real.
  • Health check hits nginx, which still serves the old release, so it proves nothing yet. Leave it empty, or point it at a temporary vhost on the new path.

Then click deploy and watch the log stream: you'll see every server fetch, build and run its pre-activate hook, the migrations run once on the primary, and then each server switch its current symlink.

Check the result on a server:

ls -l /home/forge/example.com-symflip/current
cd /home/forge/example.com-symflip/current && php artisan about

Fill the post-deploy and health check fields back in before the cutover.

Step 7: cut over

This is the only step that touches live traffic. On each server:

  1. Point nginx at the new release. In Forge, edit the site's nginx configuration and set root to /home/forge/example.com-symflip/current/public, then reload nginx. The new path sits next to the site folder, so edit the config directly rather than the web directory field. A reload is graceful; in-flight requests finish on the old config.
  2. Update the scheduler cron. schedule:run should run from the new current path.
  3. Update queue workers. Point your Supervisor or Forge daemon command at the new current path and restart the workers.
  4. Turn off Envoyer's push-to-deploy so a push doesn't trigger two tools. Then set up symflip's webhook (GitHub or GitLab) if you want push-to-deploy.

If anything looks wrong, point nginx back at the Envoyer directory and reload. Nothing in the old directory changed.

Step 8: run both for a while, then cancel

Keep your Envoyer subscription for a couple of weeks of real deploys. Once you've shipped a few releases on symflip and nobody misses the old setup, cancel it and delete the old directory on each server.

If you relied on Envoyer heartbeats, replace them before you cancel. symflip doesn't monitor cron jobs. Any external heartbeat service will do.

What changes day to day

  • Fleet deploys build everywhere before the switch. Every server builds first; if one fails, none go live. Then each server switches in parallel. If any server fails to activate, or the release fails its health check with auto-rollback on, the servers that already went live roll back, so the fleet ends on one release. If a rollback itself fails on a host, symflip says so loudly instead of hiding it.
  • Rollback doesn't rebuild. Old releases stay on disk; new projects keep five by default. On a fleet, rely on the automatic rollback after a failed health check: the one-click rollback button only covers single-server projects today.
  • Rollback doesn't undo migrations. No symlink tool can. Read The one thing symflip can't undo for the expand and contract pattern that makes this a non-issue.
  • Notifications go to an incoming webhook that Slack and Mattermost accept. For Discord, use the channel's webhook URL with /slack on the end.
  • On Business you also get approvals before go-live and an audit log.

If you want to see exactly what runs on each server and in what order, Anatomy of a release, step by step walks through it line by line.

Try it on one project

Start with one project you know well. The free plan covers a single server, so you can learn the pipeline there; adding the second server needs Team (EUR29/month, cancel anytime). Create an account or compare plans.

If you get stuck moving a real project, email your Envoyer hook list to hello@symflip.com and I'll send back how it maps to symflip's fields.

Laravel, Laravel Forge and Laravel Envoyer are trademarks of their owner. symflip is not affiliated with or endorsed by Laravel. Competitor details are as published on the linked pages on 18 October 2026.