Guides
External object storage
Need somewhere to keep uploads, images, or files? Astrodock can hand your app a bucket on the bundled storage automatically, or you can point it at an S3-compatible service you already use — AWS S3, Cloudflare R2, DigitalOcean Spaces, and friends. As with the database, your app code is the same either way.
Internal or external?
Each app picks a storage mode in app.json:
| Mode | What Astrodock does | Reach for it when… |
|---|---|---|
internal | Auto-provisions a bucket on the bundled object store and injects all the connection details, plus a per-app key prefix. | You just need storage and don't care where the bytes live. |
external | Nothing is provisioned. You supply five credentials as secrets; the deploy is blocked until you do. | You already use S3/R2/Spaces, or want a CDN, lifecycle rules, or storage outside the box. |
none | No storage, nothing injected. | The app doesn't store files. |
Step 1 — Set the mode in app.json
{
"schemaVersion": "1",
"slug": "gallery",
"name": "Gallery",
"subdomain": "gallery",
"runtime": { "type": "node" },
"auth": { "mode": "platform" },
"database": { "mode": "none" },
"storage": { "mode": "external" }
}
Apply it:
astrodock apply
Step 2 — Set the five storage secrets
With storage external, these five ASTRODOCK_STORAGE_* values become required secrets.
The deploy is blocked until all five exist — the required-variable gate will list any that are missing:
| Secret | What it is |
|---|---|
ASTRODOCK_STORAGE_ENDPOINT | The S3 API endpoint URL. |
ASTRODOCK_STORAGE_REGION | The region (some providers want a placeholder like auto or us-east-1). |
ASTRODOCK_STORAGE_BUCKET | The bucket name. |
ASTRODOCK_STORAGE_ACCESS_KEY | The access key ID. |
ASTRODOCK_STORAGE_SECRET_KEY | The secret access key. |
Set each one with set-secret (pipe the secret ones via stdin to keep them out of your shell history):
astrodock set-secret ASTRODOCK_STORAGE_ENDPOINT 'https://s3.us-east-1.amazonaws.com'
astrodock set-secret ASTRODOCK_STORAGE_REGION 'us-east-1'
astrodock set-secret ASTRODOCK_STORAGE_BUCKET 'my-app-uploads'
printf '%s' 'AKIA…' | astrodock set-secret ASTRODOCK_STORAGE_ACCESS_KEY
printf '%s' 'wJalrXUtn…' | astrodock set-secret ASTRODOCK_STORAGE_SECRET_KEY
Prefer the dashboard?
Open the app and use the Env tab to set the same five keys. See Secrets & environment variables for details.
Provider examples
Same five keys, different values. Here's the shape for the common providers:
AWS S3
ASTRODOCK_STORAGE_ENDPOINT | https://s3.us-east-1.amazonaws.com |
ASTRODOCK_STORAGE_REGION | us-east-1 |
ASTRODOCK_STORAGE_BUCKET | my-app-uploads |
ASTRODOCK_STORAGE_ACCESS_KEY / ..._SECRET_KEY | An IAM access key pair. |
Cloudflare R2
ASTRODOCK_STORAGE_ENDPOINT | https://<account-id>.r2.cloudflarestorage.com |
ASTRODOCK_STORAGE_REGION | auto |
ASTRODOCK_STORAGE_BUCKET | Your R2 bucket name. |
ASTRODOCK_STORAGE_ACCESS_KEY / ..._SECRET_KEY | An R2 API token's key pair. |
DigitalOcean Spaces
ASTRODOCK_STORAGE_ENDPOINT | https://nyc3.digitaloceanspaces.com |
ASTRODOCK_STORAGE_REGION | nyc3 |
ASTRODOCK_STORAGE_BUCKET | Your Space name. |
ASTRODOCK_STORAGE_ACCESS_KEY / ..._SECRET_KEY | A Spaces access key pair. |
Your app uses the same S3 SDK either way
Internal or external, your app reads the same variable names and uses the same S3 client — only the values differ:
import { S3Client } from '@aws-sdk/client-s3';
const s3 = new S3Client({
endpoint: process.env.ASTRODOCK_STORAGE_ENDPOINT,
region: process.env.ASTRODOCK_STORAGE_REGION,
credentials: {
accessKeyId: process.env.ASTRODOCK_STORAGE_ACCESS_KEY,
secretAccessKey: process.env.ASTRODOCK_STORAGE_SECRET_KEY
},
forcePathStyle: true
});
A note on the prefix
When storage is internal, Astrodock also injects ASTRODOCK_STORAGE_PREFIX — a
per-app key prefix so multiple apps can safely share the bundled bucket. Prefix your object keys with it
when it's present:
const prefix = process.env.ASTRODOCK_STORAGE_PREFIX || '';
const key = prefix + 'uploads/photo.jpg';
External mode has no prefix
With external storage you own the whole bucket, so ASTRODOCK_STORAGE_PREFIX is not set. The
snippet above handles both cases: it falls back to an empty string when the prefix is absent.
Next steps
- Give the app a managed database too: External database
- How secrets and the deploy gate work: Secrets & environment variables
- All the fields in
app.json: App structure & app.json