Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Digitalocean Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

Implementation Patterns

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.

Gotchas and Tips

  • Everything you care about is async. Droplet create, rebuild, resize, snapshot, and power actions all return actions. Any code that acts on a resource immediately after 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.
  • Non-idempotent creates are the money bug. A retried 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.
  • Rate limits bite when tokens are shared. DigitalOcean enforces roughly 5,000 requests/hour per token. A polling loop at 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.
  • Pin both hops. Pin 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.
  • Config caching resolves env at build time. 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.
  • Proxy / CA / timeout behavior lives in the SDK's Guzzle client, not this package. In corporate egress environments, verify the bridge doesn't instantiate its own client that bypasses HTTPS_PROXY and your CA bundle before this touches anything scheduled.
  • Errors are SDK exceptions, not Laravel ones. The SDK throws its own exception hierarchy for 4xx/5xx; wrap calls at your interface boundary and translate into your domain's exception types. Don't let DigitalOceanV2\Exception\* escape into controllers.
  • Log action IDs and resource IDs at creation. The 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.
  • Pagination is handled but not free. 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.
  • Extension points are thin by design. There is no repository layer, no retry policy, no queue integration, and no action-polling helper — the manager, config, and facade are the whole package. Anything beyond client wiring is yours to build, so budget it explicitly rather than assuming the package will grow into it.
  • The bus factor is one, and that's survivable here. The package is a few hundred lines of glue; if it falls behind a Laravel major, vendoring it is an afternoon. That's the reason to keep the blast radius at one file — the replacement cost is your insurance policy.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin