# 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

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

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

```
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-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](https://docs.danubedata.ro/serverless-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](https://docs.danubedata.ro/serverless-authentication) — Secure your container endpoints
- [Custom domains](https://docs.danubedata.ro/serverless-domains) — Add your own domain with automatic TLS

---

**Questions?** Contact support at support@danubedata.ro
