A comprehensive, production-ready PHP SDK for the Laravel Forge API. Built with Saloon, this package provides a clean, strongly-typed interface for managing your Laravel Forge infrastructure programmatically.
Warning
This package is currently under active development. Once a 0.* version has been tagged, we strongly recommend locking your application to a specific working version because we might make breaking changes even in patch releases until we've tagged 1.0.
- 🚀 Complete API Coverage - All 132+ Laravel Forge API endpoints
- 🎯 Strongly Typed - PHP 8.3+ enums for all API values (server types, PHP versions, database types, etc.)
- 🛠️ Atomic Artisan Commands - Individual commands for each operation (create, list, destroy, etc.)
- 🔒 Production Ready - Confirmation prompts for destructive operations with
--dangerously-skip-confirmationflag - 📝 Comprehensive Logging - Configurable logging for all API operations and command executions
- ✅ Laravel & Laravel Zero Compatible - Minimal dependencies for maximum portability
- 🧪 Fully Tested - Comprehensive test suite with mocked API responses
- 📚 Excellent Documentation - Detailed examples for every resource and command
- Installation
- Configuration
- Quick Start
- Basic Usage
- Artisan Commands Reference
- Enums
- Error Handling
- Troubleshooting
- Contributing
- License
Install the package via Composer:
composer require artisan-build/forge-clientThe package works out of the box, but you can publish the configuration file to customize settings:
php artisan vendor:publish --tag="forge-client-config"This creates config/forge-client.php where you can configure API settings, logging, retry behavior, and defaults.
Set your Forge API token in your .env file:
FORGE_API_TOKEN=your-forge-api-token-hereYou can generate an API token from your Forge account settings.
# Default organization (slug or ID) - optional
FORGE_ORGANIZATION=my-organization
# Default server (name or ID) - optional
FORGE_SERVER=my-server
# Server Creation Defaults - optional
FORGE_PHP_VERSION=php84
FORGE_DATABASE=mysql8
# API Configuration
FORGE_API_URL=https://forge.laravel.com/api/v1
FORGE_TIMEOUT=30
# Retry Configuration
FORGE_RETRY_TIMES=3
FORGE_RETRY_SLEEP=1000
# Logging Configuration
FORGE_LOG_CHANNEL=stack
FORGE_LOG_LEVEL=infoSetting default organization and server in your configuration simplifies commands:
// config/forge-client.php
return [
'default_organization' => env('FORGE_ORGANIZATION', 'my-org'),
'default_server' => env('FORGE_SERVER', 'production-server'),
];With defaults configured, you can run commands without specifying organization/server:
# Without defaults
php artisan forge:list-sites --server=my-server --organization=my-org
# With defaults configured
php artisan forge:list-sitesAll commands follow a consistent argument ordering pattern: most specific → least specific.
This means resource-specific identifiers always come before broader context arguments:
# Correct: deployment → site → server → organization
php artisan forge:get-deployment {deployment} --site={site} --server={server} --organization={org}
# Correct: site → server → organization
php artisan forge:get-site {site} --server={server} --organization={org}
# Correct: database-user → server → organization
php artisan forge:get-database-user {user} --server={server} --organization={org}
# Correct: server → organization
php artisan forge:get-server {server} --organization={org}This pattern makes commands intuitive and predictable across all resources. When both organization and server can have config defaults, the more specific resource (server) is still specified before the broader context (organization).
use ArtisanBuild\ForgeClient\ForgeClient;
// Initialize the SDK
$forge = new ForgeClient();
// Authenticate with your API token
$forge->authenticate(config('forge-client.api_token'));
// List all organizations
$response = $forge->organizations()->organizationsIndex(
sort: '-created_at',
pagesize: 25,
pagecursor: null
);
$organizations = $response->json('data');
foreach ($organizations as $org) {
echo "Organization: {$org['name']} (ID: {$org['id']})\n";
}# List all organizations
php artisan forge:list-organizations
# Get a specific server (by ID or name)
php artisan forge:get-server 12345
php artisan forge:get-server production-server
# Create a new server (requires confirmation)
php artisan forge:create-server \
--organization=my-org \
--name=staging-server \
--provider=ocean2 \
--credential=123 \
--size=456 \
--region=nyc3
# Deploy a site (requires confirmation)
php artisan forge:deploy-site my-production-app
# Automated workflows (skip confirmation)
php artisan forge:deploy-site my-production-app --dangerously-skip-confirmationOrganizations are the top-level entity in Forge. All servers, sites, and resources belong to an organization.
use ArtisanBuild\ForgeClient\ForgeClient;
$forge = new ForgeClient();
$forge->authenticate(config('forge-client.api_token'));
$response = $forge->organizations()->organizationsIndex(
sort: '-created_at',
pagesize: 25,
pagecursor: null
);
$organizations = $response->json('data');Artisan Command:
php artisan forge:list-organizations$response = $forge->organizations()->organizationsShow(
organization: 'my-organization' // slug or ID
);
$organization = $response->json('data');Artisan Command:
php artisan forge:get-organization my-organizationServers are the core infrastructure managed by Forge.
$response = $forge->servers()->organizationsServersIndex(
organization: 'my-org',
sort: '-created_at',
pagesize: 25,
pagecursor: null,
filteripAddress: null,
filtername: null,
filterregion: null,
filtersize: null,
filterprovider: null,
filterubuntuVersion: null,
filterphpVersion: null,
filterdatabaseType: null
);
$servers = $response->json('data');Artisan Command:
# List all servers in organization
php artisan forge:list-servers --organization=my-org
# With filters
php artisan forge:list-servers \
--organization=my-org \
--filter-provider=ocean2 \
--filter-region=nyc3$response = $forge->servers()->organizationsServersShow(
organization: 'my-org',
server: 12345 // server ID
);
$server = $response->json('data');Artisan Command:
# By ID
php artisan forge:get-server 12345 --organization=my-org
# By name (automatically resolved to ID)
php artisan forge:get-server production-server --organization=my-orguse ArtisanBuild\ForgeClient\Enums\CloudProvider;
use ArtisanBuild\ForgeClient\Enums\ServerType;
use ArtisanBuild\ForgeClient\Enums\DatabaseType;
use ArtisanBuild\ForgeClient\Enums\PhpVersion;
use ArtisanBuild\ForgeClient\Enums\UbuntuVersion;
$response = $forge->servers()->organizationsServersStore(
organization: 'my-org'
);
// Note: Body parameters are set on the request object
// Check the generated request class for available parametersArtisan Command:
php artisan forge:create-server \
--organization=my-org \
--name=new-server \
--provider=ocean2 \
--credential=123 \
--size=456 \
--region=nyc3 \
--type=app \
--php-version=php84 \
--database=mysql8 \
--ubuntu-version=24.04Available Enums:
- CloudProvider:
aws,ocean2,hetzner,vultr,akamai,laravel,custom - ServerType:
app,web,loadbalancer,database,cache,worker,meilisearch - DatabaseType:
mysql8,mysql,mariadb,postgres,none - PhpVersion:
php74,php80,php81,php82,php83,php84,php85 - UbuntuVersion:
2204,2404
$response = $forge->servers()->organizationsServersActionsStore(
organization: 'my-org',
server: 12345
);Artisan Command:
# Requires confirmation
php artisan forge:reboot-server 12345 --organization=my-org
# Skip confirmation for automation
php artisan forge:reboot-server 12345 \
--organization=my-org \
--dangerously-skip-confirmation$response = $forge->servers()->organizationsServersDestroy(
organization: 'my-org',
server: 12345
);Artisan Command:
# Requires confirmation (destructive operation)
php artisan forge:destroy-server 12345 --organization=my-org
# For automation
php artisan forge:destroy-server 12345 \
--organization=my-org \
--dangerously-skip-confirmationSites represent the web applications hosted on your servers.
$response = $forge->sites()->organizationsServersSitesIndex(
organization: 'my-org',
server: 12345,
sort: '-created_at',
pagesize: 25,
pagecursor: null,
filteraliases: null,
filtername: null
);
$sites = $response->json('data');Artisan Command:
php artisan forge:list-sites \
--organization=my-org \
--server=12345$response = $forge->sites()->organizationsServersSitesShow(
organization: 'my-org',
server: 12345,
site: 67890
);
$site = $response->json('data');Artisan Command:
php artisan forge:get-site 67890 \
--organization=my-org \
--server=12345$response = $forge->sites()->organizationsServersSitesStore(
organization: 'my-org',
server: 12345
);
// Body parameters set on request objectArtisan Command:
php artisan forge:create-site \
--organization=my-org \
--server=12345 \
--domain=example.com \
--project-type=php \
--directory=/public$response = $forge->sites()->organizationsServersSitesUpdate(
organization: 'my-org',
server: 12345,
site: 67890
);Artisan Command:
php artisan forge:update-site 67890 \
--organization=my-org \
--server=12345 \
--directory=/public$response = $forge->sites()->organizationsServersSitesActionsStore(
organization: 'my-org',
server: 12345,
site: 67890
);Artisan Command:
# Triggers deployment (requires confirmation)
php artisan forge:deploy-site 67890 \
--organization=my-org \
--server=12345// Enable quick deploy
$response = $forge->sites()->organizationsServersSitesQuickDeployStore(
organization: 'my-org',
server: 12345,
site: 67890
);
// Disable quick deploy
$response = $forge->sites()->organizationsServersSitesQuickDeployDestroy(
organization: 'my-org',
server: 12345,
site: 67890
);Artisan Commands:
# Enable quick deploy
php artisan forge:enable-quick-deploy 67890 \
--organization=my-org \
--server=12345
# Disable quick deploy
php artisan forge:disable-quick-deploy 67890 \
--organization=my-org \
--server=12345$response = $forge->sites()->organizationsServersSitesDestroy(
organization: 'my-org',
server: 12345,
site: 67890
);Artisan Command:
# Requires confirmation (destructive)
php artisan forge:destroy-site 67890 \
--organization=my-org \
--server=12345Monitor and manage site deployments.
$response = $forge->deployments()->organizationsServersSitesDeploymentsIndex(
organization: 'my-org',
server: 12345,
site: 67890,
sort: '-created_at',
pagesize: 25,
pagecursor: null
);
$deployments = $response->json('data');Artisan Command:
php artisan forge:list-deployments 67890 \
--organization=my-org \
--server=12345$response = $forge->deployments()->organizationsServersSitesDeploymentsShow(
organization: 'my-org',
server: 12345,
site: 67890,
deployment: 111213
);
$deployment = $response->json('data');Artisan Command:
php artisan forge:get-deployment 111213 \
--site=67890 \
--organization=my-org \
--server=12345$response = $forge->deployments()->organizationsServersSitesDeploymentsStore(
organization: 'my-org',
server: 12345,
site: 67890
);Artisan Command:
# Requires confirmation
php artisan forge:trigger-deployment 67890 \
--organization=my-org \
--server=12345$response = $forge->deployments()->organizationsServersSitesDeploymentsScriptsUpdate(
organization: 'my-org',
server: 12345,
site: 67890
);
// Body contains the deployment script contentArtisan Command:
php artisan forge:update-deployment-script 67890 \
--organization=my-org \
--server=12345 \
--script="cd /home/forge/site && git pull && php artisan migrate --force"Manage MySQL/PostgreSQL databases and users.
$response = $forge->databases()->organizationsServersDatabasesIndex(
organization: 'my-org',
server: 12345,
sort: '-created_at',
pagesize: 25,
pagecursor: null,
filtername: null
);
$databases = $response->json('data');Artisan Command:
php artisan forge:list-databases \
--organization=my-org \
--server=12345$response = $forge->databases()->organizationsServersDatabasesShow(
organization: 'my-org',
server: 12345,
database: 44556
);
$database = $response->json('data');Artisan Command:
php artisan forge:get-database 44556 \
--organization=my-org \
--server=12345$response = $forge->databases()->organizationsServersDatabasesStore(
organization: 'my-org',
server: 12345
);
// Body parameters include database nameArtisan Command:
php artisan forge:create-database \
--organization=my-org \
--server=12345 \
--name=my_database$response = $forge->databases()->organizationsServersDatabasesDestroy(
organization: 'my-org',
server: 12345,
database: 44556
);Artisan Command:
# Requires confirmation (destructive)
php artisan forge:destroy-database 44556 \
--organization=my-org \
--server=12345$response = $forge->databases()->organizationsServersDatabaseUsersIndex(
organization: 'my-org',
server: 12345,
sort: '-created_at',
pagesize: 25,
pagecursor: null,
filtername: null
);
$users = $response->json('data');Artisan Command:
php artisan forge:list-database-users \
--organization=my-org \
--server=12345$response = $forge->databases()->organizationsServersDatabaseUsersStore(
organization: 'my-org',
server: 12345
);
// Body parameters include username, password, and databasesArtisan Command:
php artisan forge:create-database-user \
--organization=my-org \
--server=12345 \
--name=app_user \
--password=secure_password \
--databases=my_database$response = $forge->databases()->organizationsServersDatabaseUsersUpdate(
organization: 'my-org',
server: 12345,
databaseUser: 77889
);Artisan Command:
php artisan forge:update-database-user 77889 \
--organization=my-org \
--server=12345 \
--databases=my_database,another_database$response = $forge->databases()->organizationsServersDatabaseUsersDestroy(
organization: 'my-org',
server: 12345,
databaseUser: 77889
);Artisan Command:
# Requires confirmation (destructive)
php artisan forge:destroy-database-user 77889 \
--organization=my-org \
--server=12345Manage background processes (daemons) running on your servers.
$response = $forge->backgroundProcesses()->organizationsServersBackgroundProcessesIndex(
organization: 'my-org',
server: 12345,
sort: '-created_at',
pagesize: 25,
pagecursor: null,
filtercommand: null,
filterstatus: null
);
$processes = $response->json('data');Artisan Command:
php artisan forge:list-background-processes \
--organization=my-org \
--server=12345$response = $forge->backgroundProcesses()->organizationsServersBackgroundProcessesShow(
organization: 'my-org',
server: 12345,
backgroundProcess: 99887
);
$process = $response->json('data');Artisan Command:
php artisan forge:get-background-process 99887 \
--organization=my-org \
--server=12345$response = $forge->backgroundProcesses()->organizationsServersBackgroundProcessesStore(
organization: 'my-org',
server: 12345
);
// Body includes command, user, directoryArtisan Command:
php artisan forge:create-background-process \
--organization=my-org \
--server=12345 \
--command="php artisan horizon" \
--user=forge \
--directory=/home/forge/app$response = $forge->backgroundProcesses()->organizationsServersBackgroundProcessesUpdate(
organization: 'my-org',
server: 12345,
backgroundProcess: 99887
);Artisan Command:
php artisan forge:update-background-process 99887 \
--organization=my-org \
--server=12345 \
--command="php artisan horizon"$response = $forge->backgroundProcesses()->organizationsServersBackgroundProcessesActionsStore(
organization: 'my-org',
server: 12345,
backgroundProcess: 99887
);Artisan Command:
php artisan forge:restart-background-process 99887 \
--organization=my-org \
--server=12345$response = $forge->backgroundProcesses()->organizationsServersBackgroundProcessesDestroy(
organization: 'my-org',
server: 12345,
backgroundProcess: 99887
);Artisan Command:
# Requires confirmation
php artisan forge:destroy-background-process 99887 \
--organization=my-org \
--server=12345Manage server firewall rules for security.
$response = $forge->firewallRules()->organizationsServersFirewallRulesIndex(
organization: 'my-org',
server: 12345,
sort: '-created_at',
pagesize: 25,
pagecursor: null,
filtername: null,
filterport: null,
filtertype: null
);
$rules = $response->json('data');Artisan Command:
php artisan forge:list-firewall-rules \
--organization=my-org \
--server=12345$response = $forge->firewallRules()->organizationsServersFirewallRulesShow(
organization: 'my-org',
server: 12345,
firewallRule: 55443
);
$rule = $response->json('data');Artisan Command:
php artisan forge:get-firewall-rule 55443 \
--organization=my-org \
--server=12345use ArtisanBuild\ForgeClient\Enums\FirewallRuleType;
$response = $forge->firewallRules()->organizationsServersFirewallRulesStore(
organization: 'my-org',
server: 12345
);
// Body includes name, port, type, ip_addressArtisan Command:
php artisan forge:create-firewall-rule \
--organization=my-org \
--server=12345 \
--name="Allow Redis" \
--port=6379 \
--type=allow \
--ip-address=192.168.1.100Firewall Rule Types:
allow- Allow trafficdeny- Deny traffic
$response = $forge->firewallRules()->organizationsServersFirewallRulesDestroy(
organization: 'my-org',
server: 12345,
firewallRule: 55443
);Artisan Command:
# Requires confirmation
php artisan forge:destroy-firewall-rule 55443 \
--organization=my-org \
--server=12345Manage SSL certificates for your sites.
$response = $forge->sites()->organizationsServersSitesCertificatesIndex(
organization: 'my-org',
server: 12345,
site: 67890,
sort: '-created_at',
pagesize: 25,
pagecursor: null,
filterdomain: null
);
$certificates = $response->json('data');Artisan Command:
php artisan forge:list-ssl-certificates 67890 \
--organization=my-org \
--server=12345$response = $forge->sites()->organizationsServersSitesCertificatesShow(
organization: 'my-org',
server: 12345,
site: 67890,
certificate: 22334
);
$certificate = $response->json('data');Artisan Command:
php artisan forge:get-ssl-certificate 22334 \
--site=67890 \
--organization=my-org \
--server=12345use ArtisanBuild\ForgeClient\Enums\CertificateType;
$response = $forge->sites()->organizationsServersSitesCertificatesStore(
organization: 'my-org',
server: 12345,
site: 67890
);
// Body includes domain, type (letsencrypt, existing, clone)Artisan Command:
# Let's Encrypt certificate
php artisan forge:create-ssl-certificate 67890 \
--organization=my-org \
--server=12345 \
--domain=example.com \
--type=letsencrypt
# Existing certificate
php artisan forge:create-ssl-certificate 67890 \
--organization=my-org \
--server=12345 \
--type=existing \
--certificate="$(cat certificate.crt)" \
--key="$(cat private.key)"$response = $forge->sites()->organizationsServersSitesCertificatesActionsStore(
organization: 'my-org',
server: 12345,
site: 67890,
certificate: 22334
);Artisan Command:
php artisan forge:activate-ssl-certificate 22334 \
--site=67890 \
--organization=my-org \
--server=12345$response = $forge->sites()->organizationsServersSitesCertificatesDestroy(
organization: 'my-org',
server: 12345,
site: 67890,
certificate: 22334
);Artisan Command:
# Requires confirmation
php artisan forge:destroy-ssl-certificate 22334 \
--site=67890 \
--organization=my-org \
--server=12345The Commands resource represents one-off commands executed on servers (not to be confused with Artisan commands).
// List server commands
$response = $forge->commands()->organizationsServersCommandsIndex(
organization: 'my-org',
server: 12345,
sort: '-created_at',
pagesize: 25,
pagecursor: null
);Manage cron jobs on your servers.
// List scheduled jobs
$response = $forge->scheduledJobs()->organizationsServersScheduledJobsIndex(
organization: 'my-org',
server: 12345,
sort: '-created_at',
pagesize: 25,
pagecursor: null
);The SDK provides full access to all Forge API resources:
- Integrations - Source control providers (GitHub, GitLab, Bitbucket)
- Logs - Server and application logs
- Monitors - Server monitoring configuration
- Nginx - Nginx configuration templates
- Providers - Cloud provider credentials
- Recipes - Server provisioning recipes
- Redirect Rules - Site redirect/rewrite rules
- Roles - Organization role management
- SSH Keys - Server SSH key management
- Security Rules - Additional security configurations
- Server Credentials - Server access credentials
- Teams - Organization team management
- User - Current user information
All resources follow the same pattern as shown above. Check the resource classes in src/Resource/ for available methods.
All commands support the --dangerously-skip-confirmation flag to bypass confirmation prompts for automation.
php artisan forge:list-organizationsOptions:
- None
php artisan forge:get-organization {organization}Arguments:
organization- Organization slug or ID
Example:
php artisan forge:get-organization my-organizationphp artisan forge:list-server-credentials {organization?}Arguments:
organization- Organization slug or ID (optional ifFORGE_ORGANIZATIONis set)
Options:
--pagesize=- Number of results per page--pagecursor=- Cursor for pagination
Example:
# Using organization argument
php artisan forge:list-server-credentials my-organization
# Or using environment variable
export FORGE_ORGANIZATION="my-organization"
php artisan forge:list-server-credentialsThis command lists all server credentials (cloud provider API keys) configured for your organization. You'll need the credential ID when creating servers.
php artisan forge:list-providersOptions:
--pagesize=- Number of results per page--pagecursor=- Cursor for pagination
Example:
php artisan forge:list-providersphp artisan forge:list-provider-regions {provider}Arguments:
provider- Provider ID
Options:
--pagesize=- Number of results per page--pagecursor=- Cursor for pagination
Example:
# First, get the provider ID
php artisan forge:list-providers
# Then list regions for that provider
php artisan forge:list-provider-regions 1php artisan forge:list-provider-sizes {provider}Arguments:
provider- Provider ID
Options:
--pagesize=- Number of results per page--pagecursor=- Cursor for pagination
Example:
# First, get the provider ID
php artisan forge:list-providers
# Then list sizes for that provider
php artisan forge:list-provider-sizes 1php artisan forge:list-serversOptions:
--organization=- Organization slug or ID (optional if default configured)--filter-name=- Filter by server name--filter-provider=- Filter by cloud provider--filter-region=- Filter by region--filter-size=- Filter by server size--filter-ip-address=- Filter by IP address--filter-ubuntu-version=- Filter by Ubuntu version--filter-php-version=- Filter by PHP version--filter-database-type=- Filter by database type
Example:
php artisan forge:list-servers \
--organization=my-org \
--filter-provider=ocean2 \
--filter-region=nyc3php artisan forge:get-server {server}Arguments:
server- Server ID or name
Options:
--organization=- Organization slug or ID (optional if default configured)
Example:
# By ID
php artisan forge:get-server 12345 --organization=my-org
# By name
php artisan forge:get-server production-server --organization=my-orgphp artisan forge:create-serverOptions:
--organization=- Organization slug or ID (required unless set in config viaFORGE_ORGANIZATION)--name=- Server name (required)--provider=- Cloud provider: laravel, ocean2, hetzner, vultr, akamai, aws, custom (required)--credential=- Server credential ID (required - useforge:list-server-credentialsto find IDs)--size=- Server size ID (required - useforge:list-provider-sizesto find IDs)--region=- Region code (required - useforge:list-provider-regionsto find codes)--type=- Server type: app, web, database, cache, worker, meilisearch, scheduler, loadbalancer (default: app)--ubuntu-version=- Ubuntu version: 22.04, 24.04 (default: 24.04)--php-version=- PHP version: php81, php82, php83, php84 (optional - defaults toFORGE_PHP_VERSIONif set)--database=- Database type: mysql8, postgres, mariadb, none (optional - defaults toFORGE_DATABASEif set)--dangerously-skip-confirmation- Skip confirmation prompt (for automation)
Prerequisites: Before creating a server, you need to:
- Get a credential ID:
php artisan forge:list-server-credentials --organization=my-org - Get a provider ID:
php artisan forge:list-providers - Get a region code:
php artisan forge:list-provider-regions {provider-id} - Get a size ID:
php artisan forge:list-provider-sizes {provider-id}
Example:
# Create a DigitalOcean server with explicit options
php artisan forge:create-server \
--organization=my-org \
--name=staging-server \
--provider=ocean2 \
--credential=123 \
--region=nyc3 \
--size=456 \
--type=app \
--php-version=php84 \
--database=mysql8 \
--ubuntu-version=24.04
# Create a Laravel-managed server using config defaults
# (Assuming FORGE_PHP_VERSION=php84 and FORGE_DATABASE=mysql8 in .env)
php artisan forge:create-server \
--organization=my-org \
--name=laravel-server \
--provider=laravel \
--credential=789 \
--region=us-east \
--size=1php artisan forge:reboot-server {server}Arguments:
server- Server ID or name
Options:
--organization=- Organization slug or ID (optional if default configured)--dangerously-skip-confirmation- Skip confirmation prompt
Example:
# With confirmation
php artisan forge:reboot-server production-server
# Skip confirmation
php artisan forge:reboot-server production-server --dangerously-skip-confirmationphp artisan forge:destroy-server {server}Arguments:
server- Server ID or name
Options:
--organization=- Organization slug or ID (optional if default configured)--dangerously-skip-confirmation- Skip confirmation prompt
Example:
php artisan forge:destroy-server old-server --dangerously-skip-confirmationphp artisan forge:list-sitesOptions:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)--filter-name=- Filter by site name--filter-aliases=- Filter by site aliases
php artisan forge:get-site {site}Arguments:
site- Site ID or domain name
Options:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)
php artisan forge:create-siteOptions:
--organization=- Organization slug or ID (required)--server=- Server ID or name (required)--domain=- Domain name (required)--project-type=- Project type: laravel, symfony, statamic, wordpress, phpmyadmin, php, next.js, nuxt.js, static-html, other, custom (optional)--directory=- Web directory, e.g., /public (optional)
Example:
php artisan forge:create-site \
--organization=my-org \
--server=production-server \
--domain=example.com \
--project-type=php \
--directory=/publicphp artisan forge:update-site {site}Arguments:
site- Site ID or domain name
Options:
--organization=- Organization slug or ID (required)--server=- Server ID or name (required)--directory=- Update web directory
php artisan forge:deploy-site {site}Arguments:
site- Site ID or domain name
Options:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)--dangerously-skip-confirmation- Skip confirmation prompt
Example:
php artisan forge:deploy-site example.com --dangerously-skip-confirmationphp artisan forge:enable-quick-deploy {site}Arguments:
site- Site ID or domain name
Options:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)
php artisan forge:disable-quick-deploy {site}Arguments:
site- Site ID or domain name
Options:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)
php artisan forge:destroy-site {site}Arguments:
site- Site ID or domain name
Options:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)--dangerously-skip-confirmation- Skip confirmation prompt
php artisan forge:list-deployments {site}Arguments:
site- Site ID or domain name
Options:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)
php artisan forge:get-deployment {deployment}Arguments:
deployment- Deployment ID
Options:
--site=- Site ID or domain name (required)--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)
php artisan forge:trigger-deployment {site}Arguments:
site- Site ID or domain name
Options:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)--dangerously-skip-confirmation- Skip confirmation prompt
php artisan forge:update-deployment-script {site}Arguments:
site- Site ID or domain name
Options:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)--script=- Deployment script content (required)
Example:
php artisan forge:update-deployment-script example.com \
--script="cd /home/forge/example.com && git pull origin main && composer install --no-dev && php artisan migrate --force"php artisan forge:list-databasesOptions:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)--filter-name=- Filter by database name
php artisan forge:get-database {database}Arguments:
database- Database ID or name
Options:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)
php artisan forge:create-databaseOptions:
--organization=- Organization slug or ID (required)--server=- Server ID or name (required)--name=- Database name (required)
Example:
php artisan forge:create-database \
--organization=my-org \
--server=production-server \
--name=my_application_dbphp artisan forge:destroy-database {database}Arguments:
database- Database ID or name
Options:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)--dangerously-skip-confirmation- Skip confirmation prompt
php artisan forge:list-database-usersOptions:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)--filter-name=- Filter by username
php artisan forge:get-database-user {user}Arguments:
user- Database user ID or username
Options:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)
php artisan forge:create-database-userOptions:
--organization=- Organization slug or ID (required)--server=- Server ID or name (required)--name=- Username (required)--password=- Password (required)--databases=- Comma-separated list of database names to grant access (required)
Example:
php artisan forge:create-database-user \
--organization=my-org \
--server=production-server \
--name=app_user \
--password=secure_password_here \
--databases=my_app_db,my_app_cachephp artisan forge:update-database-user {user}Arguments:
user- Database user ID or username
Options:
--organization=- Organization slug or ID (required)--server=- Server ID or name (required)--databases=- Comma-separated list of database names to grant access (required)
php artisan forge:destroy-database-user {user}Arguments:
user- Database user ID or username
Options:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)--dangerously-skip-confirmation- Skip confirmation prompt
php artisan forge:list-background-processesOptions:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)--filter-command=- Filter by command--filter-status=- Filter by status
php artisan forge:get-background-process {process}Arguments:
process- Background process ID
Options:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)
php artisan forge:create-background-processOptions:
--organization=- Organization slug or ID (required)--server=- Server ID or name (required)--command=- Command to run (required)--user=- User to run as (optional, defaults to forge)--directory=- Working directory (optional)
Example:
php artisan forge:create-background-process \
--organization=my-org \
--server=production-server \
--command="php artisan horizon" \
--user=forge \
--directory=/home/forge/example.comphp artisan forge:update-background-process {process}Arguments:
process- Background process ID
Options:
--organization=- Organization slug or ID (required)--server=- Server ID or name (required)--command=- Updated command (required)
php artisan forge:restart-background-process {process}Arguments:
process- Background process ID
Options:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)--dangerously-skip-confirmation- Skip confirmation prompt
php artisan forge:destroy-background-process {process}Arguments:
process- Background process ID
Options:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)--dangerously-skip-confirmation- Skip confirmation prompt
php artisan forge:list-firewall-rulesOptions:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)--filter-name=- Filter by rule name--filter-port=- Filter by port--filter-type=- Filter by type (allow/deny)
php artisan forge:get-firewall-rule {rule}Arguments:
rule- Firewall rule ID
Options:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)
php artisan forge:create-firewall-ruleOptions:
--organization=- Organization slug or ID (required)--server=- Server ID or name (required)--name=- Rule name (optional)--port=- Port number (required)--type=- Rule type: allow, deny (required)--ip-address=- IP address to allow/deny (optional, defaults to 0.0.0.0/0)
Example:
php artisan forge:create-firewall-rule \
--organization=my-org \
--server=production-server \
--name="Allow Redis from app server" \
--port=6379 \
--type=allow \
--ip-address=192.168.1.100php artisan forge:destroy-firewall-rule {rule}Arguments:
rule- Firewall rule ID
Options:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)--dangerously-skip-confirmation- Skip confirmation prompt
php artisan forge:list-ssl-certificates {site}Arguments:
site- Site ID or domain name
Options:
--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)--filter-domain=- Filter by domain
php artisan forge:get-ssl-certificate {certificate}Arguments:
certificate- SSL certificate ID
Options:
--site=- Site ID or domain name (required)--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)
php artisan forge:create-ssl-certificate {site}Arguments:
site- Site ID or domain name
Options:
--organization=- Organization slug or ID (required)--server=- Server ID or name (required)--domain=- Domain name (required)--type=- Certificate type: letsencrypt, existing, clone (required)--certificate=- Certificate content (required if type=existing)--key=- Private key content (required if type=existing)
Example:
# Let's Encrypt
php artisan forge:create-ssl-certificate example.com \
--organization=my-org \
--server=production-server \
--domain=example.com \
--type=letsencrypt
# Existing certificate
php artisan forge:create-ssl-certificate example.com \
--organization=my-org \
--server=production-server \
--type=existing \
--certificate="$(cat /path/to/certificate.crt)" \
--key="$(cat /path/to/private.key)"php artisan forge:activate-ssl-certificate {certificate}Arguments:
certificate- SSL certificate ID
Options:
--site=- Site ID or domain name (required)--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)--dangerously-skip-confirmation- Skip confirmation prompt
php artisan forge:destroy-ssl-certificate {certificate}Arguments:
certificate- SSL certificate ID
Options:
--site=- Site ID or domain name (required)--organization=- Organization slug or ID (optional if default configured)--server=- Server ID or name (optional if default configured)--dangerously-skip-confirmation- Skip confirmation prompt
The SDK provides strongly-typed enums for all Forge API values, ensuring type safety and preventing invalid API requests.
use ArtisanBuild\ForgeClient\Enums\CloudProvider;
CloudProvider::AWS; // 'aws'
CloudProvider::OCEAN2; // 'ocean2' (DigitalOcean)
CloudProvider::HETZNER; // 'hetzner'
CloudProvider::VULTR; // 'vultr'
CloudProvider::AKAMAI; // 'akamai' (Linode/Akamai)
CloudProvider::LARAVEL; // 'laravel' (Laravel Cloud)
CloudProvider::CUSTOM; // 'custom'Helper Methods:
CloudProvider::OCEAN2->label(); // "DigitalOcean"
CloudProvider::OCEAN2->description(); // "DigitalOcean cloud hosting"
CloudProvider::isValid('ocean2'); // true
CloudProvider::options(); // Array of all optionsuse ArtisanBuild\ForgeClient\Enums\ServerType;
ServerType::APP; // 'app'
ServerType::WEB; // 'web'
ServerType::LOADBALANCER; // 'loadbalancer'
ServerType::DATABASE; // 'database'
ServerType::CACHE; // 'cache'
ServerType::WORKER; // 'worker'
ServerType::MEILISEARCH; // 'meilisearch'use ArtisanBuild\ForgeClient\Enums\DatabaseType;
DatabaseType::MYSQL8; // 'mysql8'
DatabaseType::MYSQL; // 'mysql'
DatabaseType::MARIADB; // 'mariadb'
DatabaseType::POSTGRES; // 'postgres'
DatabaseType::NONE; // 'none'Helper Methods:
DatabaseType::MYSQL8->label(); // "MySQL 8"
DatabaseType::MYSQL8->description(); // "MySQL 8.0"
DatabaseType::MYSQL8->isMySQL(); // true
DatabaseType::POSTGRES->isPostgreSQL(); // trueuse ArtisanBuild\ForgeClient\Enums\PhpVersion;
PhpVersion::PHP74; // 'php74'
PhpVersion::PHP80; // 'php80'
PhpVersion::PHP81; // 'php81'
PhpVersion::PHP82; // 'php82'
PhpVersion::PHP83; // 'php83'
PhpVersion::PHP84; // 'php84'
PhpVersion::PHP85; // 'php85'Helper Methods:
PhpVersion::PHP84->label(); // "PHP 8.4"
PhpVersion::PHP84->version(); // "8.4"
PhpVersion::PHP84->isSupported(); // true
PhpVersion::latest(); // PhpVersion::PHP85
PhpVersion::supported(); // Array of supported versionsuse ArtisanBuild\ForgeClient\Enums\UbuntuVersion;
UbuntuVersion::UBUNTU_22_04; // '2204'
UbuntuVersion::UBUNTU_24_04; // '2404'Helper Methods:
UbuntuVersion::UBUNTU_24_04->label(); // "Ubuntu 24.04"
UbuntuVersion::UBUNTU_24_04->version(); // "24.04"
UbuntuVersion::UBUNTU_24_04->isLTS(); // true
UbuntuVersion::latest(); // UbuntuVersion::UBUNTU_24_04use ArtisanBuild\ForgeClient\Enums\FirewallRuleType;
FirewallRuleType::ALLOW; // 'allow'
FirewallRuleType::DENY; // 'deny'use ArtisanBuild\ForgeClient\Enums\CertificateType;
CertificateType::LETSENCRYPT; // 'letsencrypt'
CertificateType::EXISTING; // 'existing'
CertificateType::CLONE; // 'clone'use ArtisanBuild\ForgeClient\Enums\SiteType;
SiteType::LARAVEL; // 'laravel'
SiteType::SYMFONY; // 'symfony'
SiteType::STATAMIC; // 'statamic'
SiteType::WORDPRESS; // 'wordpress'
SiteType::PHPMYADMIN; // 'phpmyadmin'
SiteType::PHP; // 'php'
SiteType::NEXTJS; // 'next.js'
SiteType::NUXTJS; // 'nuxt.js'
SiteType::STATIC_HTML; // 'static-html'
SiteType::OTHER; // 'other'
SiteType::CUSTOM; // 'custom'use ArtisanBuild\ForgeClient\Enums\IntegrationType;
IntegrationType::GITHUB; // 'github'
IntegrationType::GITLAB; // 'gitlab'
IntegrationType::BITBUCKET; // 'bitbucket'use ArtisanBuild\ForgeClient\Enums\JobFrequency;
JobFrequency::MINUTELY; // 'minutely'
JobFrequency::HOURLY; // 'hourly'
JobFrequency::NIGHTLY; // 'nightly'
JobFrequency::WEEKLY; // 'weekly'
JobFrequency::MONTHLY; // 'monthly'
JobFrequency::CUSTOM; // 'custom'use ArtisanBuild\ForgeClient\Enums\LogType;
LogType::APP; // 'app'
LogType::NGINX; // 'nginx'
LogType::PHP; // 'php'
LogType::MYSQL; // 'mysql'use ArtisanBuild\ForgeClient\Enums\MonitorType;
MonitorType::CPU; // 'cpu'
MonitorType::MEMORY; // 'memory'
MonitorType::DISK; // 'disk'
MonitorType::CUSTOM; // 'custom'Enums provide type safety and validation:
use ArtisanBuild\ForgeClient\Enums\CloudProvider;
use ArtisanBuild\ForgeClient\Enums\PhpVersion;
use ArtisanBuild\ForgeClient\Enums\DatabaseType;
// In your code
$provider = CloudProvider::OCEAN2;
$phpVersion = PhpVersion::PHP84;
$database = DatabaseType::MYSQL8;
// Validation
if (CloudProvider::isValid('ocean2')) {
// Valid provider
}
// Get all options for dropdowns
$providers = CloudProvider::options();
// Returns: ['ocean2' => 'DigitalOcean', 'akamai' => 'Akamai', ...]
// Get latest/recommended versions
$latestPhp = PhpVersion::latest(); // PHP 8.5
$supportedPhp = PhpVersion::supported(); // All supported versionsThe SDK provides comprehensive exception handling with detailed error messages.
Base exception for all Forge Client errors.
use ArtisanBuild\ForgeClient\Exceptions\ForgeException;
try {
$response = $forge->servers()->organizationsServersShow('my-org', 12345);
} catch (ForgeException $e) {
echo "Forge Error: {$e->getMessage()}";
echo "Context: " . json_encode($e->getContext());
}Methods:
getMessage()- Error messagegetContext()- Additional error context (array)getCode()- Error code
Thrown when request parameters fail validation before the API call.
use ArtisanBuild\ForgeClient\Exceptions\ValidationException;
try {
// Invalid PHP version
$response = $forge->servers()->organizationsServersStore('my-org');
} catch (ValidationException $e) {
echo "Validation Error: {$e->getMessage()}";
echo "Failed Field: {$e->getField()}";
echo "Invalid Value: {$e->getValue()}";
}Methods:
getField()- Field that failed validationgetValue()- Invalid value providedgetExpected()- Expected value format/type
Thrown when the Forge API returns an error response.
use ArtisanBuild\ForgeClient\Exceptions\ApiException;
try {
$response = $forge->servers()->organizationsServersShow('my-org', 99999);
} catch (ApiException $e) {
echo "API Error: {$e->getMessage()}";
echo "Status Code: {$e->getStatusCode()}";
echo "Response: " . json_encode($e->getResponseData());
}Methods:
getStatusCode()- HTTP status code (404, 500, etc.)getResponseData()- Full API response bodygetErrorCode()- Forge-specific error code
Thrown when API authentication fails.
use ArtisanBuild\ForgeClient\Exceptions\AuthenticationException;
try {
$forge->authenticate('invalid-token');
$response = $forge->organizations()->organizationsIndex();
} catch (AuthenticationException $e) {
echo "Authentication Error: {$e->getMessage()}";
echo "Troubleshooting:\n";
echo "1. Check your API token in .env\n";
echo "2. Verify token is active in Forge dashboard\n";
echo "3. Ensure token has required permissions\n";
}Thrown when Forge API rate limit is exceeded (60 requests per minute).
use ArtisanBuild\ForgeClient\Exceptions\RateLimitException;
try {
$response = $forge->servers()->organizationsServersIndex('my-org');
} catch (RateLimitException $e) {
echo "Rate Limit Exceeded: {$e->getMessage()}";
echo "Retry After: {$e->getRetryAfter()} seconds";
// Wait and retry
sleep($e->getRetryAfter());
$response = $forge->servers()->organizationsServersIndex('my-org');
}Methods:
getRetryAfter()- Seconds until rate limit resetsgetLimit()- Rate limit maximum (60)getRemaining()- Remaining requests
use ArtisanBuild\ForgeClient\Exceptions\{
ValidationException,
AuthenticationException,
RateLimitException,
ApiException,
ForgeException
};
try {
$response = $forge->servers()->organizationsServersShow('my-org', $serverId);
$server = $response->json('data');
} catch (ValidationException $e) {
// Handle validation errors (before API call)
Log::error('Invalid parameters', [
'field' => $e->getField(),
'value' => $e->getValue(),
]);
} catch (AuthenticationException $e) {
// Handle authentication failures
Log::error('Authentication failed', ['message' => $e->getMessage()]);
throw $e; // Re-throw for application-level handling
} catch (RateLimitException $e) {
// Handle rate limiting
Log::warning('Rate limit hit', ['retry_after' => $e->getRetryAfter()]);
sleep($e->getRetryAfter());
// Retry logic here
} catch (ApiException $e) {
// Handle API errors (404, 500, etc.)
if ($e->getStatusCode() === 404) {
Log::warning('Server not found', ['server_id' => $serverId]);
} else {
Log::error('API error', [
'status' => $e->getStatusCode(),
'response' => $e->getResponseData(),
]);
}
} catch (ForgeException $e) {
// Catch-all for any other SDK errors
Log::error('Forge Client error', ['message' => $e->getMessage()]);
}Add to app/Exceptions/Handler.php:
use ArtisanBuild\ForgeClient\Exceptions\RateLimitException;
use ArtisanBuild\ForgeClient\Exceptions\AuthenticationException;
public function register(): void
{
$this->reportable(function (RateLimitException $e) {
// Log rate limit hits for monitoring
Log::warning('Forge rate limit exceeded', [
'retry_after' => $e->getRetryAfter(),
]);
});
$this->reportable(function (AuthenticationException $e) {
// Alert team about auth failures
Log::critical('Forge authentication failed', [
'message' => $e->getMessage(),
]);
});
}Problem: AuthenticationException: Invalid API token
Solutions:
- Verify your API token in
.env:FORGE_API_TOKEN=your-actual-token
- Generate a new token at https://forge.laravel.com/user-profile/api
- Ensure the token has required permissions (full access recommended)
- Check the token wasn't revoked in Forge dashboard
Problem: RateLimitException: Too many requests
Details: Forge API limits requests to 60 per minute per API token.
Solutions:
-
Implement request throttling in your application:
use Illuminate\Support\Facades\Cache; $key = 'forge-api-requests'; $requests = Cache::increment($key); if ($requests === 1) { Cache::put($key, 1, now()->addMinute()); } if ($requests > 55) { sleep(60); // Wait for rate limit reset }
-
Batch operations when possible
-
Cache Forge data locally to reduce API calls
-
Use webhooks for real-time updates instead of polling
Problem: ApiException: Server not found (404)
Solutions:
- Verify the server ID is correct
- Check you're using the correct organization
- Ensure the server wasn't deleted
- If using server name, ensure it's unique:
# This might fail if multiple servers named "staging" php artisan forge:get-server staging # Use ID instead php artisan forge:get-server 12345
Problem: Artisan commands hang or fail
Solutions:
-
Ensure defaults are configured if not passing arguments:
FORGE_ORGANIZATION=my-org FORGE_SERVER=my-server
-
Check command syntax:
# Incorrect php artisan forge:create-server my-server # Correct php artisan forge:create-server \ --name=my-server \ --organization=my-org \ --provider=ocean2 \ --credential=123 \ --size=456 \ --region=nyc3
-
Use
--helpto see required options:php artisan forge:create-server --help
Problem: No logs appearing for Forge operations
Solutions:
-
Check logging configuration:
FORGE_LOG_CHANNEL=stack FORGE_LOG_LEVEL=info
-
Ensure log channel exists in
config/logging.php -
Verify log permissions:
chmod -R 775 storage/logs
Problem: Let's Encrypt certificate creation fails
Solutions:
- Verify domain DNS points to server
- Ensure site is accessible via HTTP first
- Check firewall allows ports 80 and 443
- Domain must be publicly accessible (no local/private IPs)
Problem: Requests timing out
Solutions:
-
Increase timeout in config:
FORGE_TIMEOUT=60
-
Check network connectivity:
curl -I https://forge.laravel.com/api
-
Verify no firewall blocking outbound HTTPS
Enable detailed logging for troubleshooting:
// In a service provider or bootstrap file
use Illuminate\Support\Facades\Log;
if (config('app.debug')) {
// Log all Forge API calls
Event::listen(\Saloon\Events\SendingRequest::class, function ($event) {
Log::debug('Forge API Request', [
'method' => $event->request->getMethod(),
'uri' => $event->request->getUri(),
]);
});
Event::listen(\Saloon\Events\ResponseReceived::class, function ($event) {
Log::debug('Forge API Response', [
'status' => $event->response->status(),
'body' => $event->response->body(),
]);
});
}- Forge API Documentation: https://forge.laravel.com/docs/api-reference/introduction
- Legacy API Documentation: https://forge.laravel.com/api-documentation
- Forge Status: https://status.laravel.com/
- Laravel Forge Community: https://laracasts.com/discuss/channels/forge
This package is part of our internal toolkit and is optimized for our own purposes. We do not accept issues or PRs in this repository.
However, if you find this package useful and want to build upon it for your own needs, you're welcome to fork it!
The MIT License (MIT). Please see License File for more information.
