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:

Text
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

Bash
# 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

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);
JavaScript
// 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);
JavaScript
// 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

Python
import requests

# Basic GET request
response = requests.get('https://my-api-acme.danubedata.run')
data = response.json()
print(data)
Python
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}')
Python
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)
Python
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
<?php
// Using file_get_contents
$response = file_get_contents('https://my-api-acme.danubedata.run');
$data = json_decode($response, true);
print_r($data);
PHP
<?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
<?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

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))
}
Go
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))
}
Go
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:

FactorImpact
Image sizeSmaller images start faster. Use Alpine or slim base images.
Application startup timeMinimize initialization work at boot.
Resource profileLarger 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

LimitValue
Max request bodyNo platform-imposed limit (tested up to 500 MB; bounded by request timeout and upload speed)
Max response bodyNo platform-imposed limit (streamed through the gateway; bounded by request timeout)
Request timeout300 seconds (default); configurable per container up to 3600 seconds (1 hour) from the edit page
Stream idle timeout600 seconds (no bytes flowing closes the stream — relevant for SSE / long-polling)
Idle timeout5 minutes before scale-to-zero
Min billing increment100 ms

Headers

DanubeData injects the following headers into requests forwarded to your container:

HeaderDescription
X-Forwarded-ForFull proxy chain: client IP followed by internal hops. Use this one.
X-Forwarded-Protohttps on the *.danubedata.run address, http through a custom domain (see below)
ForwardedRFC 7239 equivalent of the same chain
X-Request-IdUnique request identifier for tracing
X-Envoy-External-AddressSet 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 set X-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:

Text
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:

RangeType
10.0.0.0/8private
172.16.0.0/12private
192.168.0.0/16private
169.254.0.0/16link-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-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.
  • 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.

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 codeMeaning
200Success
400Bad request — invalid input from the caller
401Unauthorized — missing or invalid credentials
404Not found — endpoint does not exist
429Too many requests — caller should back off and retry
500Internal server error — unexpected failure in your application
502Bad gateway — container is starting or crashed
503Service 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:

Text
# 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


Questions? Contact support at support@danubedata.ro