graham-campbell/digitalocean
Graham Campbell's DigitalOcean package offers a simple, elegant Laravel integration for the DigitalOcean API, wrapping the official PHP client with a service provider, facade, and configuration so you can manage Droplets, volumes, and more right from your Laravel app.
Install and wire up.
composer require graham-campbell/digitalocean
php artisan vendor:publish --provider="GrahamCampbell\DigitalOcean\DigitalOceanServiceProvider" --tag=config
Package auto-discovery handles the service provider; the facade is aliased as DigitalOcean. Add DIGITALOCEAN_TOKEN=... to .env — the published config/digitalocean.php contains a connections array keyed by name (each with a driver of digitalocean and a token) plus a default pointer.
Look first at config/digitalocean.php (the connections block is the only real configuration surface) and the facade root at GrahamCampbell\DigitalOcean\Facades\DigitalOcean. Then read the upstream digitalocean/digitalocean SDK docs — every manager method (droplet(), image(), region(), size(), sshKey(), snapshot(), domain(), loadBalancer(), firewall()) is a thin proxy to it, and the SDK's docs are the actual API reference.
First use case — read-only inventory. It's the safest possible smoke test and needs nothing but a scoped read token:
use GrahamCampbell\DigitalOcean\Facades\DigitalOcean;
$droplets = DigitalOcean::droplet()->getAll(); // collection of your droplets
$regions = DigitalOcean::region()->getAll();
$sizes = DigitalOcean::size()->getAll();
$images = DigitalOcean::image()->getAll(['type' => 'distribution']);
If that returns data, your token, config, container binding, and autoload are all correct. Do this in php artisan tinker before writing a line of feature code.
Never let the facade reach domain code. The facade is a service locator with static call sites; if it leaks into controllers or domain services you cannot swap providers, fake calls cleanly, or contain an SDK major bump. Define a small internal interface and implement it with this package:
interface InfrastructureProvider {
public function listDroplets(): array;
public function provisionDroplet(string $name, string $size, string $region): string;
public function destroyDroplet(string $id): void;
}
One or two files hold the facade calls; everything else depends on your interface. This is an afternoon of work and it is the single highest-leverage decision with this package.
Use named connections for environment isolation. The connections array is the built-in escape hatch for "staging vs production" (and for a second DigitalOcean team). Switch per call with DigitalOcean::connection('staging')->droplet() rather than mutating the default at runtime. Set DIGITALOCEAN_TOKEN per environment so the default connection is always the local one, and never construct a connection from a request parameter. Note this is not a per-customer BYO-token model — credentials are static config, resolved at boot.
Provisioning is a two-step workflow, always. droplet()->create() returns immediately with an action ID; the droplet does not exist yet in any usable sense. The reliable pattern is:
$droplet = DigitalOcean::droplet()->create($name, $region, $size, $image, $sshKeys);
$action = DigitalOcean::action()->get($droplet->actionIds[0]);
while ($action->status !== 'completed') {
sleep(5);
$action = DigitalOcean::action()->get($action->id);
// bail out with a timeout, and handle 'errored' explicitly
}
Wrap this in a helper. Every consumer in your codebase needs it, and every consumer will otherwise reinvent it badly. Run the whole thing in a queued job, never synchronously in a request.
Make creates idempotent with tags. Provisioning is non-idempotent: a retry (network blip, queue redelivery, timeout) bills you a second droplet. Tag on create — $name, a managed-by:yourapp, and a business key like env:demo-1234 — and before creating, query droplet()->getAll(['tag_name' => 'env:demo-1234']). If one exists, reconcile against it instead of creating. This tag convention also gives you cleanup (tag()->untag(), list-by-tag, then destroy), which is what makes ephemeral environments economical.
Keep destructive operations behind Artisan commands. destroyDroplet, rebuild, resize, and volume deletes should be php artisan infra:destroy {droplet} executed by an admin or a scheduler — not a controller action reachable from a web request. The package supplies no authorization, confirmation, or dry-run guardrail. If you need them in a UI, build an explicit confirmation + audit-log layer first, and start internal-only.
Scheduled reconciliation beats event-driven trust. A nightly job that lists droplets by your tags, compares against your own DB records, and flags orphans catches drift, leaked resources, and failed destroys. You need this the moment anything provisions on a schedule.
Test against your interface, not the facade. Mock your InfrastructureProvider in feature tests. For integration tests, fake the container binding ($this->instance(...)) with hand-built SDK value objects rather than asserting against DigitalOcean::shouldReceive(...) everywhere — the latter couples every test to SDK method names you'll change on upgrade.
create() is a latent bug that fails only under load and only sometimes — the expensive kind. Poll with a timeout and treat errored as a first-class state.droplet()->create() creates a second billable droplet. Tags + pre-flight existence check, or don't retry at all. Do not put a naive retry(3) around provisioning.sleep(1) times twenty concurrent jobs will get you throttled (HTTP 429) silently. Poll at 3–5 second intervals, add backoff with jitter, and give each environment its own token so a runaway job can't starve the rest.graham-campbell/digitalocean and audit which digitalocean/digitalocean major it resolves — the ecosystem renamed the client (DigitalOceanV2 → digitalocean/digitalocean) and call signatures changed. A transitive major bump can invalidate your call sites with no change on your side. Record the resolved SDK and Guzzle majors in your runbook, and re-check on every Laravel upgrade.php artisan config:cache in deploy is fine provided DIGITALOCEAN_TOKEN is set in the deploy environment, not read at request time. A token rotated in .env without config:clear will keep serving the old value — and rotations are exactly when you can't afford that.HTTPS_PROXY and your CA bundle before this touches anything scheduled.DigitalOceanV2\Exception\* escape into controllers.X-Request-Id response header is the fastest path to DigitalOcean support, but the SDK may not surface it — log the action ID, droplet ID, and your idempotency tag together at the point of creation so a failed provision is diagnosable from application logs alone.getAll() loops pages for you; on large accounts it's multiple HTTP calls per invocation. Cache inventory reads (a few minutes is plenty) rather than calling getAll() on a dashboard page load.How can I help you explore Laravel packages today?