BudgetHostAPI DOCUMENTATION
API v1OpenAPI
DEPLOYMENT GUIDEREST / JSON

Deploy your application

Configure, upload, deploy. Keep control of every release, from the first build to an explicit rollback.

If you only have SLICE_ID and SLICE_TOKEN, start with applications and storage to create or select an application and obtain its service token. A slice token cannot be used directly at the service endpoints.

Service credentials#

The slice guide saves SERVICE_ID and SERVICE_TOKEN in a private file and sets SERVICE_ENV to its path. Continue in the same shell. For a later release, set SERVICE_ENV to that saved file's path first. If you received a service token from the application's API tokens page in my.budgethost, put those two variables in a trusted, private .service.env file.

set -euo pipefail
set +x
SERVICE_ENV=${SERVICE_ENV:-.service.env}
chmod 600 "$SERVICE_ENV"
source "$SERVICE_ENV"
BASE="https://api.budgethost.io/v1/deploy/services/$SERVICE_ID"
umask 077
# Use a fresh private state directory for this release, outside the source tree.
STATE=$(mktemp -d)
printf 'Save this deployment state directory: %s\n' "$STATE"
api() { curl --fail-with-body -sS -H "Authorization: Bearer $SERVICE_TOKEN" "$@"; }
uuid() { python3 -c 'import uuid; print(uuid.uuid4())'; }

All commands below use this shell's BASE, STATE, api and uuid definitions. Use the saved service credentials for subsequent releases; do not provision another application or storage volume each time.

Check the service#

api "$BASE" | jq .data
api "$BASE/configuration" > "$STATE/configuration.json"
jq .data "$STATE/configuration.json"

Capabilities report supported inputs, target platform, upload limits and ready. If ready is false, inspect the reason. Missing application settings can be configured below; placement/capacity problems require BudgetHost support. Configuration lists assigned resources and environment names but omits environment values and secret values. Existing Carsol deployments can normally skip configuration and upload their next release directly.

Configuration, storage and secrets#

The final Dockerfile stage's single literal EXPOSE port can be inferred. Override it with manifest.port when needed. Source uploads do not implicitly apply settings. The app must actually listen on that port on all container interfaces. Dockerfile ENV, VOLUME and shell HEALTHCHECK instructions are not imported as API settings.

If persistent storage is not already attached, create it once. Do not recreate an existing Carsol database volume:

uuid > "$STATE/storage.key"
printf '%s\n' '{"name":"data","mount_path":"/data"}' > "$STATE/storage-request.json"
api -H 'Content-Type: application/json' \
  -H "X-Idempotency-Key: $(cat "$STATE/storage.key")" \
  --data-binary @"$STATE/storage-request.json" "$BASE/storage" > "$STATE/storage.json"
api "$BASE/configuration" > "$STATE/configuration.json"

The mount is saved in desired configuration and takes effect at deployment. For an existing mount at /data, no additional storage binding is needed. For an unmounted resource, use its returned storage ID/path key in the configuration request's bindings.storage map. Never guess IDs or use another service's storage.

Create a secret from a private JSON file containing {"value":"YOUR_SECRET"}. Do not put this file inside the source tree:

# SECRET_FILE is an absolute path to your private JSON file.
api -X PUT -H 'Content-Type: application/json' --data-binary @"$SECRET_FILE" \
  "$BASE/configuration/secrets/Jwt__Key" > "$STATE/jwt-secret.json"
JWT_SECRET_ID=$(jq -er '.data.secret_id' "$STATE/jwt-secret.json")

The response contains a secret ID, never its value. Repeat for other secret names as needed. Replacing signing keys or application passwords can affect sessions; retain existing secrets when you are only deploying new code.

Example request for Carsol (assumes /data is already attached and required bootstrap credentials are already configured). Adjust readiness to an HTTP path that represents your own app's startup, and include any additional required settings for a new installation:

jq -n --rawfile dockerfile Dockerfile --arg jwt "$JWT_SECRET_ID" '{
  dockerfile: $dockerfile,
  manifest: {
    version: 1, port: 8080, readiness: {path: "/api/version"},
    storage: [{name: "data", mount_path: "/data"}],
    environment: {
      ConnectionStrings__Default: {required: true, default: "Data Source=/data/carsolutions.db"},
      Jwt__Key: {required: true, secret: true}
    }
  },
  bindings: {secret_refs: {Jwt__Key: $jwt}}
}' > "$STATE/configuration-request.json"
api -H 'Content-Type: application/json' --data-binary @"$STATE/configuration-request.json" \
  "$BASE/configuration/preview" > "$STATE/review.json"
jq .data "$STATE/review.json"

Review changes, warnings and missing. Resolve missing settings before saving. Defaults only fill absent environment variables. To change a declared non-secret variable explicitly, use bindings.environment. New installations must also declare and bind their required bootstrap username/password; a required secret uses { "required": true, "secret": true } plus its ID in bindings.secret_refs.

jq -e '.data.can_apply == true' "$STATE/review.json" >/dev/null
jq --slurpfile review "$STATE/review.json" \
  '. + {review_id:$review[0].data.review_id}' "$STATE/configuration-request.json" \
  > "$STATE/configuration-apply.json"
api -H 'Content-Type: application/json' --data-binary @"$STATE/configuration-apply.json" \
  "$BASE/configuration/apply" | jq .data

Apply saves settings but does not restart the application. Deploy afterwards. A stale review returns 409: inspect current settings and preview again. Configuration mutations are blocked during active deployments.

Prepare source or an image#

Choose one input. Read current capabilities for actual limits; defaults are 256 MiB compressed source, 2 GiB image archive, 2 GiB extracted source, 100,000 source entries and a 30-minute preparation timeout.

Source: prepare a clean build-context directory containing the root Dockerfile, .dockerignore, and required source files. Exclude credentials, .git, databases, user uploads and build output before archiving. .dockerignore does not remove files from the uploaded tarball. Links, special files, duplicate paths and traversal are rejected; use ustar rather than PAX/GNU extension records.

# Set SOURCE_DIR to your reviewed, clean build-context directory.
test -f "$SOURCE_DIR/Dockerfile"
test -f "$SOURCE_DIR/.dockerignore"
FILE="$STATE/source.tar.gz"
tar --format=ustar -czf "$FILE" -C "$SOURCE_DIR" .
TYPE=source

Image: build for the platform returned by capabilities, then save exactly one image. Use docker save, not docker export. No registry login/push is required:

PLATFORM=$(api "$BASE" | jq -er '.data.platform')
docker build --platform "$PLATFORM" -t my-app:release .
FILE="$STATE/image.tar"
docker save -o "$FILE" my-app:release
TYPE=image

Build-time private package credentials are not supplied through runtime secret settings. Contact support if the build requires a private dependency source.

Create, upload and start#

Create retry keys once for this release and retain the state directory. Do not rerun the key-generation commands when recovering an interrupted request.

uuid > "$STATE/create.key"
uuid > "$STATE/start.key"
BYTES=$(stat -c %s "$FILE")
SHA=$(sha256sum "$FILE" | cut -d ' ' -f 1)
jq -n --arg type "$TYPE" --arg sha "$SHA" --argjson bytes "$BYTES" \
  '{type:$type,bytes:$bytes,sha256:$sha}' > "$STATE/create.json"
api -H 'Content-Type: application/json' \
  -H "X-Idempotency-Key: $(cat "$STATE/create.key")" \
  --data-binary @"$STATE/create.json" "$BASE/deployments" > "$STATE/deployment.json"
ID=$(jq -er '.data.id' "$STATE/deployment.json")
printf '%s\n' "$ID" > "$STATE/deployment.id"
api -X PUT -H 'Content-Type: application/octet-stream' --data-binary @"$FILE" \
  "$BASE/deployments/$ID/artifact" > "$STATE/upload.json"
api -H 'Content-Type: application/json' \
  -H "X-Idempotency-Key: $(cat "$STATE/start.key")" --data '{}' \
  "$BASE/deployments/$ID/start" > "$STATE/start.json"

Start returns 202 Accepted. Preparation finishes before the running app is changed. A build/import failure leaves the current app in place. A failure during runtime replacement can affect availability; inspect the failure and explicitly redeploy a retained successful image when appropriate.

Poll and read logs#

while :; do
  api "$BASE/deployments/$ID" > "$STATE/status.json"
  STATUS=$(jq -er '.data.status' "$STATE/status.json")
  printf '%s\n' "$STATUS"
  case "$STATUS" in
    succeeded) break ;;
    failed) jq '.data.failure' "$STATE/status.json"; break ;;
  esac
  sleep 3
done
jq .data "$STATE/status.json"
# A script must fail when deployment failed.
jq -e '.data.status == "succeeded"' "$STATE/status.json" >/dev/null

Stages are awaiting_upload, ready, queued, building or importing, deploying, verifying, and finally succeeded or failed. To stop waiting, interrupt the polling command; this does not cancel the deployment.

Run log commands separately, including after a failed deployment:

CURSOR=0
api "$BASE/deployments/$ID/logs?cursor=$CURSOR&limit=100" > "$STATE/logs.json"
jq '.data.entries' "$STATE/logs.json"
CURSOR=$(jq -er '.data.next_cursor' "$STATE/logs.json")
# Repeat the request with the updated CURSOR for subsequent entries.
api "$BASE/deployments" | jq .data

These are deployment/build logs, not a live application stdout stream. They include bounded build output and platform stage messages. Avoid printing secrets in builds; redaction cannot recognize every possible secret.

Recover interrupted requests#

  • Lost create response: repeat the exact create POST using saved create.json and create.key; extract the returned ID again. Do not create a fresh release.
  • Interrupted upload: inspect deployment status. Before start, retry the complete PUT from byte zero with the same archive. Partial files do not become ready.
  • Lost start response: repeat POST /deployments/{id}/start with the saved start key and {}. Then poll the same ID. Do not upload again after start.
  • 401: token expired, revoked or invalid. Renew or replace it in my.budgethost. Check history before retrying mutations under a different token.
  • 409: read error.code and error.details; a deployment may be active, input may differ from the original key, or configuration may have changed. Poll/inspect rather than blindly creating another operation.
  • 413: archive exceeds limits. Reduce its size or contact support. A checksum failure requires the original matching bytes or a new creation request/key.
  • Network/5xx: preserve state and inspect/retry the same request. For recurring errors, send support the deployment ID and redacted response/request ID.

Roll back explicitly#

List deployment history and select a retained, successful deployment from this same service. Rollback creates a new deployment and uses the current configuration. Database migrations, persistent files and secret changes are not undone.

# Set PREVIOUS_SUCCESSFUL_ID after inspecting history.
ROLLBACK_STATE=$(mktemp -d)
uuid > "$ROLLBACK_STATE/create.key"
uuid > "$ROLLBACK_STATE/start.key"
jq -n --arg id "$PREVIOUS_SUCCESSFUL_ID" \
  '{type:"redeploy",deployment_id:$id}' > "$ROLLBACK_STATE/create.json"
api -H 'Content-Type: application/json' \
  -H "X-Idempotency-Key: $(cat "$ROLLBACK_STATE/create.key")" \
  --data-binary @"$ROLLBACK_STATE/create.json" "$BASE/deployments" \
  > "$ROLLBACK_STATE/deployment.json"
ID=$(jq -er '.data.id' "$ROLLBACK_STATE/deployment.json")
api -H 'Content-Type: application/json' \
  -H "X-Idempotency-Key: $(cat "$ROLLBACK_STATE/start.key")" --data '{}' \
  "$BASE/deployments/$ID/start" > "$ROLLBACK_STATE/start.json"
STATE="$ROLLBACK_STATE"

Use the same polling/log commands. No artifact upload is required. Retry with the saved rollback keys if interrupted. There is no automatic rollback. History/logs are normally retained for 30 days; rollback also requires the referenced image to remain retained. The current image and five previous successful images are protected from registry cleanup. Check retained rather than assuming any old ID is usable.