{"slug":"serverless-invoking","title":"Invoking Serverless Containers","description":"Once your serverless container is deployed on DanubeData, it is accessible over HTTPS at a unique URL. You can invoke it from any HTTP client — a browser, a script, another service, or a CI/CD pipelin...","section":"Features","url":"https://docs.danubedata.ro/serverless-invoking","markdown_url":"https://docs.danubedata.ro/serverless-invoking.md","breadcrumbs":[{"title":"Features","slug":null},{"title":"Rapids","slug":"serverless-overview"},{"title":"Invoking Containers","slug":"serverless-invoking"}],"headings":[{"level":1,"title":"Invoking Serverless Containers","id":"invoking-serverless-containers"},{"level":2,"title":"Container URL","id":"container-url"},{"level":2,"title":"Making requests","id":"making-requests"},{"level":3,"title":"cURL","id":"curl"},{"level":3,"title":"JavaScript","id":"javascript"},{"level":3,"title":"Python","id":"python"},{"level":3,"title":"PHP","id":"php"},{"level":3,"title":"Go","id":"go"},{"level":2,"title":"Cold starts","id":"cold-starts"},{"level":2,"title":"Request and response limits","id":"request-and-response-limits"},{"level":2,"title":"Headers","id":"headers"},{"level":3,"title":"HTTPS and the original host","id":"https-and-the-original-host"},{"level":3,"title":"Getting the real client IP","id":"getting-the-real-client-ip"},{"level":2,"title":"Custom domains","id":"custom-domains"},{"level":2,"title":"Scaling behavior","id":"scaling-behavior"},{"level":2,"title":"Error handling","id":"error-handling"},{"level":2,"title":"Connecting to other DanubeData services","id":"connecting-to-other-danubedata-services"},{"level":2,"title":"Next steps","id":"next-steps"}],"format":"markdown","word_count":469,"content":"# Invoking Serverless Containers\n\nOnce your serverless container is deployed on DanubeData, it is accessible over HTTPS at a unique URL. You can invoke it from any HTTP client — a browser, a script, another service, or a CI/CD pipeline.\n\n## Container URL\n\nEvery container gets an auto-generated URL in the format:\n\n```\nhttps://{container-slug}-{team-name}.danubedata.run\n```\n\nYou can find this URL on the container detail page under **Connection Details**. If you have configured a custom domain, your container is also reachable at that domain.\n\n## Making requests\n\nYour container receives standard HTTP requests on the port you configured (default `8080`). DanubeData's edge layer terminates TLS and forwards the request to your container.\n\nAll HTTP methods are supported: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, and `OPTIONS`.\n\n### cURL\n\n```bash\n# Basic GET request\ncurl https://my-api-acme.danubedata.run\n\n# With custom headers\ncurl -X GET https://my-api-acme.danubedata.run \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Authorization: Bearer YOUR_API_TOKEN\"\n\n# POST request with JSON body\ncurl -X POST https://my-api-acme.danubedata.run/api/endpoint \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"key\": \"value\", \"data\": \"example\"}'\n\n# With timeout and verbose output\ncurl -X GET https://my-api-acme.danubedata.run \\\n  --max-time 30 \\\n  --connect-timeout 10 \\\n  -v\n```\n\n### JavaScript\n\n```javascript\n// Using fetch API\nconst response = await fetch('https://my-api-acme.danubedata.run');\nconst data = await response.json();\nconsole.log(data);\n\n// With custom headers\nconst response = await fetch('https://my-api-acme.danubedata.run', {\n    method: 'GET',\n    headers: {\n        'Content-Type': 'application/json',\n        'Authorization': 'Bearer YOUR_API_TOKEN',\n    },\n});\n\nif (!response.ok) {\n    throw new Error(`HTTP error! status: ${response.status}`);\n}\n\nconst data = await response.json();\nconsole.log(data);\n```\n\n```javascript\n// POST request with JSON body\nconst response = await fetch('https://my-api-acme.danubedata.run/api/endpoint', {\n    method: 'POST',\n    headers: {\n        'Content-Type': 'application/json',\n    },\n    body: JSON.stringify({\n        key: 'value',\n        data: 'example',\n    }),\n});\n\nconst result = await response.json();\nconsole.log(result);\n```\n\n```javascript\n// Using Axios\nimport axios from 'axios';\n\nconst { data } = await axios.get('https://my-api-acme.danubedata.run');\nconsole.log(data);\n\n// POST with Axios\nconst response = await axios.post('https://my-api-acme.danubedata.run/api/endpoint', {\n    key: 'value',\n    data: 'example',\n});\nconsole.log(response.data);\n```\n\n### Python\n\n```python\nimport requests\n\n# Basic GET request\nresponse = requests.get('https://my-api-acme.danubedata.run')\ndata = response.json()\nprint(data)\n```\n\n```python\nimport requests\n\n# With custom headers\nheaders = {\n    'Content-Type': 'application/json',\n    'Authorization': 'Bearer YOUR_API_TOKEN',\n}\n\nresponse = requests.get('https://my-api-acme.danubedata.run', headers=headers)\n\nif response.status_code == 200:\n    data = response.json()\n    print(data)\nelse:\n    print(f'Error: {response.status_code}')\n```\n\n```python\nimport requests\n\n# POST request with JSON body\npayload = {\n    'key': 'value',\n    'data': 'example',\n}\n\nresponse = requests.post(\n    'https://my-api-acme.danubedata.run/api/endpoint',\n    json=payload,\n    headers={'Content-Type': 'application/json'}\n)\n\nresult = response.json()\nprint(result)\n```\n\n```python\nimport aiohttp\nimport asyncio\n\n# Async example with aiohttp\nasync def call_container():\n    async with aiohttp.ClientSession() as session:\n        async with session.get('https://my-api-acme.danubedata.run') as response:\n            data = await response.json()\n            return data\n\ndata = asyncio.run(call_container())\nprint(data)\n```\n\n### PHP\n\n```php\n<?php\n// Using file_get_contents\n$response = file_get_contents('https://my-api-acme.danubedata.run');\n$data = json_decode($response, true);\nprint_r($data);\n```\n\n```php\n<?php\n// Using cURL with headers\n$ch = curl_init();\n\ncurl_setopt_array($ch, [\n    CURLOPT_URL => 'https://my-api-acme.danubedata.run',\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Content-Type: application/json',\n        'Authorization: Bearer YOUR_API_TOKEN',\n    ],\n]);\n\n$response = curl_exec($ch);\n$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\n\nif ($httpCode === 200) {\n    $data = json_decode($response, true);\n    print_r($data);\n} else {\n    echo \"Error: HTTP $httpCode\";\n}\n```\n\n```php\n<?php\n// Using Guzzle HTTP client\nuse GuzzleHttp\\Client;\n\n$client = new Client();\n\n$response = $client->get('https://my-api-acme.danubedata.run');\n$data = json_decode($response->getBody(), true);\nprint_r($data);\n\n// POST with Guzzle\n$response = $client->post('https://my-api-acme.danubedata.run/api/endpoint', [\n    'json' => [\n        'key' => 'value',\n        'data' => 'example',\n    ],\n]);\n\n$result = json_decode($response->getBody(), true);\nprint_r($result);\n```\n\n### Go\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"io\"\n    \"net/http\"\n)\n\nfunc main() {\n    resp, err := http.Get(\"https://my-api-acme.danubedata.run\")\n    if err != nil {\n        panic(err)\n    }\n    defer resp.Body.Close()\n\n    body, _ := io.ReadAll(resp.Body)\n    fmt.Println(string(body))\n}\n```\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"io\"\n    \"net/http\"\n)\n\n// With custom headers\nfunc main() {\n    client := &http.Client{}\n    req, _ := http.NewRequest(\"GET\", \"https://my-api-acme.danubedata.run\", nil)\n\n    req.Header.Add(\"Content-Type\", \"application/json\")\n    req.Header.Add(\"Authorization\", \"Bearer YOUR_API_TOKEN\")\n\n    resp, err := client.Do(req)\n    if err != nil {\n        panic(err)\n    }\n    defer resp.Body.Close()\n\n    body, _ := io.ReadAll(resp.Body)\n    fmt.Println(string(body))\n}\n```\n\n```go\npackage main\n\nimport (\n    \"bytes\"\n    \"encoding/json\"\n    \"fmt\"\n    \"io\"\n    \"net/http\"\n)\n\n// POST request with JSON body\nfunc main() {\n    payload := map[string]string{\n        \"key\":  \"value\",\n        \"data\": \"example\",\n    }\n    jsonData, _ := json.Marshal(payload)\n\n    resp, err := http.Post(\n        \"https://my-api-acme.danubedata.run/api/endpoint\",\n        \"application/json\",\n        bytes.NewBuffer(jsonData),\n    )\n    if err != nil {\n        panic(err)\n    }\n    defer resp.Body.Close()\n\n    body, _ := io.ReadAll(resp.Body)\n    fmt.Println(string(body))\n}\n```\n\n## Cold starts\n\nIf your container is scaled to zero (no running replicas), the first request triggers a cold start. During a cold start, DanubeData pulls your container image, starts the container, and routes the request once it is ready.\n\nCold start latency depends on:\n\n| Factor | Impact |\n|--------|--------|\n| **Image size** | Smaller images start faster. Use Alpine or slim base images. |\n| **Application startup time** | Minimize initialization work at boot. |\n| **Resource profile** | Larger profiles allocate more CPU during startup. |\n\n**To avoid cold starts entirely**, set **Min Replicas** to 1 or higher on the container edit page. This keeps at least one replica running at all times.\n\n## Request and response limits\n\n| Limit | Value |\n|-------|-------|\n| **Max request body** | No platform-imposed limit (tested up to 500 MB; bounded by request timeout and upload speed) |\n| **Max response body** | No platform-imposed limit (streamed through the gateway; bounded by request timeout) |\n| **Request timeout** | 300 seconds (default); configurable per container up to 3600 seconds (1 hour) from the edit page |\n| **Stream idle timeout** | 600 seconds (no bytes flowing closes the stream — relevant for SSE / long-polling) |\n| **Idle timeout** | 5 minutes before scale-to-zero |\n| **Min billing increment** | 100 ms |\n\n## Headers\n\nDanubeData injects the following headers into requests forwarded to your container:\n\n| Header | Description |\n|--------|-------------|\n| `X-Forwarded-For` | Full proxy chain: client IP followed by internal hops. **Use this one.** |\n| `X-Forwarded-Proto` | `https` on the `*.danubedata.run` address, `http` through a custom domain (see below) |\n| `Forwarded` | RFC 7239 equivalent of the same chain |\n| `X-Request-Id` | Unique request identifier for tracing |\n| `X-Envoy-External-Address` | Set by our gateway, but **not reliable — do not use it** (see below) |\n\n### HTTPS and the original host\n\nEvery request from the internet reaches your container over HTTPS: plain-HTTP requests are redirected to HTTPS before they get there. A request through a **custom domain**, though, passes through our gateway twice, and the second pass is an internal plain-HTTP hop, so `X-Forwarded-Proto` (and the `proto=` in `Forwarded`) arrives as `http`.\n\n- Treat production traffic as HTTPS rather than reading `X-Forwarded-Proto`. If your app redirects to HTTPS itself, base the redirect on your configured base URL, not on this header, or it will loop on custom domains.\n- Read the original domain from `Host`, which carries the domain the visitor used. We don't set `X-Forwarded-Host`; if one arrives, the client sent it, so don't trust it.\n\n### Getting the real client IP\n\nParse **`X-Forwarded-For`**. It arrives in this shape:\n\n```\nX-Forwarded-For: 203.0.113.45, 169.254.42.1, 10.42.6.170, 10.42.6.91\n                 └ real client   └────────── internal hops ────────┘\n```\n\nDo **not** read the leftmost entry. `X-Forwarded-For` is appended to rather than replaced, so anything the caller sent stays at the front of the list — a request arriving with `X-Forwarded-For: 1.2.3.4` reaches your container as `1.2.3.4, 203.0.113.45, ...`.\n\nDo **not** hard-code a hop count either. The number of internal hops varies with how the container is reached: a request through a **custom domain** passes through our gateway twice and therefore carries one more hop than the same request to the container's `*.danubedata.run` URL. They are also not all in the same address range.\n\nInstead, walk the list from the right, discard private and link-local addresses, and take the first public address you reach:\n\n| Range | Type |\n|-------|------|\n| `10.0.0.0/8` | private |\n| `172.16.0.0/12` | private |\n| `192.168.0.0/16` | private |\n| `169.254.0.0/16` | link-local |\n\nThis rule is safe against forgery. A caller who sends `X-Forwarded-For: 9.9.9.9` produces `9.9.9.9, 203.0.113.45, …` — the forged value ends up to the *left* of the real address, so walking from the right still yields the genuine client.\n\n> **Warning:** Two headers look useful here but are not.\n>\n> - **`X-Envoy-External-Address`** holds a single address and is set by our gateway, which makes it tempting. But when a container is reached through a **custom domain** the request passes through the gateway twice, and the second pass overwrites this header with an *internal* address (for example `169.254.42.1` or a `10.42.x.x` pod address). It is only correct on the `*.danubedata.run` URL, so it is not safe to rely on.\n> - **`X-Real-IP`** is not set by us at all, but we *do* forward it if the caller supplies one — a request sent with `X-Real-IP: 5.5.5.5` arrives at your container with exactly that value. Any code reading it is reading attacker-controlled input.\n\nIf your domain sits behind a CDN or reverse proxy of your own, `X-Envoy-External-Address` and the rightmost `X-Forwarded-For` entries will be **your CDN's edge address**, not the visitor's — we can only observe whoever connects to us. In that case read the header your CDN sets (for example Cloudflare's `CF-Connecting-IP`).\n\nReading the client IP correctly matters for anything security-relevant: rate limiting, audit logs, fraud signals, and identity APIs such as BankID that require the genuine end-user address.\n\n## Custom domains\n\nYou can add custom domains to your container from the **Manage Domains** page. Custom domains include:\n\n- Automatic TLS certificates via Let's Encrypt\n- DNS verification to confirm domain ownership\n- Primary domain designation for canonical routing\n\nAfter adding a domain, create a CNAME record pointing to your container's auto-generated URL.\n\nSee [Custom domains](https://docs.danubedata.ro/serverless-domains) for setup instructions.\n\n## Scaling behavior\n\nContainers scale automatically based on incoming traffic:\n\n- **Scale up** — When requests per second exceed the configured target (default: 100 RPS per replica), new replicas are added.\n- **Scale down** — When traffic drops, replicas are removed after a cooldown period.\n- **Scale to zero** — If no requests arrive for 5 minutes, the container scales to zero replicas and stops incurring compute charges.\n\nYou can configure the scaling range (min and max replicas), target concurrency, and the scaling metric on the container edit page.\n\n## Error handling\n\nYour container should return appropriate HTTP status codes. Common patterns:\n\n| Status code | Meaning |\n|-------------|---------|\n| `200` | Success |\n| `400` | Bad request — invalid input from the caller |\n| `401` | Unauthorized — missing or invalid credentials |\n| `404` | Not found — endpoint does not exist |\n| `429` | Too many requests — caller should back off and retry |\n| `500` | Internal server error — unexpected failure in your application |\n| `502` | Bad gateway — container is starting or crashed |\n| `503` | Service unavailable — container is scaling up (retry after a short delay) |\n\nIf you receive a `502` or `503`, wait a few seconds and retry. This usually means a cold start is in progress.\n\n## Connecting to other DanubeData services\n\nServerless containers can access your team's databases and caches within the same namespace using internal cluster DNS:\n\n```\n# Database (MySQL example)\nmysql -h my-database.tenant-acme.svc.cluster.local -u user -p\n\n# Cache (Redis example)\nredis-cli -h my-cache.tenant-acme.svc.cluster.local -p 6379\n```\n\nPass these connection strings as environment variables in your container configuration.\n\n## Next steps\n\n- [Authentication](https://docs.danubedata.ro/serverless-authentication) — Secure your container endpoints\n- [Custom domains](https://docs.danubedata.ro/serverless-domains) — Add your own domain with automatic TLS\n\n---\n\n**Questions?** Contact support at support@danubedata.ro\n","prev":{"title":"Deploy with CR: Troubleshooting","slug":"tutorial-cr-rapids-troubleshooting","url":"https://docs.danubedata.ro/tutorial-cr-rapids-troubleshooting","markdown_url":"https://docs.danubedata.ro/tutorial-cr-rapids-troubleshooting.md","json_url":"https://docs.danubedata.ro/tutorial-cr-rapids-troubleshooting.json"},"next":{"title":"Authentication","slug":"serverless-authentication","url":"https://docs.danubedata.ro/serverless-authentication","markdown_url":"https://docs.danubedata.ro/serverless-authentication.md","json_url":"https://docs.danubedata.ro/serverless-authentication.json"},"index_url":"https://docs.danubedata.ro/index.json"}