# DanubeData > DanubeData is a European managed infrastructure provider offering VPS instances, managed databases (MySQL, PostgreSQL, MariaDB), managed caches (Redis, Valkey, Dragonfly), managed RabbitMQ queues, S3-compatible object storage, a private container registry, serverless containers, static site hosting, managed Nextcloud (Storage Share), and managed apps (WordPress, n8n, Ghost). All infrastructure runs on dedicated Hetzner servers in Germany with GDPR-compliant EU data residency. Website: https://danubedata.ro API Base URL: https://danubedata.ro/api/v1 S3 Endpoint: https://s3.danubedata.ro Container Registry: https://cr.danubedata.ro Rapids (serverless) URLs: https://*.danubedata.run Interactive API Docs: https://danubedata.ro/docs/api (OpenAPI 3.1) OpenAPI Spec: https://danubedata.ro/docs/api.json MCP Server (AI agents): https://danubedata.ro/mcp (Streamable HTTP, API token) Docs as markdown: append .md to any page, e.g. https://docs.danubedata.ro/concepts.md Docs as JSON: append .json to any page, e.g. https://docs.danubedata.ro/concepts.json Docs page index: https://docs.danubedata.ro/index.json CLI: npm install -g @danubedata/cli Terraform Provider: AdrianSilaghi/danubedata ~> 0.1 Support: support@danubedata.ro Sales: sales@danubedata.ro --- ## Core Concepts ### Teams (Projects) Teams are the primary organizational and billing unit. All resources are scoped to a team. - Each team gets an isolated Kubernetes namespace (`tenant-{team_id}`) - Resources, billing, and access are per-team - Team member roles: Owner, Administrator, Editor - Default limits: 10 VPS, 10 databases, 10 caches, 10 firewalls, 25 SSH keys, 10 team members ### Billing Model - Hourly billing recorded every 5 minutes (0.0833 hours per interval) - Monthly invoices generated on the 1st of each month - No long-term contracts, no setup fees - Payment via Visa, Mastercard, American Express (Stripe) - Free: inbound traffic, private network traffic, API calls, dashboard, standard support ### Volume Discounts | Monthly Spend | Discount | |---------------|----------| | < EUR 500 | Standard | | EUR 500-2,000 | 5% | | EUR 2,000-5,000 | 10% | | EUR 5,000-10,000 | 15% | | EUR 10,000+ | Contact sales | ### Data Centers | Code | Location | Country | |------|----------|---------| | `fsn1` | Falkenstein | Germany | | `nbg1` | Nuremberg | Germany | | `hel1` | Helsinki | Finland | | `ash` | Ashburn | USA | Default datacenter: `fsn1` --- ## Agent & Automation Guide This section is the contract for scripts, CI pipelines and AI agents that operate DanubeData without a human watching. Field names and value domains here change additively, never in place. **It applies to every product.** VPS, databases, cache, queues, object storage, static sites, managed apps, uptime checks, metric alerts, block volumes and Rapids containers all report state through the same `status_details` model, return the same envelope, and answer the same diagnostic questions. Learn it once and it works everywhere. Full reference: https://docs.danubedata.ro/platform-automation Every failure code: https://docs.danubedata.ro/failure-codes ### Connecting over MCP An AI agent can skip learning the REST API and use DanubeData's MCP server, which serves nearly every operation of the public API as a typed tool. It is open to every account. Full guide: https://docs.danubedata.ro/api-mcp - **Endpoint:** `https://danubedata.ro/mcp` (Streamable HTTP) - **Auth:** an API token, `Authorization: Bearer YOUR_TOKEN`. Give the agent a token of its own, with only the abilities it needs. Lock it to one project unless it needs several projects or project creation; project creation needs an account-wide token. - **Tools:** one per API operation, named after the route (`vps_list`, `vps_create`, `database_list`, `storage_buckets_create`, `operations_get`, ...) and generated from the OpenAPI spec, so a new endpoint is a tool from its first release. Each call runs the REST endpoint with the connection's token: the token's abilities, its project lock, validation and the API rate limit all apply, so an agent can do exactly what the token can do. - **Not tools:** uploading a ZIP or a static site build (use the CLI) and managing partner sub-accounts (use the REST API). - **Narrowing the list:** `?toolsets=vps,database` lists only some toolsets: `vps`, `database`, `cache`, `queue`, `storage`, `serverless`, `static_sites`, `apps`, `kubernetes`, `registry`, `snapshots`, `firewalls`, `ssh_keys`, `monitoring`, `account`. `?read_only=1` lists only tools that read, and the server refuses every write. A misspelt toolset or an unknown parameter is a 400, never every tool. Products not open to the account are never listed. - **Project management:** the `account` toolset includes `projects_list`, `projects_create`, `projects_get`, `projects_update`, `projects_invitations_create`, `projects_members_list`, `projects_members_delete` and `projects_delete`. They require the opt-in `project:read`, `project:create`, `project:write`, `project:invite`, `project:members:read`, `project:members:delete` and `project:delete` permissions. Create uses an account-wide token and always creates a new project ID, even when its name already exists. Project-specific tools take the project ID in the `team` argument. Only the owner can rename, invite, remove members or delete the project. The member list contains accepted users only, with the owner first and roles `owner`, `admin` or `editor`; the owner cannot remove themselves. Invites accept `admin` or `editor` through the existing email flow. Project deletion requires a verified email and is blocked by personal projects, remaining resources or retained data (including VPS snapshots and customer-managed secrets), and billing blockers. Exported VPS backups remain protected after their source VPS is removed; delete the snapshots explicitly and wait for their removal to finish. A successful delete responds `204` while infrastructure cleanup runs in the background. It revokes project-scoped tokens and leaves account-wide tokens active. Removing a member responds `204` and revokes only their tokens locked to that project; account-wide tokens and tokens locked to their other projects remain active. - **Project:** resource calls act on the project the token is locked to. An account-wide token acts on the current project unless the client configuration sends `X-Team-Id`. Project API calls with a `{team}` path target that project for the request; listing and creation are account-level, and creation requires an account-wide token. These calls do not change the current console project. A project-locked token can access only its own project and cannot create another, even if it has wildcard abilities. `user_teams` lists the projects, the one a resource call acts on (`current_team_id`) and the one the token is locked to (`token_team_id`, null for an account-wide token). - **IDs:** path arguments are IDs in snake_case (`ssh_key`, `vps_instance`), and most name the tool that lists them in their description. A 404 names it too. - **Lists:** most list tools answer one page at a time. `per_page` sets its size, up to the maximum in the tool's schema (200 on most lists). Walk the pages with `page` until `last_page`, or with `cursor` until `next_cursor` is null where a tool takes a cursor. To count, ask for `per_page: 1` and read `total`. - **Results:** a status line (`201 Created · POST /api/v1/ssh-keys`) followed by the API's JSON answer. A refused call is a tool error with the status, one line of advice and the answer. Answers over about 80 KB are cut. - **Background work and retries:** a write that answers `202 Accepted` finishes in the background, so poll the resource (its `*_get` or `*_status` tool): where the answer has `status_details`, the work is done when `status_details.operation.terminal` is true, whatever the status string says. `operations_get` follows only Rapids deployments, not the `operation_id` of a database or snapshot change. Tools whose route accepts `Idempotency-Key` take an `idempotency_key` argument: send the same key when retrying a create. - **Limits:** 300 requests a minute per token at the endpoint. Each tool call also counts against the project's API rate limit. Client setup. Where a client can read an environment variable, keep the token in `DANUBEDATA_TOKEN`. Claude Code: ```bash claude mcp add --transport http danubedata https://danubedata.ro/mcp \ --header "Authorization: Bearer YOUR_TOKEN" ``` Codex (`~/.codex/config.toml`): ```toml [mcp_servers.danubedata] url = "https://danubedata.ro/mcp" bearer_token_env_var = "DANUBEDATA_TOKEN" ``` Cursor (`~/.cursor/mcp.json`): ```json { "mcpServers": { "danubedata": { "url": "https://danubedata.ro/mcp", "headers": { "Authorization": "Bearer ${env:DANUBEDATA_TOKEN}" } } } } ``` VS Code, OpenCode and any other Streamable HTTP client: see the full guide. ### Running non-interactively Nothing below ever prompts. Authenticate with an environment variable rather than an interactive login: ```bash export DANUBE_TOKEN="your-api-token" export DANUBE_TEAM_ID="42" # default project for this shell danube rapids ls --json ``` | Variable | Purpose | |----------|---------| | `DANUBE_TOKEN` | API token. Set this instead of `danube login`. | | `DANUBE_TEAM_ID` | Default project (team) id | | `DANUBE_API_BASE` | Override the API base URL | | `DANUBE_JSON` | Force JSON output globally | | `DANUBE_NO_UPDATE_CHECK` | Suppress the update notifier (use in CI) | `--json` and `--project` are global flags accepted by **every** command, so they work the same way regardless of subcommand. ### Project (team) scoping Every resource belongs to a project. Say which one explicitly — omitting it uses the account default, which is rarely what automation wants. This applies to resource requests; project-management API calls use account-level routes or the project ID in their path. ```bash # API curl -H "Authorization: Bearer $DANUBE_TOKEN" \ -H "X-Team-Id: 42" \ https://danubedata.ro/api/v1/serverless # CLI, for this invocation only danube --project 42 rapids get my-api --json # CLI, persist the default without prompting danube project select --project 42 ``` `--team` is an accepted alias. Passing both with **different** values is an error rather than a silent preference, so automation cannot end up pointed at the wrong project without noticing. ### Token abilities | Resource | Read | Write | Delete | Other | |----------|------|-------|--------|-------| | VPS (and block volumes) | `vps:read` | `vps:write` | `vps:delete` | `vps:diagnostics`, `vps:credentials` | | Databases | `database:read` | `database:write` | `database:delete` | `database:diagnostics`, `database:credentials` | | Cache | `cache:read` | `cache:write` | `cache:delete` | `cache:diagnostics`, `cache:credentials` | | Queues | `queue:read` | `queue:write` | `queue:delete` | `queue:diagnostics`, `queue:credentials` | | Storage | `storage:read` | `storage:write` | `storage:delete` | `storage:diagnostics` | | Secrets (`/api/secrets`) | `secret:read` | `secret:write` | - | | | Serverless (Rapids) | `serverless:read` | `serverless:write` | `serverless:delete` | `serverless:diagnostics` | | Static Sites | `static-site:read` | `static-site:write` | `static-site:delete` | `static-site:diagnostics` | | Managed Apps | `app:read` | `app:write` | `app:delete` | `app:diagnostics`, `app:credentials` | | Container Registry | `registry:read` | `registry:write` | `registry:delete` | | | Uptime Checks | `uptime:read` | `uptime:write` | `uptime:delete` | | | Metric Alerts | `metric-alert:read` | `metric-alert:write` | `metric-alert:delete` | | | Parameter Groups | `parameter-group:read` | `parameter-group:write` | `parameter-group:delete` | | | Firewalls | `firewall:read` | `firewall:write` | `firewall:delete` | | | Snapshots | `snapshot:read` | `snapshot:write` | `snapshot:delete` | | | SSH Keys | `ssh-key:read` | `ssh-key:write` | `ssh-key:delete` | | | Kubernetes | `kubernetes:read` | `kubernetes:write` | `kubernetes:delete` | | | Webhooks | `webhook:read` | `webhook:write` | - | | | Audit | `audit:read` | - | - | `audit:export`, `audit:raw` | | Account limits | - | - | - | `account-limits:request` | | Support Tickets | `support:read` | `support:write` | - | | | Projects | `project:read`, `project:members:read` | `project:create`, `project:write` | `project:delete`, `project:members:delete` | `project:invite` | Predefined roles: Editor (read + write, no delete), Admin (full project-resource access). Project API permissions are independent and opt-in; existing roles and default token abilities do not grant them. `{product}:diagnostics` is deliberately separate from `{product}:read`: a token allowed to list your databases should not automatically be able to read what those databases print to their logs. Grant it only to tokens that need logs and events. `{product}:credentials` (VPS, databases, cache, queues, managed apps) is separate from `{product}:read` for the same reason: it returns a working password. Without it, `GET /vps/{id}/password`, `GET /database/{id}/credentials` and `GET /cache/{id}/credentials` answer 403. The routes that describe how to connect (`/cache/{id}/connection-info`, `/queue/{id}/connection-info`, and `GET` on a database, cache or queue instance) still answer to `:read`, but `password` and every connection URL (`connection_info`, `amqp_url`, which embed the password) come back `null`. None is in the default token abilities: a token that has to read passwords must be given them. The Secrets API (`/api/secrets`) has the same split: `secret:read` reads the values you stored (`?include_value=true`), and `secret:write` creates, changes and deletes them. Each time a token is handed a password, the project's Activity Log records which token read the password of which resource (one entry per token and resource per ten minutes, never the password itself). Block volumes reuse the `vps:*` abilities — a volume is not a separate product, it is part of an instance. Uptime checks, metric alerts and the registry have no `:diagnostics` ability because they expose no workload output: an uptime check's results and an alert's history are ordinary reads under `:read`. **The container registry does not use API tokens.** It has its own `cr_...` credentials — see the Container Registry section. ### Asking for higher account limits A create call that hits an account limit answers 422 with the address of the Account limits page. A token cannot raise the limit by itself: it asks, and the **project owner** decides in the console. Nothing changes until they approve. ```bash curl -X POST https://danubedata.ro/api/v1/account-limits/increase-requests \ -H "Authorization: Bearer $DANUBE_TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -d 'reason=Two more staging servers and a database for each staging environment.' \ -d 'requested_vps_limit=5' ``` The token needs `account-limits:request`, and the request must name at least one limit (or resource profile) above the project's current one: if none is, the answer is `422` ("Ask for more than your current limits.") and nothing is opened. The answer is `202` with `data.approval` (`id`, `status: "pending"`, `summary`, `expires_at`, `review_url`). The owner is emailed. Poll `GET /api/v1/approvals/{id}` no faster than `poll_after_ms`, and stop when `terminal` is `true`: `approved` (the request is now with DanubeData for review), `denied`, `expired` (nobody decided in 24 hours) or `failed` (see `failure_reason`; also when the token was deleted while the request waited). While one approval is waiting, or a limit request is already open, a new request answers `409` (`approval.already_pending` or `account_limits.request_pending`). Do not retry around it, and do not try to approve it yourself: decisions are made only in the console. ### Response envelopes Two shapes exist. Collection endpoints return: ```json { "data": [...], "pagination": { "current_page": 1, "last_page": 5, "per_page": 15, "total": 72 } } ``` Diagnostic endpoints — on every product — return an explicit envelope, because "we could not answer" and "the answer is empty" are different facts: ```json { "success": true, "data": { ... }, "error": null, "meta": { ... } } ``` On failure, `error` carries `{ code, message, retryable }`. Branch on `code`; `message` is written for humans and may be reworded at any time. ### The status model Every resource response — cache, database, queue, VPS, volume, bucket, static site, managed app, uptime check, Rapids container — carries `status_details`. Read this rather than the legacy `status` string: it is the same object the dashboard uses, so they cannot disagree with you. The example below is a Rapids container, but the shape is identical for every product. Only the `error.code` prefix changes (`cache.`, `database.`, `vps.`, …). ```json { "summary": "failed", "health": "unhealthy", "observed_at": "2026-08-03T18:59:00+00:00", "stale": false, "operation": { "state": "failed", "terminal": true }, "error": { "code": "serverless.image_pull_auth", "source": "reconciler", "resource": { "kind": "Revision", "name": "my-api-00007" }, "reason": "ContainerMissing", "message": "The registry rejected the configured pull credential.", "retryable": false, "observed_at": "2026-08-03T18:59:00+00:00" } } ``` | Field | Meaning | |-------|---------| | `summary` | `pending`, `in_progress`, `ready`, `degraded`, `failed`, `stopped`, `deleting`, `unknown` | | `health` | `healthy`, `degraded`, `unhealthy`, `unknown` — describes what is **currently serving** | | `operation.state` | `queued`, `running`, `succeeded`, `failed`, `cancelled` | | `operation.terminal` | **The stop condition for polling** | | `observed_at` | When the platform last checked the cluster — not when the row was last written | | `stale` | The platform could not reach a live source; the summary is the last known one | ### Polling correctly Poll until `operation.terminal` is `true`. Do not infer terminality from the status string, and do not treat `health: "unknown"` as a failure — during a rollout the platform genuinely does not yet know whether the new revision is healthy, and saying so is more honest than guessing. ```bash until [ "$(danube rapids get my-api --json | jq -r '.status_details.operation.terminal')" = "true" ]; do sleep 5 done ``` `summary: "degraded"` is terminal but **not** an outage: a new revision failed while an older one keeps serving traffic. Your site is up. Redeploying the same configuration will fail the same way. ### Capabilities: what to call before calling it Every resource states which diagnostic surfaces it can serve, so an agent can branch without probing and eating a 404 or 403: ```json "capabilities": { "logs": true, "events": true, "diagnose": true } ``` Derived, not hardcoded — a resource whose events are genuinely unavailable reports `events: false` rather than advertising an endpoint that will fail. Static sites additionally report `build_logs`. | Product | status_details | /logs | /events | /diagnose | /metrics | |---------|----------------|-------|---------|-----------|----------| | Cache | yes | yes | yes | yes | yes | | Databases | yes | yes | yes | yes | yes | | Queues | yes | yes | yes | yes | yes | | Managed apps | yes | yes | yes | yes | yes | | VPS | yes | no | yes | yes | yes | | VPS block volumes | yes | no | yes | yes | no | | Object storage | yes | no | no | yes | yes | | Static sites | yes | build logs | no | yes | no | | Uptime checks | yes | no | no | yes | no | | Metric alerts | yes | no | no | no | no | | Rapids containers | yes | yes | yes | yes | no | | Container registry | no | no | yes | no | no | VPS has no `/logs` by design: guest operating system output is yours, and the platform does not read inside your VM. Platform-side events are exposed instead. Metric alerts have no `/diagnose` because an alert's history IS its diagnosis — `/metric-alerts/{id}/history` returns each evaluation with the query behind it. The registry has no status model: repositories exist because something was pushed, so there is nothing to poll. `/registry/preflight` answers the question diagnose would. ### Failure codes `status_details.error.code` and `findings[].code` are the automation keys. `retryable` says whether trying again can possibly help. Codes are namespaced per product; the full catalogue is at https://docs.danubedata.ro/failure-codes. Emitted for every product (`{product}` is `cache`, `database`, `vps`, …): | Code | Meaning | Retryable | |------|---------|-----------| | `{product}.healthy` | Diagnose found nothing wrong | — | | `{product}.operation_in_progress` | Still running; keep polling | Yes | | `{product}.stale_observation` | In-flight and unconfirmed for 15 minutes | Yes | | `{product}.warnings_recovered` | Already recovered — NOT a current problem | — | | `{product}.event_{reason}` | A Kubernetes warning, reason lowercased (e.g. `cache.event_crashloopbackoff`) | Varies | | `{product}.logs_unavailable` | The log backend did not respond (HTTP 503) | Yes | | `{product}.events_unavailable` | Cluster events could not be read (HTTP 503) | Yes | | `{product}.events_not_permitted` | The platform may not read events here | No | | `{product}.unknown` | Unclassified failure | Yes | Rapids-specific codes: | Code | Meaning | Retryable | |------|---------|-----------| | `serverless.image_pull_auth` | The registry rejected the pull credential | No — fix the credential | | `serverless.image_not_found` | The image or tag does not exist | No — fix the reference | | `serverless.image_arch_mismatch` | Image architecture is not amd64 | No — rebuild for amd64 | | `serverless.invalid_image_ref` | The image reference is malformed | No | | `serverless.image_pull_unknown` | The image could not be fetched, cause unclear | Yes | | `serverless.config_error` | The container configuration was rejected | No | | `serverless.oom_killed` | The container exceeded its memory limit | No — raise the profile | | `serverless.crash_loop` | The container starts and exits repeatedly | No — fix the application | | `serverless.progress_deadline` | The rollout did not complete in time | Yes | | `serverless.revision_missing` | The expected revision is absent | Yes | | `serverless.volume_node_full` | The container has a volume, so every instance runs on the server that holds it, and it cannot take this one now | Yes — lower the scale or the profile, or try later | | `serverless.volume_not_ready` | The container's volume is missing, not bound yet, or being removed | No — look at the volume on the Storage tab; delete it and add it again only if it shows Not provisioned yet, otherwise delete nothing and contact support | | `serverless.unknown` | Unclassified | Yes | Retrying a non-retryable failure will not fix it and only consumes your build and request quota. The calls that add, change, back up and restore a container's volume answer with codes of their own, `serverless.volume_*` (34 of them, each with an HTTP status and a retryable flag): see https://docs.danubedata.ro/failure-codes#rapids-volumes-backups-and-restores. `serverless.volume_restore_in_progress` is NOT retryable (asked again after the restore finished, it would restore twice) and names the restore under way in `meta.restore_id`; `serverless.volume_restore_cleaning_up` is retryable. `/serverless/{id}/diagnose` adds findings that come from correlating the status with the live Knative Revision and Route — conclusions unreachable from either source alone: | Code | Meaning | Retryable | |------|---------|-----------| | `serverless.no_pod_scheduled` | Revision settled at `Ready=False` with zero replicas: no pod ever ran, so an empty log is EXPECTED | No — fix the image reference or credential | | `serverless.ingress_not_ready` | `IngressReady` is not `True`: the revision is healthy but the public URL does not serve | Only when `Unknown` (still converging) | | `serverless.degraded_but_serving` | A new revision failed while an older one keeps serving. THE SITE IS UP | No — do not auto-roll-back | | `serverless.replica_count_lagging` | `current_replicas` (a 5-minute cache) disagrees with the live revision count | No — both figures are correct | | `serverless.revision_data_unavailable` | The Service or Route could not be read, so some checks did not run | Yes — do not read as "nothing wrong" | These checks are skipped, emitting no code, while `operation.terminal` is `false` and for a stopped container: a healthy deploy is briefly `Ready=False` with zero replicas, and a stopped container is meant to have no pods. ### Diagnostic endpoints The same three paths exist on every product that reports the capability. Check `capabilities` first rather than probing: ```text GET /api/v1/{product}/{id}/logs # requires {product}:diagnostics GET /api/v1/{product}/{id}/events # requires {product}:diagnostics GET /api/v1/{product}/{id}/diagnose # correlates status + events + logs ``` `{product}` is one of `cache`, `database`, `queue`, `apps`, `vps`, `storage/buckets`, `static-sites`, `uptime-checks`, `serverless`. Block volumes are nested: `/vps/{id}/volumes/{volume}/diagnose`. `/diagnose` returns ranked `findings[]`, each with `code`, `severity` (`fatal`, `action_required`, `transient_recovered`, `informational`), `summary`, `remediation` and `retryable`. **Branch on `code`, never on prose.** `findings[0]` is the most severe. Sections degrade independently: if events are unreachable, `sections.events.available` is `false` and the rest still answers. Diagnostic endpoints share a budget separate from the general API limit: 60 requests per minute per token, 180 per minute per project. Rapids adds one surface the other products do not have: ```text GET /api/v1/serverless/{id}/revisions # requires serverless:read ``` CLI equivalents: `danube rapids logs|revisions|events --json`. **Logs.** Parameters: `since`, `until`, `cursor`, `limit`, `level`, `container`. `container` is one of `user-container` (your process), `queue-proxy` (the request sidecar) or `all`. `level` is one of `debug`, `info`, `warn`, `error`. - `data.available: false` (HTTP 503) means the log store did not answer. That says nothing about your container — retry it. - `data.available: true` with `entries: []` means your container printed nothing. Act on it. - **Always pass `since`.** The default window is 30 minutes, so a container that failed an hour ago legitimately returns no entries. - Page with `meta.next_cursor` (string or null). `meta.truncated` is a boolean. When `truncated` is `false` and `next_cursor` is `null` you have reached the end of the stream *within the requested window* — widen `since` to look further back. - Logs are retained 10 days. A `since` older than that is rejected rather than silently clamped. - `level` matches the text of each line, not a structured field, so a line that merely mentions "error" matches `level=error`. It is a filter, not a guarantee. **Revisions.** Returns **every** revision, not only those receiving traffic — the revision that failed is precisely the one with no traffic. Conditions are tri-state: | `status` | Meaning | |----------|---------| | `True` | Satisfied | | `False` | A settled verdict — this will not change on its own | | `Unknown` | Still in progress | Treating `Unknown` as a failure is the most common mistake here: it makes an in-flight deploy look like an outage. `desired_replicas` and `actual_replicas` are integers (or null). **`actual_replicas: 0` alongside a failed `Ready` condition means no pod was ever scheduled** — the image could not be fetched, so there is nothing to have produced logs. The response also includes Service and Route readiness. Route conditions include `IngressReady`, which is what separates "the deployment succeeded" from "the URL still returns 404". **Events.** Platform events for your container's service, revisions and pods. Events are **ephemeral** — the platform garbage-collects them, so their absence is not evidence that nothing happened. Revision conditions are the durable signal; prefer them when the two disagree. ### Worked example: build, push, deploy, verify ```bash set -euo pipefail export DANUBE_TOKEN="..." # API token with serverless:write + :diagnostics PROJECT=42 SLUG=acme # your team slug IMAGE="cr.danubedata.ro/$SLUG/api:$(git rev-parse --short HEAD)" # 1. Build and push an immutable tag (amd64 — other architectures are rejected) echo "$REGISTRY_TOKEN" | docker login cr.danubedata.ro -u you@example.com --password-stdin docker build --platform linux/amd64 -t "$IMAGE" . docker push "$IMAGE" # 2. Point the Rapid at the new image danube --project $PROJECT rapids update my-api --image "$IMAGE" --json # 3. Wait for a terminal outcome until [ "$(danube --project $PROJECT rapids get my-api --json \ | jq -r '.status_details.operation.terminal')" = "true" ]; do sleep 5 done # 4. Decide, and diagnose only if needed STATUS=$(danube --project $PROJECT rapids get my-api --json) case "$(echo "$STATUS" | jq -r '.status_details.summary')" in ready) echo "deployed: $(echo "$STATUS" | jq -r '.url')" ;; degraded) echo "new revision failed; the previous one is still serving" ;; *) echo "$STATUS" | jq '.status_details.error' danube --project $PROJECT rapids revisions my-api --json \ | jq '.data.revisions[0].conditions' danube --project $PROJECT rapids logs my-api --since 1h --json exit 1 ;; esac ``` ### Debugging a failed deploy ```bash danube rapids get my-api --json | jq '.status_details' # 1. terminal, and why? danube rapids revisions my-api --json | jq '.data.revisions[0].conditions' # 2. what was observed? danube rapids logs my-api --since 30m --json # 3. did it print anything? ``` If step 2 shows `Ready=False` with reason `ContainerMissing`, the image was never fetched, so **no pod was ever created** and step 3 will legitimately return nothing. That is not a logging problem — fix the image reference or the registry credential and redeploy. ### Mistakes that cost the most time | Mistake | What actually happens | |---------|----------------------| | Polling the `status` string for terminality | The loop never exits; use `operation.terminal` | | Treating `health: "unknown"` as failure | Every in-flight rollout looks like an outage | | Treating `degraded` as an outage | The site is up; an old revision is still serving | | Treating `Unknown` conditions as `False` | Same as above, one layer down | | Reading logs without `since` | Only the last 30 minutes are searched | | Confusing `available: false` with no logs | A backend outage is misread as a healthy silent app | | Hunting for logs when `actual_replicas` is 0 | No pod ever ran — the image is the problem | | Retrying a non-retryable error | Burns quota, changes nothing; check `error.retryable` | | Omitting project scope | Operates on the wrong project silently | | Parsing human-readable CLI output | Not a stable interface; use `--json` | | Branching on `message` or `summary` text | Prose gets reworded; `code` is the stable interface | | Probing an endpoint to see if it exists | `capabilities` already answers it; probing burns rate limit on 404s | | Assuming a product lacks diagnostics because Rapids docs mention it | Every product reports the same model — see platform-automation | | Treating `{product}.warnings_recovered` as an incident | It already recovered; paging on it trains people to ignore diagnostics | ### Endpoints and DNS Rapids containers are served from `*.danubedata.run`. Managed database, cache and queue endpoints use `*.danubedata.ro`. The container registry is `cr.danubedata.ro`, and object storage is `s3.danubedata.ro`. These are different domains; do not assume a Rapids URL follows managed-service DNS rules. --- ## VPS Instances KubeVirt-based virtual machines with full root access, SSH key authentication, firewall rules, and NVMe storage. ### Operating Systems Ubuntu 22.04/24.04/26.04, Debian 12/13, AlmaLinux 9/10, Rocky Linux 9/10, Fedora 43/44, Alpine Linux 3.22/3.23/3.24 ### Shared CPU Plans (3:1 overcommit) | Profile | vCPUs | RAM | Storage | Price/month | |---------|-------|-----|---------|-------------| | Nano | 2 | 2 GB | 20 GB | EUR 4.49 | | Micro | 3 | 4 GB | 40 GB | EUR 7.49 | | Small | 4 | 8 GB | 80 GB | EUR 12.49 | | Medium | 8 | 16 GB | 160 GB | EUR 24.99 | | Large | 16 | 32 GB | 320 GB | EUR 49.99 | ### Dedicated CPU Plans (guaranteed resources) | Profile | vCPUs | RAM | Storage | Price/month | |---------|-------|-----|---------|-------------| | Nano | 1 | 2 GB | 40 GB | EUR 8.99 | | Micro | 2 | 4 GB | 80 GB | EUR 14.99 | | Small | 2 | 8 GB | 160 GB | EUR 24.99 | | Medium | 4 | 16 GB | 320 GB | EUR 49.99 | | Large | 8 | 32 GB | 640 GB | EUR 99.99 | ### Features - NVMe SSD storage (3,000+ MB/s read, 100,000+ IOPS) - IPv4 and IPv6 support - SSH key authentication with full root access - Per-instance firewall rules - Snapshots (point-in-time backups, EUR 0.073/GB/month) - Automated daily snapshots (optional, 3-day retention) - VNC web console access - Real-time CPU, memory, disk, and network metrics - 20 TB egress included per VPS/month, EUR 1.00/TB overage --- ## Managed Databases Production-ready databases with automatic backups, read replicas, parameter groups, and monitoring. ### MySQL New databases run MySQL 8.4 (LTS) as a MySQL InnoDB Cluster: optional high availability (a three-server Group Replication group with automatic failover) and independent asynchronous read replicas. Every table needs a primary key and the InnoDB engine, and a single transaction is capped at 2.5% of the plan's memory (16-150 MB). Details: https://docs.danubedata.ro/databases-mysql-cluster Databases created earlier: versions 8.0, 8.4, 9.0, 9.1. Port: 3306. ACID-compliant relational database for web applications and OLTP workloads. ### PostgreSQL Versions: 15, 16, 17, 18 (default: 18). Port: 5432. Advanced relational database with JSON support, full-text search, and complex query capabilities. ### MariaDB Versions: 10.11, 11.4, 11.6. Port: 3306. MySQL-compatible with enterprise features. LTS on 10.11 and 11.4. The ports above are the engine ports, used on the private network. With public access enabled, the public hostname is reached through a shared load balancer on a per-instance port (`dns_port`) instead. Use the API's `connection_host`/`connection_port`, or the port shown in the dashboard. ### Resource Profiles | Profile | vCPU | RAM | Storage | Price/month | |---------|------|-----|---------|-------------| | Small | 2 | 4 GB | 50 GB | EUR 19.99 | | Medium | 4 | 8 GB | 100 GB | EUR 39.99 | | Large | 8 | 16 GB | 200 GB | EUR 79.99 | ### Features - Read replicas with automatic sync - Automated daily backups (7-day retention, free) - Manual snapshots (EUR 0.073/GB/month) - Custom parameter groups for tuning - SSL/TLS encrypted connections - Reader endpoint for load-balanced replica access - Real-time performance metrics (queries, connections, cache hit rate, replication lag) - Connection via hostname: `{name}.tenant-{team}.svc.cluster.local:{port}` --- ## Managed Cache High-performance in-memory data stores with replication and persistence. ### Redis Versions: 7.2, 7.4, 8.0, 8.4 (default: 8.4). Industry-standard in-memory data store with rich data structures, pub/sub, and replication. ### Valkey Versions: 7.2, 8.0, 8.1, 9.0, 9.1 (default: 9.1). Open-source Redis-compatible fork with no licensing restrictions. ### Dragonfly Versions: 1.23, 1.24. Multi-threaded Redis alternative with higher throughput on multi-core servers. 40-50% price premium. ### Resource Profiles | Profile | Memory | Redis/Valkey Price | Dragonfly Price | |---------|--------|--------------------|-----------------| | Micro | 256 MB | EUR 4.99/mo | EUR 6.99/mo | | Small | 1 GB | EUR 9.99/mo | EUR 14.99/mo | | Medium | 3 GB | EUR 19.99/mo | EUR 29.99/mo | | Large | 6 GB | EUR 39.99/mo | EUR 59.99/mo | ### Features - Read replicas for scaling - Persistence modes: None, RDB, AOF (everysec/always/no), Hybrid (RDB + AOF, recommended) - Snapshots (EUR 0.073/GB/month) - Configurable eviction policies - Parameter groups for tuning - Real-time metrics (memory, hit/miss ratio, ops/sec, connections, evictions) - Connection via hostname: `{name}.tenant-{team}.svc.cluster.local:6379` - With public access on (the cache's Access tab), the public hostname is reached through a shared load balancer on a per-instance port (`dns_port`), NOT 6379. Use the API's `connection_host`/`connection_port`, or the port shown in the dashboard. --- ## S3-Compatible Object Storage Fully managed, S3-compatible storage on replicated Ceph infrastructure. ### Pricing - Base: EUR 5.49/month (includes 1 TB storage + 1 TB egress) - Storage overage: EUR 5.49/TB/month - Egress overage: EUR 1.00/TB - Ingress: always free - Minimum billable object size: 64 KB ### Endpoints - Path-style: `https://s3.danubedata.ro/bucket-name/object-key` - Virtual-host: `https://bucket-name.s3.danubedata.ro/object-key` - Region: `fsn1` ### Features - 100% AWS S3 API compatible (S3 2006-03-01) - Up to 10 buckets per team - Objects up to 5 TB each - AES-256 server-side encryption (on by default for new buckets; SSE-KMS and SSE-C available), TLS 1.2 or 1.3 in transit - Object versioning - Lifecycle rules (automatic expiration/cleanup) - CORS support - Presigned URLs for temporary access - Multipart uploads - Object tagging - Built-in object browser in dashboard ### S3 API Operations Bucket: CreateBucket, DeleteBucket, ListBuckets, GetBucketVersioning, PutBucketVersioning, GetBucketPolicy, PutBucketPolicy, GetBucketCors, PutBucketCors, GetBucketLifecycle, PutBucketLifecycle Object: PutObject, GetObject, DeleteObject, ListObjects, ListObjectsV2, CopyObject, HeadObject, GetObjectTagging, PutObjectTagging Multipart: CreateMultipartUpload, UploadPart, CompleteMultipartUpload, AbortMultipartUpload, ListMultipartUploads Object keys: do not use control characters (ASCII 1-31 except tab, line feed and carriage return, such as `\x01`) in key names. The object is stored, but the XML of listings (a page that ends on such a key when more keys follow) and of multipart uploads is invalid. Clients with a strict XML parser fail (tested: the AWS CLI, boto3, the AWS SDK for PHP, mc; the AWS SDK for JavaScript v3 read the listing and CreateMultipartUpload responses we tried): the AWS CLI and boto3 report `500 ... reached max retries` although the request succeeded. DeleteObjects (multi-delete) answers 400 and deletes nothing, in every client we tried. Use a printable separator such as `_`, `-`, `~` or `/`. Details: https://docs.danubedata.ro/object-storage-supported-actions#control-characters-in-object-keys ### Code Examples AWS CLI: ```bash aws configure set aws_access_key_id YOUR_ACCESS_KEY aws configure set aws_secret_access_key YOUR_SECRET_KEY aws --endpoint-url https://s3.danubedata.ro s3 ls aws --endpoint-url https://s3.danubedata.ro s3 cp file.txt s3://my-bucket/ ``` Python (boto3): ```python import boto3 s3 = boto3.client('s3', endpoint_url='https://s3.danubedata.ro', aws_access_key_id='YOUR_ACCESS_KEY', aws_secret_access_key='YOUR_SECRET_KEY') s3.upload_file('local.txt', 'my-bucket', 'remote.txt') ``` Node.js (AWS SDK v3): ```javascript import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({ endpoint: 'https://s3.danubedata.ro', region: 'fsn1', credentials: { accessKeyId: 'KEY', secretAccessKey: 'SECRET' }, forcePathStyle: true, }); await s3.send(new PutObjectCommand({ Bucket: 'my-bucket', Key: 'file.txt', Body: 'Hello' })); ``` PHP (Laravel): ```php // config/filesystems.php 's3' disk 'endpoint' => 'https://s3.danubedata.ro', 'use_path_style_endpoint' => true, // Usage Storage::disk('s3')->put('file.txt', 'Hello'); ``` --- ## Container Registry (cr.danubedata.ro) A managed OCI registry your CI/CD pushes to and your Rapids pull from, with no third-party credentials for your own team's images. Storage is backed by the object-storage tier; pulls from inside the platform stay on the cluster LAN and are free. ### Image naming ``` cr.danubedata.ro/{team-slug}/{repository}:{tag} ``` The first path segment **must** be your team slug — the same `tenant_name` shown in Team Settings > General. Pushing anywhere else returns `denied: requested access to the resource is denied`. ``` cr.danubedata.ro/acme/web:1.0.0 cr.danubedata.ro/acme/worker:abc1234 cr.danubedata.ro/acme/staging/web:abc1234 ``` Path segments must match `[a-z0-9]+(?:[._-][a-z0-9]+)*`. Uppercase is not allowed in the path; it is allowed in tags. ### Authentication Token-only. Generate an access key under Container Registry > Access Keys, choose **Push + Pull** or **Pull only**, and use the token as the Docker *password*. The username field can be any string — the key is identified by hashing the token — so your account email is the conventional choice. ```bash echo "$REGISTRY_TOKEN" | docker login cr.danubedata.ro \ -u you@example.com --password-stdin ``` - Tokens are prefixed `cr_` so credential scanners (`trufflehog`, `git-secrets`) and humans spot them in `.env` files, CI logs and `~/.docker/config.json`. - Shown once at creation, stored hashed. Revocation takes effect within 5 minutes (the underlying JWTs are short-lived). - These are **not** API tokens. `DANUBE_TOKEN` will not authenticate a `docker push`, and a `cr_...` token will not authenticate an API call. Verify a token without pushing: ```bash curl -i -u "you@example.com:$REGISTRY_TOKEN" \ "https://danubedata.ro/oci/token?service=cr.danubedata.ro&scope=repository:acme/hello:pull" ``` `200` with a JSON `{"token": "..."}` body means the credential is valid; `401` means it is wrong, revoked or expired. ### Using an image in a Rapid Set deployment type to **Docker image**, the image to `cr.danubedata.ro/{slug}/{repo}:{tag}`, and pick **DanubeData Container Registry** as the registry credential — it is pre-seeded on every team. The platform manages a short-lived pull-only credential for first-party images, so there is nothing to create or rotate. First-party pulls only work when the team that owns the Rapid matches the team slug in the image path. ### Tag conventions - Use **immutable, sortable** tags (commit SHA or semver) in production. Kubernetes will not re-pull `:latest` once a node has cached it, so a moving tag produces deploys that appear to succeed while serving the old code. - Tags are not overwrite-protected today: pushing the same tag twice replaces the manifest. - Images must be **amd64**. Other architectures fail with `serverless.image_arch_mismatch`. On Apple Silicon build with `docker build --platform linux/amd64`. ### CI/CD ```yaml # GitHub Actions - uses: docker/login-action@v3 with: registry: cr.danubedata.ro username: ${{ secrets.DD_REGISTRY_USERNAME }} # your email — not secret password: ${{ secrets.DD_REGISTRY_TOKEN }} # the cr_... token - uses: docker/build-push-action@v6 with: context: . push: true platforms: linux/amd64 tags: cr.danubedata.ro/acme/hello:${{ github.sha }} ``` ```bash # GitLab CI / Bitbucket Pipelines echo "$DD_REGISTRY_TOKEN" | docker login cr.danubedata.ro -u "$DD_REGISTRY_USERNAME" --password-stdin docker build --platform linux/amd64 -t cr.danubedata.ro/acme/hello:$CI_COMMIT_SHORT_SHA . docker push cr.danubedata.ro/acme/hello:$CI_COMMIT_SHORT_SHA ``` Only the token needs to be a secret; the username can live in plain variables. ### Plans & limits | Plan | Repositories | Storage | Price | |------|--------------|---------|-------| | Starter | 1 | 500 MB | Free (on every team) | | Basic | 5 | 5 GB | EUR 3.99/mo | | Professional | unlimited | 100 GB | EUR 14.99/mo | | Enterprise | custom | custom | Contact sales | Upgrades take effect immediately. Downgrading while over the lower plan's limits succeeds, but new pushes are blocked until you delete enough to fit. ### Deleting images Delete a single tag, a multi-selection of tags, or a whole repository from the Container Registry UI. Blob storage is reclaimed at the nightly GC, but quota frees immediately, so a blocked push succeeds as soon as you delete. Tag and repository deletions are not recoverable. ### Registry error reference | Error | Cause | |-------|-------| | `unauthorized: authentication required` | Token wrong, revoked or expired | | `denied: requested access to the resource is denied` | First path segment is not your team slug | | `denied: Storage quota exceeded (...)` | At the plan storage cap — upgrade or delete tags | | `denied: Repository limit reached (...)` | Pushing a **new** repo at the plan repo cap; extra tags on existing repos are unaffected | | `ImagePullBackOff` on a Rapid | Tag does not exist, image belongs to another team, or the seeded credential is stale (redeploy re-materialises it) | For third-party registries (GHCR, Docker Hub, GitLab, ECR), see https://docs.danubedata.ro/serverless-private-registry --- ## Serverless Containers Knative-based containerized applications with automatic scaling, scale-to-zero, and pay-per-use pricing. ### Deployment Methods 1. **Docker image** — Pull from Docker Hub, GHCR, GCR, GitLab, Quay, ECR, MCR 2. **Git repository** — Auto-build from GitHub/GitLab/Bitbucket on push using Buildpacks or Dockerfile 3. **ZIP upload** — Upload source code via API (max 500 MB) ### Buildpack Runtimes Auto-detected from project files: Node.js (package.json), Python (requirements.txt/pyproject.toml), Go (go.mod), Java (pom.xml/build.gradle), Ruby (Gemfile), PHP (composer.json), .NET (*.csproj), Rust (Cargo.toml) ### Resource Profiles | Profile | vCPU | Memory | Price/month | |---------|------|--------|-------------| | Micro | 0.25-0.5 | 128-256 MB | EUR 5.00 | | Small | 0.5-1 | 256-512 MB | EUR 10.00 | | Medium | 1-2 | 512 MB-1 GB | EUR 20.00 | | Large | 2-4 | 1-2 GB | EUR 40.00 | ### Pay-Per-Use Pricing | Metric | Price | |--------|-------| | vCPU-second | EUR 0.000012 | | Memory (GiB)-second | EUR 0.000002 | | Requests | EUR 0.12 per million | | Egress | EUR 0.09 per GB | | Minimum billing increment | 100 ms | ### Free Tier - 2,000,000 requests/month - 250,000 vCPU-seconds (~69 vCPU-hours) - 500,000 GiB-seconds (~139 GiB-hours) - 5 GB egress/month ### Scaling - Auto-scaling based on RPS (default: 100/pod) or concurrency (default: 100/pod) - Min replicas: 0-100 (default: 0 for scale-to-zero) - Max replicas: 1-100 (default: 10) - Idle timeout: 5 minutes (fixed) - Scheduled scaling: time-based rules for predictable traffic ### Persistent Volumes (optional) https://docs.danubedata.ro/serverless-volumes - One volume per container, 1-50 GB in whole GB, mounted at a path you choose into every instance and every run. It keeps its files across deployments, restarts and scale to zero. - Price: EUR 0.12 per GB a month on the full size, from provisioning until removal starts, also while the container is stopped or scaled to zero. No free allowance. 100 GB of volumes per project in total unless raised. - One copy of the files on one server (every instance runs on that server). Not encrypted at rest. Backups are kept apart from the volume in the same data centre campus; they are not off-site. - A larger size is billed at once, but the file system grows only when the volume is next mounted: stop and start the container to use the space now. A volume cannot be made smaller. - The first request after scale to zero takes about 10 to 15 seconds longer (the volume is attached first). Set the request timeout to 30 seconds or more, or `min_scale` to 1. - Backups: up to 10 per container, one at a time, 10 minutes apart, 24 in any 24 hours (deleted ones count). Removing one takes the backup store about 5 minutes plus half a minute per GB it held (an hour at most), and no backup can be taken or restored meanwhile. Billed EUR 0.073 per GB a month on the space they take in the backup store, which can exceed the files on the volume. Kept when the volume is deleted, deleted with the container. Taken while the container runs (a crash-consistent snapshot). - Restore replaces the files on the volume with the backup's: writes since the backup are lost. The copy takes about 20-25 seconds per GB of data plus up to 2 minutes; the restore is finished a few minutes after that, when the container serves the restored data. One at a time, 12 in any 24 hours. The second copy is not billed; a restore into a container without a volume makes one, billed from when it is ready. - API: `/api/v1/serverless/{id}/volumes`, `/volume-backups`, `/volume-restores` with `serverless:read`, `serverless:write`, `serverless:delete`. Writes answer 202 with `meta.poll_url`; poll until `terminal` for backups and restores. Send an `Idempotency-Key`; `serverless.volume_restore_in_progress` is NOT retryable and names the restore under way. Error codes `serverless.volume_*` are in Failure Codes. Docs: https://docs.danubedata.ro/api-serverless-volumes ### Features - Custom domains (up to 10 per container) with automatic TLS - Environment variables (up to 100 per container, stored encrypted) - Git webhook auto-deploy - Traffic splitting between revisions (canary deployments) - Instant rollbacks via revision activation - Real-time metrics (requests, latency, error rate, replicas) - Internal networking to team databases/caches via cluster DNS - Build timeout: 30 minutes, max 50 builds/day per team ### Container URL ``` https://{container-slug}-{team-name}.danubedata.run ``` ### Limits - 50 containers per team, 10 per user - Max request/response body: 10 MB - Request timeout: 300 seconds by default, up to 3600 seconds (1 hour) per container. It limits the whole request: a response still streaming when it is reached is cut off - Ephemeral storage: 1 GB per container, empty at every start. Optional persistent volume: 1 per container, 1-50 GB - 50 revisions kept, 90-day retention ### Security Containers run with: non-root (UID 1000), read-only root filesystem, no privilege escalation, all capabilities dropped, seccomp RuntimeDefault, blocked cloud metadata and Kubernetes API access. --- ## Static Sites Deploy static websites and SPAs with automatic TLS, custom domains, and nginx caching. ### Deployment Methods 1. **ZIP upload** — Upload pre-built files via dashboard (max 100 MB on Free, 500 MB on Starter and Pro) 2. **Git repository** — Auto-deploy from GitHub/GitLab/Bitbucket on push 3. **CLI push** — Deploy via API with tarball ### Plans | Plan | Storage | Bandwidth | Sites | Custom Domains | Price/month | |------|---------|-----------|-------|----------------|-------------| | Free | 100 MB | 10 GB | 2 | 2 | EUR 0.00 | | Starter | 500 MB | 100 GB | 10 | 10 per site | EUR 2.99 | | Pro | 2 GB | 500 GB | 50 | 50 per site | EUR 9.99 | ### Features - Automatic builds via Kaniko (containerized nginx images) - Custom domains with DNS TXT verification and Let's Encrypt TLS - Password protection (HTTP Basic Auth) - Instant rollbacks to any previous deployment - Real-time build logs via WebSocket - Git webhook auto-deploy (GitHub, GitLab, Bitbucket) - HMAC-SHA256 webhook signature verification ### Caching | Content Type | Cache Policy | |-------------|-------------| | CSS and JavaScript | `public, max-age=31536000, immutable` (1 year) | | Images | `public, max-age=604800` (7 days) | | HTML | `no-cache` (always revalidated) | ### Site URL ``` https://{slug}.pages.danubedata.ro ``` ### Build Pipeline Upload/Git push/CLI push → Build queued → Kaniko builds nginx container image → Push to registry → GitOps deploy via ArgoCD → Site active ### Build Timeout 10 minutes. Web server: nginx 1.27 (Alpine). ### Supported Site Types React, Vue, Next.js (static export), Nuxt (static), Hugo, Jekyll, Astro, Eleventy, plain HTML/CSS/JS, or any static output. ### Git Private Repository Auth - SSH Key: paste deploy key - Access Token: GitHub fine-grained token (Contents: Read), GitLab project token (read_repository), Bitbucket app password (Repositories: Read) ### Custom Domain Setup 1. Add domain in dashboard 2. Add DNS TXT record: `_danubedata-verify.your-domain.com` with verification token 3. Click Verify 4. Add CNAME record pointing to `{slug}.pages.danubedata.ro` 5. TLS certificate provisioned automatically via Let's Encrypt For root/apex domains: use Cloudflare CNAME flattening, Route53 ALIAS, or provider-specific ALIAS/ANAME records. ### CLI Deployment ```bash tar -czf site.tar.gz -C ./dist . curl -X POST \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -F "file=@site.tar.gz" \ https://danubedata.ro/api/v1/static-sites/{site-id}/deploy ``` ### Building with AI You can use LLMs (Claude, ChatGPT, Gemini) to generate complete static websites. Ask for a single HTML file with inline CSS for the simplest deployment — one file, no build step. Example prompt: ``` Create a single-page portfolio website as a single HTML file with inline CSS. Include: name, title, bio, 3 project cards, GitHub/LinkedIn links. Dark color scheme, responsive, mobile-friendly. ``` Save the output as `index.html`, ZIP it, and upload to DanubeData. --- ## Storage Share (Managed Nextcloud) Fully managed Nextcloud 32 instances with S3-backed storage, WebDAV, and mobile app support. ### Resource Profiles | Profile | Name | Storage | Subdomains | Connections | Price/month | |---------|------|---------|------------|-------------|-------------| | DD1 | Sulina | 1 TB | 3 | 50 | EUR 4.99 | | DD5 | Sf. Gheorghe | 5 TB | 7 | 100 | EUR 14.99 | | DD10 | Chilia | 10 TB | 10 | 200 | EUR 29.99 | ### Features - S3-backed object storage - Custom subdomains at `{name}.storage.danubedata.ro` - Mobile app QR code setup (iOS and Android) - WebDAV endpoint for desktop sync - Full Nextcloud admin access - Automatic TLS certificates ### Instance URL ``` https://{subdomain}.storage.danubedata.ro ``` ### WebDAV Endpoint ``` https://{subdomain}.storage.danubedata.ro/remote.php/dav/files/{username}/ ``` ### Limits - Max 2 instances per team (default) - No limit on Nextcloud user accounts - Max connections determined by plan --- ## Managed Apps Fully managed WordPress, n8n, and Ghost with dedicated resources, custom domains, and monitoring. ### WordPress 6.9 CMS with MariaDB 11.8 and Redis 8 cache. | Profile | CPU | Memory | Storage | Price/month | |---------|-----|--------|---------|-------------| | Starter | 0.5 vCPU | 512 MB | 10 GB | EUR 6.99 | | Professional | 1 vCPU | 1 GB | 20 GB | EUR 14.99 | | Business | 2 vCPU | 2 GB | 40 GB | EUR 29.99 | | Enterprise | 4 vCPU | 4 GB | 80 GB | EUR 59.99 | Admin: `https://{subdomain}.apps.danubedata.ro/wp-admin` ### n8n 2.8 Workflow automation with PostgreSQL 17 and Redis 8. | Profile | CPU | Memory | Storage | Parallel Workflows | Price/month | |---------|-----|--------|---------|-------------------|-------------| | Starter | 0.5 vCPU | 512 MB | 5 GB | 5 | EUR 9.99 | | Professional | 1 vCPU | 1 GB | 10 GB | 10 | EUR 19.99 | | Business | 2 vCPU | 2 GB | 20 GB | 15 | EUR 39.99 | | Enterprise | 4 vCPU | 4 GB | 40 GB | 20 | EUR 79.99 | Admin: `https://{subdomain}.apps.danubedata.ro` (the owner account is created on the first visit) ### Ghost 6 Publishing platform with MySQL 8.4. | Profile | CPU | Memory | Storage | Price/month | |---------|-----|--------|---------|-------------| | Starter | 0.5 vCPU | 512 MB | 10 GB | EUR 7.99 | | Professional | 1 vCPU | 1 GB | 20 GB | EUR 14.99 | | Business | 2 vCPU | 2 GB | 40 GB | EUR 29.99 | | Enterprise | 4 vCPU | 4 GB | 80 GB | EUR 59.99 | Admin: `https://{subdomain}.apps.danubedata.ro/ghost` ### Features - Custom domains (up to 10 per instance) with DNS verification and automatic TLS - Snapshots for backup and restore - Plan upgrades without data loss (downgrade not supported) - Real-time CPU, memory, and network metrics - Full admin access, plugin/extension installation - No generated credentials: each app creates its admin account on the first visit (WordPress installer, n8n owner sign-up, Ghost setup at `/ghost`), and the first visitor to an unconfigured app sets it up, so create it as soon as the instance is running ### Instance URL ``` https://{subdomain}.apps.danubedata.ro ``` --- ## API Reference ### Base URL ``` https://danubedata.ro/api/v1 ``` ### Authentication All requests require a bearer token: ``` Authorization: Bearer YOUR_TOKEN_HERE ``` Create tokens at Security > API tokens in the console. Tokens shown only once at creation. ### Token Permissions | Resource | Read | Write | Delete | Other | |----------|------|-------|--------|-------| | VPS | `vps:read` | `vps:write` | `vps:delete` | | | Databases | `database:read` | `database:write` | `database:delete` | | | Cache | `cache:read` | `cache:write` | `cache:delete` | | | Storage | `storage:read` | `storage:write` | `storage:delete` | | | Serverless | `serverless:read` | `serverless:write` | `serverless:delete` | `serverless:diagnostics` | | Static Sites | `static-site:read` | `static-site:write` | `static-site:delete` | | | Firewalls | `firewall:read` | `firewall:write` | `firewall:delete` | | | Snapshots | `snapshot:read` | `snapshot:write` | `snapshot:delete` | | | SSH Keys | `ssh-key:read` | `ssh-key:write` | `ssh-key:delete` | | | Webhooks | `webhook:read` | `webhook:write` | - | | | Audit | `audit:read` | - | - | `audit:export`, `audit:raw` | | Account limits | - | - | - | `account-limits:request` | | Support Tickets | `support:read` | `support:write` | - | | | Projects | `project:read`, `project:members:read` | `project:create`, `project:write` | `project:delete`, `project:members:delete` | `project:invite` | Predefined roles: Editor (read + write), Admin (full project-resource access including delete). Project API permissions are independent and opt-in; team roles do not grant them. `serverless:diagnostics` gates logs and platform events separately from `serverless:read` — see the Agent & Automation Guide. The container registry uses its own `cr_...` credentials, not these tokens. ### Project Management The project permissions above are independent, opt-in scopes; existing token defaults and team roles do not grant them. The REST endpoints and required JSON fields are: | Method and path | Permission | Request | |---|---|---| | `GET /api/v1/projects` | `project:read` | List projects available to the token. | | `POST /api/v1/projects` | `project:create` | `{"name":"Production"}`; account-wide token only. | | `GET /api/v1/projects/{team}` | `project:read` | Read that project. | | `PATCH /api/v1/projects/{team}` | `project:write` | `{"name":"Production EU"}`; owner only. | | `POST /api/v1/projects/{team}/invitations` | `project:invite` | `{"email":"person@example.com","role":"editor"}`; owner only; role is `admin` or `editor`. | | `GET /api/v1/projects/{team}/members` | `project:members:read` | List accepted members as `{id, name, email, role}`; owner first; roles are `owner`, `admin` or `editor`. | | `DELETE /api/v1/projects/{team}/members/{user}` | `project:members:delete` | Owner only; returns `204`; revokes only the removed member's tokens locked to this project. | | `DELETE /api/v1/projects/{team}` | `project:delete` | Owner only; returns `204` while infrastructure cleanup runs in the background. | Create, rename, invite, remove-member and delete operations require a verified email address and share a limit of 10 requests per minute per user across their tokens and projects. Project deletion is unavailable for personal projects. It also waits on remaining resources or retained data, including VPS snapshots and customer-managed secrets. Exported VPS backups remain protected after their source VPS is removed; delete the snapshots explicitly and wait for their removal to finish. Billing blockers include linked organizations, charges not covered by credits, or current-period usage that has not yet been invoiced. Project path operations target the `{team}` in the path for that request; they do not change the current console project. A token locked to one project can access only that project and cannot create another, even with wildcard abilities. Every project create call creates a new project ID, even when its name already exists; creation is not get-or-create and has no idempotency key. Invitations use the existing email acceptance flow. The API does not expose project role changes. ### Response Format ```json { "data": [...], "pagination": { "current_page": 1, "last_page": 5, "per_page": 15, "total": 72 } } ``` ### Error Codes - 200 OK, 201 Created, 204 No Content - 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found - 422 Validation Error, 429 Rate Limited, 500 Server Error ### Rate Limiting Response headers: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` ### OpenAPI Spec Download: `https://danubedata.ro/docs/api.json` Interactive docs: `https://danubedata.ro/docs/api` ### SDK Generation ```bash curl https://danubedata.ro/docs/api.json > api-spec.json openapi-generator-cli generate -i api-spec.json -g python -o danubedata-python-client openapi-generator-cli generate -i api-spec.json -g typescript-axios -o danubedata-ts-client ``` ### MCP Server An AI agent can call this API as MCP tools at `https://danubedata.ro/mcp` instead of over HTTP. See "Connecting over MCP" in the Agent & Automation Guide above, and https://docs.danubedata.ro/api-mcp. ### Key API Endpoints VPS: - GET /api/v1/vps — List instances - POST /api/v1/vps — Create instance - GET /api/v1/vps/{id} — Get instance - DELETE /api/v1/vps/{id} — Delete instance Databases: - GET /api/v1/databases — List instances - POST /api/v1/databases — Create instance - GET /api/v1/databases/{id} — Get instance - DELETE /api/v1/databases/{id} — Delete instance Cache: - GET /api/v1/caches — List instances - POST /api/v1/caches — Create instance - GET /api/v1/caches/{id} — Get instance - DELETE /api/v1/caches/{id} — Delete instance Static Sites: - GET /api/v1/static-sites — List sites - POST /api/v1/static-sites — Create site - GET /api/v1/static-sites/{id} — Get site - DELETE /api/v1/static-sites/{id} — Delete site - POST /api/v1/static-sites/{id}/deploy — Deploy tarball - GET /api/v1/static-sites/{id}/builds/latest — Latest build - GET /api/v1/static-sites/{id}/deployments — Deployment history - POST /api/v1/static-sites/{id}/deployments/{id}/activate — Rollback - GET /api/v1/static-sites/{id}/domains — List domains - POST /api/v1/static-sites/{id}/domains — Add domain - POST /api/v1/static-sites/{id}/domains/{id}/verify — Verify domain Serverless (Rapids): - GET /api/v1/serverless — List containers - POST /api/v1/serverless — Create container - GET /api/v1/serverless/{id} — Get container (read `status_details`, not `status`) - DELETE /api/v1/serverless/{id} — Delete container - POST /api/v1/serverless/{id}/deploy — Deploy source - GET /api/v1/serverless/{id}/logs — Container logs (needs `serverless:diagnostics`) - GET /api/v1/serverless/{id}/revisions — Revisions, conditions, Service/Route readiness - GET /api/v1/serverless/{id}/events — Platform events (needs `serverless:diagnostics`) - GET /api/v1/serverless/{id}/volumes — The container's volumes and what is on offer - POST /api/v1/serverless/{id}/volumes — Add a volume (`size_gb`, `mount_path`): 202 - GET /api/v1/serverless/{id}/volumes/{volume} — One volume (also while `deleting`) - PUT|PATCH /api/v1/serverless/{id}/volumes/{volume} — A larger `size_gb`, or a new `mount_path`: 202 (200 with `meta.changed: false` when it already is) - DELETE /api/v1/serverless/{id}/volumes/{volume} — Delete the volume (needs `serverless:delete`): 202 - GET|POST /api/v1/serverless/{id}/volume-backups — List backups; take one (optional `name`): 202 - GET|DELETE /api/v1/serverless/{id}/volume-backups/{backup} — One backup (poll until `terminal`); delete it (needs `serverless:delete`) - POST /api/v1/serverless/{id}/volume-backups/{backup}/restore — Restore it over the volume: 202. Send an `Idempotency-Key` - GET /api/v1/serverless/{id}/volume-restores[/{restore}] — The last twenty restores; one restore (poll until `terminal`) Support Tickets (needs `support:read` / `support:write`, not in the default abilities): - GET /api/v1/support-tickets: List your tickets in the current project (`status`, `per_page`) - POST /api/v1/support-tickets: Open a ticket (`type`, `priority`, `subject`, `message`, optional `resource_type` + `resource_id`; JSON only, `attachments` is refused with 422) - GET /api/v1/support-tickets/{number}: A ticket with its replies (`TICK-2026-00224`) - POST /api/v1/support-tickets/{number}/replies: Reply (reopens a ticket that waited on you or had ended) - POST /api/v1/support-tickets/{number}/resolve: Mark resolved (409 if already resolved or closed) A token without the ability a call needs gets 403 and the message names the missing one (`This token does not have the support:write ability. ...`): add it to the token under Security, API tokens. A ticket opened, or a reply sent, with a token is marked as sent with an API token: a token proves someone holds a credential, not who is asking. Support verifies the sender with the account's customer number, email and support PIN before changing anything or sharing account data. You reach only the tickets your user opened in the current project. 10 new tickets and 60 replies an hour per person across all their tokens, then 429 with `retry_after`. Send `Idempotency-Key` on opening a ticket and on replying to retry safely (a replay returns the first answer with `Idempotent-Replay: true`; keys are per member; never reuse one for a different request). The owner is emailed when a token opens a ticket, and once an hour per ticket when a token replies. Details: https://docs.danubedata.ro/api-support-tickets Object Storage: - S3 API at https://s3.danubedata.ro (use any S3 SDK) - Dashboard API for bucket management at /api/v1/storage-buckets --- ## DanubeData CLI The `danube` CLI deploys and manages static sites and serverless containers from the terminal. ### Installation ```bash npm install -g @danubedata/cli ``` Requires Node.js 18+. Package: `@danubedata/cli` on npm. ### Authentication ```bash # Interactive login danube login # Or via environment variable (recommended for CI/CD) export DANUBE_TOKEN="your-api-token" # Check identity danube whoami # Log out danube logout ``` Credentials stored at `~/.danube/config.json` (mode 0600). ### Static Sites Commands (danube pages) ```bash # Link current directory to a site (interactive) danube pages link # Deploy current directory (or --dir ./dist) danube pages deploy [--dir ] [--no-wait] # List deployments danube pages deployments ls # Roll back to revision 3 danube pages deployments rollback 3 # Domain management danube pages domains ls danube pages domains add www.example.com danube pages domains verify www.example.com danube pages domains remove www.example.com ``` ### Serverless Commands (danube rapids) ```bash # List containers danube rapids ls # Create from Docker image danube rapids create --name my-api --type docker_image --image node --tag 20-alpine --profile small --port 3000 # Create from Git repo danube rapids create --name my-api --type git_repository --repo https://github.com/user/repo --source-type dockerfile # Create from local directory danube rapids create --name my-api --type zip_upload --source-type buildpack danube rapids deploy my-api --dir ./ # Show details danube rapids show my-api # Update configuration danube rapids update my-api --profile medium --min-scale 1 --max-scale 20 danube rapids update my-api --env DATABASE_URL=postgres://... --env API_KEY=secret danube rapids update my-api --rm-env OLD_KEY # Delete danube rapids rm my-api [-y] # Deployment history danube rapids deployments my-api # Usage and billing danube rapids usage my-api [--period 2026-01] ``` ### Project Configuration `.danube/project.json` (created by `danube pages link`): ```json { "siteId": 42, "teamId": 1, "siteName": "my-site", "defaultDomain": "my-site-abc.pages.danubedata.ro" } ``` `danube.json` (optional, project root): ```json { "outputDir": "dist", "ignore": ["*.map", "test/**"] } ``` ### CI/CD Environment Variables | Variable | Description | |----------|-------------| | `DANUBE_TOKEN` | API token | | `DANUBE_API_BASE` | Override API URL (default: https://danubedata.ro) | | `DANUBE_SITE_ID` | Site ID for headless deploys | | `DANUBE_TEAM_ID` | Team ID for headless deploys | | `CI` | Suppress update notifications | ### GitHub Actions Example ```yaml - run: npm install -g @danubedata/cli - run: danube pages deploy --dir ./dist env: DANUBE_TOKEN: ${{ secrets.DANUBE_TOKEN }} DANUBE_SITE_ID: ${{ secrets.DANUBE_SITE_ID }} DANUBE_TEAM_ID: ${{ secrets.DANUBE_TEAM_ID }} ``` ### File Exclusion Always excluded: `.git`, `node_modules`, `.danube`. Also respects `.gitignore`, `.daubeignore`, and `danube.json` `ignore` patterns. --- ## Terraform Provider ### Installation ```hcl terraform { required_providers { danubedata = { source = "AdrianSilaghi/danubedata" version = "~> 0.1" } } } provider "danubedata" { # Uses DANUBEDATA_API_TOKEN environment variable } ``` ### Authentication ```bash export DANUBEDATA_API_TOKEN="your-api-token" ``` ### Resources (9 types) | Resource | Description | |----------|-------------| | `danubedata_vps` | Virtual machines | | `danubedata_database` | Managed databases | | `danubedata_cache` | Cache instances | | `danubedata_storage_bucket` | S3 storage buckets | | `danubedata_storage_access_key` | S3 access keys | | `danubedata_ssh_key` | SSH keys | | `danubedata_serverless` | Serverless containers | | `danubedata_firewall` | Network rules | | `danubedata_vps_snapshot` | VPS snapshots | ### Data Sources (4 types) | Data Source | Description | |-------------|-------------| | `danubedata_vps_images` | Available OS images | | `danubedata_cache_providers` | Cache providers and versions | | `danubedata_database_providers` | Database engines and versions | | `danubedata_ssh_keys` | Existing SSH keys | ### Example: Full Stack ```hcl resource "danubedata_ssh_key" "deploy" { name = "deploy-key" public_key = file("~/.ssh/id_rsa.pub") } resource "danubedata_vps" "app" { name = "app-server" datacenter = "fsn1" image = "ubuntu-24.04" profile = "small-shared" ssh_keys = [danubedata_ssh_key.deploy.id] } resource "danubedata_database" "db" { name = "app-db" engine = "postgresql" version = "17" profile = "small" } resource "danubedata_cache" "cache" { name = "app-cache" provider = "redis" version = "7.4" profile = "micro" } resource "danubedata_storage_bucket" "assets" { name = "app-assets" versioning_enabled = true encryption_enabled = true } resource "danubedata_firewall" "web" { name = "web-firewall" rules { direction = "inbound" protocol = "tcp" port = "80" source = "0.0.0.0/0" } rules { direction = "inbound" protocol = "tcp" port = "443" source = "0.0.0.0/0" } rules { direction = "inbound" protocol = "tcp" port = "22" source = "0.0.0.0/0" } vps_ids = [danubedata_vps.app.id] } ``` ### Remote State Backend (using DanubeData S3) ```hcl terraform { backend "s3" { bucket = "terraform-state" key = "my-project/terraform.tfstate" region = "fsn1" endpoint = "https://s3.danubedata.ro" skip_credentials_validation = true skip_metadata_api_check = true skip_region_validation = true force_path_style = true } } ``` --- ## Networking ### Firewalls - Per-instance inbound/outbound rules - Protocols: TCP, UDP, ICMP - Stateful rules - Port ranges supported - Free, unlimited firewalls and rules ### Egress Traffic - 20 TB included per VPS/month (pooled across team) - Overage: EUR 1.00/TB - Inbound: always free - Private network traffic: free ### Internal DNS Services accessible within the cluster: ``` {service-name}.tenant-{team-name}.svc.cluster.local ``` --- ## Security - Two-factor authentication (TOTP) - SSH key authentication for VPS - API bearer tokens with granular scopes - AES-256 server-side encryption for object storage (on by default for new buckets) - TLS 1.2 or 1.3 on the dashboard, API and object storage endpoints; TLS available on database connections - No encryption at rest for VPS disks, volumes, databases, caches or snapshots - Let's Encrypt automatic TLS for custom domains - Per-instance firewall rules - Network isolation per team namespace - Serverless: non-root, read-only filesystem, no privilege escalation --- ## Monitoring ### Available Metrics VPS: CPU, memory, disk usage, disk I/O, network traffic Databases: queries, connections, cache hit rate, replication lag, storage Cache: memory usage, hit/miss ratio, ops/sec, connections, evictions Serverless: requests, latency, error rate, replica count ### Logging - Real-time log streaming - 10-day retention - Search and filtering ### Alerting - Email notifications - Webhook integration - Threshold-based triggers --- ## Support ### Standard (Free) - Email support - 24-48 hour response - Business hours ### Premium (EUR 99/month or 10% of spend) - 24/7 support - < 4 hour response - Phone support - Dedicated engineer ### Enterprise (Contact sales) - 24/7 priority - < 1 hour response - Dedicated team - Custom SLA --- ## Price Comparison vs Competitors | Resource | DanubeData | AWS EC2 | DigitalOcean | |----------|------------|---------|--------------| | 2 vCPU, 2 GB (Shared) | EUR 4.49 | EUR 17.28 | EUR 18.00 | | 4 vCPU, 8 GB (Shared) | EUR 12.49 | EUR 34.56 | EUR 48.00 | | 8 vCPU, 16 GB (Shared) | EUR 24.99 | EUR 69.12 | EUR 96.00 |