Private Registries
Public images need no credentials. To deploy an image from a private registry, supply registry_username and registry_password in app_config. The registry host is derived from image_ref, so you only provide the username and credential.
Pull from a private registry
curl -u "$USER:$PASS" -X POST https://api.foundrydb.com/app-services \
-H "Content-Type: application/json" \
-d '{
"name": "my-api",
"plan_name": "tier-2",
"zone": "se-sto1",
"app_config": {
"image_ref": "ghcr.io/acme/api:1.0.0",
"container_port": 8080,
"registry_username": "acme-bot",
"registry_password": "ghp_XXXXXXXXXXXXXXXXXX"
}
}'
The platform authenticates the pull using these credentials on the VM. For GitHub Container Registry (ghcr.io) the password is a personal access token or a fine-grained token scoped to the repository. For Docker Hub (docker.io) it is your Docker Hub password or an access token. For other registries, the username and password are whatever the registry expects for its docker login equivalent.
How pull authentication works
When the platform deploys your app, the agent running on the VM:
- Writes an OCI-format auth file (equivalent to
~/.docker/config.json) containing the registry host, username, and password. - Passes that auth file to the container runtime when pulling the image.
- Removes the auth file from the working directory after the pull completes.
The credential travels from the controller to the agent over the encrypted internal task channel. It is never logged, never returned in any API response, and not stored in plaintext on disk.
Credential handling
registry_password is write-only: the API never returns it in any response. The credential travels to the VM over the encrypted internal task channel, where the agent writes an OCI auth file before pulling. It is not logged or stored in plaintext.
On a redeploy (via PATCH), you can omit registry_password to keep the existing credential:
curl -u "$USER:$PASS" -X PATCH https://api.foundrydb.com/app-services/{id} \
-H "Content-Type: application/json" \
-d '{
"app_config": {
"image_ref": "ghcr.io/acme/api:1.1.0",
"container_port": 8080,
"registry_username": "acme-bot"
}
}'
The stored password is reused as long as registry_username is unchanged. If you change the username, you must also supply the new password.
On rollback, the same rule applies: the current stored password is reused when the rolled-back revision's registry_username matches the current one.
Rotating credentials
If you rotate the registry credential (for example, revoke and reissue a GitHub personal access token), supply the new password on the next PATCH:
curl -u "$USER:$PASS" -X PATCH https://api.foundrydb.com/app-services/{id} \
-H "Content-Type: application/json" \
-d '{
"app_config": {
"image_ref": "ghcr.io/acme/api:1.1.0",
"container_port": 8080,
"registry_username": "acme-bot",
"registry_password": "ghp_NEW_TOKEN_HERE"
}
}'
This triggers a redeploy that pulls the image with the new credential. If the old credential was already revoked, include the new password; otherwise the pull will fail at start-container-green and the previous revision continues serving.
For AWS ECR, the temporary password expires every 12 hours. A PATCH that includes a freshly obtained ECR password is required before each redeploy.
Common registries
| Registry | image_ref prefix | Notes |
|---|---|---|
| GitHub Container Registry | ghcr.io/org/image:tag | Use a personal access token with read:packages scope. |
| Docker Hub (private) | docker.io/org/image:tag | Use a Docker Hub access token (not your account password). |
| GitLab Container Registry | registry.gitlab.com/group/project:tag | Use a deploy token or personal access token with read_registry scope. |
| AWS ECR | 123456789.dkr.ecr.eu-west-1.amazonaws.com/image:tag | Use an IAM access key ID as username and the temporary ECR password as the password. Note ECR passwords expire every 12 hours; a PATCH is required to refresh. |
| Custom | Any hostname in the image_ref | The registry host is derived from the first segment of image_ref. |
Troubleshooting pull failures
A failed pull surfaces in the deploy log as a failed step at start-container-green. The message field will indicate an authentication error (401 Unauthorized) or image-not-found error (404). The most common causes:
- Wrong token scope. For
ghcr.io, the token must haveread:packages. For GitLab, the deploy token must haveread_registry. - Username mismatch. The
registry_usernamemust be the account or bot that owns the token, not the organization name. - Expired credential. AWS ECR tokens expire after 12 hours. Other registries may issue short-lived tokens as well.
- Image tag not found. The tag in
image_refdoes not exist in the registry, or the repository is private and the credential does not have access.