The tool your app uses to get itself onto a real server — provision a machine, push your code, wire up a database, and keep it updated. A set of CLI commands, not framework machinery.
ForgeDeployment arranges the whole journey from "working in a folder" to "running in the cloud". It talks directly to a cloud provider to create a server, connects over SSH to install PHP, a database, and Nginx, then uploads your project and configures the site.
After the first full deploy, you don't repeat the heavy lifting. You push updated files with an incremental command, roll back to a previous commit if something breaks, and check the current state at any time.
Framing: this is the tooling you run from your own project to ship it. There's no "framework" layer to learn — a few commands, a config file, and your provider credentials.
php forge.php package:install-module --module=forge-deployment
Two environment variables hold the credentials for the providers you use:
| Variable | Used for |
|---|---|
FORGE_DEPLOYMENT_DIGITALOCEAN_API_TOKEN |
Creating and managing servers on DigitalOcean |
FORGE_DEPLOYMENT_CLOUDFLARE_API_TOKEN |
Configuring DNS records on Cloudflare |
Deployment settings live in a PHP file at your project root. Generate a starter one with:
php forge.php modules:forge-deployment:init
This creates forge-deployment.php
(the loader also accepts deployment.php),
returning an array with three sections:
| Section | What it describes |
|---|---|
server |
Name, region, size, OS image, and SSH key path for the new machine |
provision |
PHP version, database type/version, and the app database name/user/password |
deployment |
Domain, SSL email, commands to run, post-deployment commands, and extra env vars |
The post-deployment commands are the useful part — run on the server after files land, each through your PHP binary. A typical set from the generated template:
'post_deployment_commands' => [
'cache:flush',
'cache:warm',
'db:migrate --type=all',
'storage:link',
'modules:forge-deployment:fix-permissions',
'asset:link --type=module --module=forge-wire',
],
Any command starting with php forge.php
has its php swapped for the provisioned
version (e.g. php8.4); plain strings
are run as {php} forge.php {command}.
All commands are prefixed with
modules:forge-deployment: and most are
wizard-driven — run them bare to be prompted, or
pass options to skip the questions.
| Command | Purpose |
|---|---|
...:init |
Generate the forge-deployment.php config file |
...:deploy |
Full wizard-driven deploy (--provider, --ssh-key, --config) |
...:create-server |
Create a VPS on the provider (--provider, --ssh-key) |
...:delete-server |
Delete the deployed server and related resources |
...:provision |
Provision an existing server (--host, --ssh-key, --ram) |
...:deploy-app |
Upload your app to a provisioned server (--host, --domain, --php-version) |
...:deploy-env |
Deploy a merged .env to the server |
...:update |
Push changed files to an existing server (--skip-commands, --working-tree, --force-full) |
...:rollback |
Revert to the previous commit (--skip-commands, --skip-confirmation) |
...:resume |
Continue a failed deployment from its last checkpoint |
...:status |
Show the current deployment state and completed steps |
...:reset |
Clear the saved deployment state to start fresh (--force) |
...:setup-ssl |
Issue a Let's Encrypt certificate (--host, --domain, --php-version) |
...:configure-dns |
Add the A record via Cloudflare (--domain, --ip) |
...:fix-permissions |
Fix ownership and permissions in the current project root |
A full deploy walks through a fixed set
of steps, saving each one as it completes:
/var/www/<domain>.
Each finished step is written to a state file,
.forge-deployment-state.json at your
project root. It records the server IP and ID, the
domain, which steps are done, and — once you ship an
update — the last deployed commit. That state is
what lets resume pick up a failed deploy
right where it stopped and update know
exactly what changed since last time.
Scope guard: the source tree is
filtered by a .forgeignore file at
your project root (gitignore-style — !
to un-ignore, /* or trailing
/ for directories, * and
? wildcards, # for
comments). Keep secrets and heavy assets out of
what gets shipped.
Once your app is live, update deploys
just what changed. It compares commits through git,
uploads only the changed files, then runs your
post-deployment commands:
php forge.php modules:forge-deployment:update
php forge.php modules:forge-deployment:update --force-full # full re-sync
php forge.php modules:forge-deployment:update --working-tree # diff against uncommitted changes
--skip-commands uploads files but skips
the post-deployment commands, handy for a quick
asset push.
If a deploy goes wrong, rollback restores
the previously deployed commit's files and re-runs
commands (or skips them with
--skip-commands). Both commands verify
the server is reachable first and refuse to run if it
isn't.
Note: incremental updates rely on
a git repository. If your project isn't a git
working tree, use deploy or
update --force-full to push the whole
tree.
Your deployment.env_vars config entries
are merged on top of your local
.env (or env-example if
.env is missing) and written to the
server as its .env. Database settings
from the config are folded in automatically, so the
remote app points at the database just provisioned:
'env_vars' => [
'APP_ENV' => 'production',
'APP_DEBUG' => 'false',
'CENTRAL_DOMAIN' => 'domain.com',
'CORS_ALLOWED_ORIGINS' => ['https://domain.com'],
],
If there's no local .env, the deployment
generates an application key on the server for you.
Use deploy-env at any time to push a
freshly merged .env without re-uploading
the whole app.
Keep it out of the archive: your
real secrets belong in the server's .env,
not in files that ride along in the deployment zip.
Add anything sensitive to .forgeignore.
DigitalOcean ships as the built-in provider for
creating and managing servers — droplets, SSH keys,
regions, sizes, and images. The provider system is a
small contract (createServer,
status, delete,
listing helpers), so other providers can be added the
same way.
Two supporting pieces round out a real deploy:
setup-ssl issues a Let's Encrypt certificate for your domain and points Nginx at it.configure-dns creates the A record through Cloudflare pointing your domain at the server IP.
When you work through the Hub, deployment history is
kept under storage/framework/deployments/,
and sensitive config values (tokens, passwords,
secrets) are masked so they never show up in logs.