seekrit
Docs/DigitalOcean

Sync to DigitalOcean

App Platform holds environment variables in the app spec and hands them to your components when it deploys them, so there is no earlier point at which seekrit could inject them. A binding writes one app's variables — either the app-level set every component inherits, or one component's own.

At a glance
What seekrit writesAn app's or component's envs, as App Platform secrets (type: SECRET)
Addressed byThe app's UUID, optionally plus a component name
Connection carriesThe token alone — a DigitalOcean token belongs to one account
Token permissionapp:read + app:update (a full-access token has both)
Takes effectA new deployment, started by the push itself
Value visibilityEncrypted at rest by DigitalOcean, shown as EV[1:…] in the app spec

New to sync? Read Third-party sync first — the decryption grant, name mapping, deletions, and failure handling are the same on every destination.

1. Create a personal access token

In DigitalOcean, go to API → Tokens and create a personal access token with app:read and app:update. The token belongs to one account and carries that scope itself, so a DigitalOcean connection has nothing else to configure:

printf '%s' "$DIGITALOCEAN_TOKEN" | seekrit sync connect \
  --name acme-digitalocean --provider digitalocean

2. Bind an environment

A binding writes into one App Platform app, named by the UUID in its dashboard URL (cloud.digitalocean.com/apps/<id>, or doctl apps list) — not by the app's name, which GET /v2/apps/{id} answers with a flat 404. Without --component, it writes the app-level variables that every component inherits:

seekrit sync verify acme-digitalocean --provider digitalocean \
  --do-app 4f6c71e2-1e90-4762-9fee-6cc4a0a9f2cf

seekrit sync enable --connection acme-digitalocean --provider digitalocean \
  --do-app 4f6c71e2-1e90-4762-9fee-6cc4a0a9f2cf \
  --app storefront --env production --acknowledge-decryption

Pass --component to write one component's own variables instead. A component-level variable overrides an app-level one of the same name, which is App Platform's rule, not seekrit's:

seekrit sync enable --connection acme-digitalocean --provider digitalocean \
  --do-app 4f6c71e2-1e90-4762-9fee-6cc4a0a9f2cf --component api \
  --app storefront --env api-production --acknowledge-decryption

A component is named as it appears in the app spec — services, workers, jobs, static_sites, and functions all qualify. Databases do not: a database component has no envs of its own, it supplies bindable values to the ones that do.

Run time, build time, or both

Values are written as App Platform secrets (type: SECRET), so DigitalOcean encrypts them at rest and shows them as EV[1:…] in the app spec rather than in the clear.

By default they are scoped to run time, which keeps them out of build logs and buildpacks. That is deliberately narrower than DigitalOcean's own default of RUN_AND_BUILD_TIME: a build-time variable is visible to every build command, every buildpack, and anything they print. Pass --env-scope BUILD_TIME or --env-scope RUN_AND_BUILD_TIME for a value a build genuinely needs — a private registry token, a sourcemap upload key.

caution

A push deploys the app. App Platform has no per-variable endpoint: environment variables live in the app spec, and the only way to change one is to submit a new spec, which starts a new deployment. That is true of the control panel and doctl too — it is not something seekrit adds.

The deployment reuses each component's current commit or image digest, so it redeploys the code already running and never ships a newer build. But it is a real deployment: a build, a health check, and a rollout. Bind an environment here knowing that changing a secret in it will roll the app.

How a push behaves

The whole app spec is the unit of writing. PUT /v2/apps/{id} replaces the spec entirely — every component, route, database, domain, and alert in one document. So a run reads the current spec, changes exactly one envs array inside it, and sends the rest back byte for byte.

That makes one rule absolute: the spec is opaque. seekrit parses it as an untyped document, mutates it at one path, and re-serializes — never mapping it onto a model of what an app spec contains. DigitalOcean's own public OpenAPI document is the reason: it omits envs, alerts, and features, all of which its Go client has and real apps use. Round-tripping through any hand-written shape would silently delete whatever that shape forgot, and the thing deleted would be a customer's production config.

So your components, routes, databases, domains, and alerts survive a push, and so does any variable seekrit did not write — including another team's encrypted secrets, which pass through as opaque blobs. A removal drops just that name.

note

seekrit cannot tell a redundant push from a real one here. Everything it writes goes in as type: SECRET, so App Platform hands back an opaque EV[1:…] blob on the next read. A connector holding plaintext and reading back ciphertext cannot compare the two, so a run with anything to push, pushes.

This is a property of the platform, not of this connector — DigitalOcean's own Terraform provider has the same reported problem. If it matters, keep the binding's include/exclude narrow, so unrelated changes elsewhere in the environment don't trigger a deployment.

Name rules

App Platform accepts environment variable names matching [_A-Za-z][_A-Za-z0-9]* — which is seekrit's own rule too, so only a binding's name transform can produce a name that fails it. A name that breaks the rule is reported as a failure against that name alone; the rest of the environment still pushes. Watch for it if you use a --prefix.

Troubleshooting

SymptomCauseFix
401 or 403, whole run stoppedThe token lacks app:update, or was revokedBoth app:read and app:update are required
404 on the app, whole run stoppedThe app's name was used instead of its UUIDTake the UUID from cloud.digitalocean.com/apps/<id> or doctl apps list
A UUID is rejected when enablingNot a UUID — usually the app nameseekrit checks the shape up front, because the API answers a name with a flat 404
404 mentioning the componentThe component name isn't in the app spec, or is a databaseUse the name as the spec writes it; databases have no envs
The app redeployed when nothing changedseekrit cannot compare encrypted values, so any run with something to push, pushesNarrow the binding's include/exclude
A build can't see a valueValues default to RUN_TIMEPass --env-scope BUILD_TIME or RUN_AND_BUILD_TIME — and only for values a build genuinely needs
A name failed but the rest landedThe mapped name isn't [_A-Za-z][_A-Za-z0-9]*Fix the prefix or rename that produced it

See also