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 writes | An app's or component's envs, as App Platform secrets (type: SECRET) |
| Addressed by | The app's UUID, optionally plus a component name |
| Connection carries | The token alone — a DigitalOcean token belongs to one account |
| Token permission | app:read + app:update (a full-access token has both) |
| Takes effect | A new deployment, started by the push itself |
| Value visibility | Encrypted 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.
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.
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
| Symptom | Cause | Fix |
|---|---|---|
401 or 403, whole run stopped | The token lacks app:update, or was revoked | Both app:read and app:update are required |
404 on the app, whole run stopped | The app's name was used instead of its UUID | Take the UUID from cloud.digitalocean.com/apps/<id> or doctl apps list |
| A UUID is rejected when enabling | Not a UUID — usually the app name | seekrit checks the shape up front, because the API answers a name with a flat 404 |
404 mentioning the component | The component name isn't in the app spec, or is a database | Use the name as the spec writes it; databases have no envs |
| The app redeployed when nothing changed | seekrit cannot compare encrypted values, so any run with something to push, pushes | Narrow the binding's include/exclude |
| A build can't see a value | Values default to RUN_TIME | Pass --env-scope BUILD_TIME or RUN_AND_BUILD_TIME — and only for values a build genuinely needs |
| A name failed but the rest landed | The mapped name isn't [_A-Za-z][_A-Za-z0-9]* | Fix the prefix or rename that produced it |
See also
- Third-party sync — the shared model, naming and filtering, deletions
- CLI reference — every
seekrit syncflag