Invoking Serverless Containers
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 pipeline.
Container URL
Every container gets an auto-generated URL in the format:
https://{container-slug}-{team-name}.danubedata.run
You 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.
Making requests
Your 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.
All HTTP methods are supported: GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS.
cURL
# Basic GET request
curl https://my-api-acme.danubedata.run
# With custom headers
curl -X GET https://my-api-acme.danubedata.run \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_TOKEN"
# POST request with JSON body
curl -X POST https://my-api-acme.danubedata.run/api/endpoint \
-H "Content-Type: application/json" \
-d '{"key": "value", "data": "example"}'
# With timeout and verbose output
curl -X GET https://my-api-acme.danubedata.run \
--max-time 30 \
--connect-timeout 10 \
-v
JavaScript
// Using fetch API
const response = await fetch('https://my-api-acme.danubedata.run');
const data = await response.json();
console.log(data);
// With custom headers
const response = await fetch('https://my-api-acme.danubedata.run', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_TOKEN',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(data);
// POST request with JSON body
const response = await fetch('https://my-api-acme.danubedata.run/api/endpoint', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
key: 'value',
data: 'example',
}),
});
const result = await response.json();
console.log(result);
// Using Axios
import axios from 'axios';
const { data } = await axios.get('https://my-api-acme.danubedata.run');
console.log(data);
// POST with Axios
const response = await axios.post('https://my-api-acme.danubedata.run/api/endpoint', {
key: 'value',
data: 'example',
});
console.log(response.data);
Python
import requests
# Basic GET request
response = requests.get('https://my-api-acme.danubedata.run')
data = response.json()
print(data)
import requests
# With custom headers
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_TOKEN',
}
response = requests.get('https://my-api-acme.danubedata.run', headers=headers)
if response.status_code == 200:
data = response.json()
print(data)
else:
print(f'Error: {response.status_code}')
import requests
# POST request with JSON body
payload = {
'key': 'value',
'data': 'example',
}
response = requests.post(
'https://my-api-acme.danubedata.run/api/endpoint',
json=payload,
headers={'Content-Type': 'application/json'}
)
result = response.json()
print(result)
import aiohttp
import asyncio
# Async example with aiohttp
async def call_container():
async with aiohttp.ClientSession() as session:
async with session.get('https://my-api-acme.danubedata.run') as response:
data = await response.json()
return data
data = asyncio.run(call_container())
print(data)
PHP
<?php
// Using file_get_contents
$response = file_get_contents('https://my-api-acme.danubedata.run');
$data = json_decode($response, true);
print_r($data);
<?php
// Using cURL with headers
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://my-api-acme.danubedata.run',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer YOUR_API_TOKEN',
],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$data = json_decode($response, true);
print_r($data);
} else {
echo "Error: HTTP $httpCode";
}
<?php
// Using Guzzle HTTP client
use GuzzleHttp\Client;
$client = new Client();
$response = $client->get('https://my-api-acme.danubedata.run');
$data = json_decode($response->getBody(), true);
print_r($data);
// POST with Guzzle
$response = $client->post('https://my-api-acme.danubedata.run/api/endpoint', [
'json' => [
'key' => 'value',
'data' => 'example',
],
]);
$result = json_decode($response->getBody(), true);
print_r($result);
Go
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
resp, err := http.Get("https://my-api-acme.danubedata.run")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
package main
import (
"fmt"
"io"
"net/http"
)
// With custom headers
func main() {
client := &http.Client{}
req, _ := http.NewRequest("GET", "https://my-api-acme.danubedata.run", nil)
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer YOUR_API_TOKEN")
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
// POST request with JSON body
func main() {
payload := map[string]string{
"key": "value",
"data": "example",
}
jsonData, _ := json.Marshal(payload)
resp, err := http.Post(
"https://my-api-acme.danubedata.run/api/endpoint",
"application/json",
bytes.NewBuffer(jsonData),
)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
Cold starts
If 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.
Cold start latency depends on:
| Factor | Impact |
|---|---|
| Image size | Smaller images start faster. Use Alpine or slim base images. |
| Application startup time | Minimize initialization work at boot. |
| Resource profile | Larger profiles allocate more CPU during startup. |
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.
Request and response limits
| Limit | Value |
|---|---|
| Max request body | No platform-imposed limit (tested up to 500 MB; bounded by request timeout and upload speed) |
| Max response body | No platform-imposed limit (streamed through the gateway; bounded by request timeout) |
| Request timeout | 300 seconds (default); configurable per container up to 3600 seconds (1 hour) from the edit page |
| Stream idle timeout | 600 seconds (no bytes flowing closes the stream — relevant for SSE / long-polling) |
| Idle timeout | 5 minutes before scale-to-zero |
| Min billing increment | 100 ms |
Headers
DanubeData injects the following headers into requests forwarded to your container:
| Header | Description |
|---|---|
X-Forwarded-For | Full proxy chain: client IP followed by internal hops. Use this one. |
X-Forwarded-Proto | https on the *.danubedata.run address, http through a custom domain (see below) |
Forwarded | RFC 7239 equivalent of the same chain |
X-Request-Id | Unique request identifier for tracing |
X-Envoy-External-Address | Set by our gateway, but not reliable — do not use it (see below) |
HTTPS and the original host
Every 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.
- 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. - Read the original domain from
Host, which carries the domain the visitor used. We don't setX-Forwarded-Host; if one arrives, the client sent it, so don't trust it.
Getting the real client IP
Parse X-Forwarded-For. It arrives in this shape:
X-Forwarded-For: 203.0.113.45, 169.254.42.1, 10.42.6.170, 10.42.6.91
└ real client └────────── internal hops ────────┘
Do 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, ....
Do 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.
Instead, walk the list from the right, discard private and link-local addresses, and take the first public address you reach:
| Range | Type |
|---|---|
10.0.0.0/8 | private |
172.16.0.0/12 | private |
192.168.0.0/16 | private |
169.254.0.0/16 | link-local |
This 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.
Warning: Two headers look useful here but are not.
X-Envoy-External-Addressholds 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 example169.254.42.1or a10.42.x.xpod address). It is only correct on the*.danubedata.runURL, so it is not safe to rely on.X-Real-IPis not set by us at all, but we do forward it if the caller supplies one — a request sent withX-Real-IP: 5.5.5.5arrives at your container with exactly that value. Any code reading it is reading attacker-controlled input.
If 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).
Reading 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.
Custom domains
You can add custom domains to your container from the Manage Domains page. Custom domains include:
- Automatic TLS certificates via Let's Encrypt
- DNS verification to confirm domain ownership
- Primary domain designation for canonical routing
After adding a domain, create a CNAME record pointing to your container's auto-generated URL.
See Custom domains for setup instructions.
Scaling behavior
Containers scale automatically based on incoming traffic:
- Scale up — When requests per second exceed the configured target (default: 100 RPS per replica), new replicas are added.
- Scale down — When traffic drops, replicas are removed after a cooldown period.
- Scale to zero — If no requests arrive for 5 minutes, the container scales to zero replicas and stops incurring compute charges.
You can configure the scaling range (min and max replicas), target concurrency, and the scaling metric on the container edit page.
Error handling
Your container should return appropriate HTTP status codes. Common patterns:
| Status code | Meaning |
|---|---|
200 | Success |
400 | Bad request — invalid input from the caller |
401 | Unauthorized — missing or invalid credentials |
404 | Not found — endpoint does not exist |
429 | Too many requests — caller should back off and retry |
500 | Internal server error — unexpected failure in your application |
502 | Bad gateway — container is starting or crashed |
503 | Service unavailable — container is scaling up (retry after a short delay) |
If you receive a 502 or 503, wait a few seconds and retry. This usually means a cold start is in progress.
Connecting to other DanubeData services
Serverless containers can access your team's databases and caches within the same namespace using internal cluster DNS:
# Database (MySQL example)
mysql -h my-database.tenant-acme.svc.cluster.local -u user -p
# Cache (Redis example)
redis-cli -h my-cache.tenant-acme.svc.cluster.local -p 6379
Pass these connection strings as environment variables in your container configuration.
Next steps
- Authentication — Secure your container endpoints
- Custom domains — Add your own domain with automatic TLS
Questions? Contact support at support@danubedata.ro