# Deploy Backend Applications | Volcano

URL: https://volcano.dev/solutions/backend-apps

> Run backend services on Volcano: functions in Node, Python and Ruby across eight regions, scaling to zero between requests, with Postgres, storage and project variables in the same project.

## A backend without the servers under it

Deploy services as functions in Node, Python or Ruby. They scale with traffic, run in eight regions, and share one database.

```console
$ volcano cloud functions deploy --all
checkout      nodejs22    deployed
invoicing     python3.12  deployed
notifications ruby3.4     deployed
$ volcano cloud functions logs checkout --type runtime --follow
POST /checkout  201  84ms  user 8f1c…
✓ scaled to zero between requests
```

## Most of a backend is not your product

A service needs a container image, a registry, a scheduler, health checks, autoscaling rules and a load balancer before it handles a single request. That machinery has to be maintained by someone, and none of it is the thing you set out to build.

On Volcano a service is a function. Deploy the directory and Volcano handles placement, scaling and TLS across eight regions, with a Postgres database beside it that your services reach over the ordinary wire protocol. Anything that takes longer than a request runs as a durable function instead of a worker you keep alive.

## Pick the language per service

*Runtimes*

Node, Python and Ruby all run as first-class functions, so a service can sit wherever its libraries already are. Nothing forces one language across the whole backend.

- Node.js 22 and 24, handler index.handler
- Python 3.10 to 3.14, handler main.handler
- Ruby 3.3, 3.4 and 4.0

```console
nodejs22    nodejs24
python3.10  python3.11  python3.12
python3.13  python3.14
ruby3.3     ruby3.4     ruby4.0
# one backend, three languages, no extra infrastructure
```

## Close to whoever called

*Placement*

One hostname resolves to the nearest of eight production regions, so a request from Sydney is not answered from Virginia. There is no region routing for you to configure.

- Eight regions across five continents
- One hostname, answered from the closest region
- No load balancer to run

Eight production regions, reached through one endpoint.: us-east-1, us-west-2, eu-west-1, eu-central-1, ap-southeast-1, ap-northeast-1, ap-southeast-2, sa-east-1.

## Nothing to provision, nothing to size

*Operations*

There is no instance to pick, no autoscaling group to tune and no pooler to keep alive. Memory and timeout are the same for every function in the project, so a new endpoint is a file and a deploy rather than a capacity decision.

- No always-on instance to right-size
- Placement and scaling handled for you
- A new endpoint is a file, not a capacity plan

```console
orders     nodejs24.x   deployed to 8 regions
invoices   python3.12   deployed to 8 regions
webhooks   ruby3.4      deployed to 8 regions
# no instance to choose, no pooler to keep alive
✓ 3/3 functions deployment started · scaling handled for you
```

## How it works

1. **Split by service**: One function per service. Each gets its own runtime, logs and deploy.
2. **Share one database**: Migrations live with the code, and every service connects with the same string.
3. **Keep secrets out**: Project variables are injected at runtime and deployed with the rest of the config.
4. **Deploy and watch**: Ship everything with one command, then follow build or runtime logs per service.

## A service, its data, its config, its deploy

### A service

```javascript
const { Client } = require('pg');
const { databaseConnectionString } = require('@volcano.dev/sdk');

exports.handler = async (event) => {
  const auth = event.__volcano_auth;
  if (!auth) {
    return { statusCode: 401, body: JSON.stringify({ error: 'sign in required' }) };
  }

  const { cartId } = event;

  // Connect as the caller so the cart policy applies here too.
  const db = new Client({
    connectionString: databaseConnectionString(process.env.DATABASE_URL, {
      userId: auth.user_id,
    }),
  });
  await db.connect();

  try {
    const { rows } = await db.query(
      'select id, total_cents from carts where id = $1 and checked_out_at is null',
      [cartId],
    );

    if (rows.length === 0) {
      return { statusCode: 404, body: JSON.stringify({ error: 'cart not found' }) };
    }

    await db.query('update carts set checked_out_at = now() where id = $1', [cartId]);

    return { statusCode: 201, body: JSON.stringify({ orderTotal: rows[0].total_cents }) };
  } finally {
    await db.end();
  }
};
```

### Another runtime

```python
import json
import os
from decimal import Decimal

import psycopg


def handler(event, context):
    # This service is internal, so it stays private: only service keys and
    # schedulers reach it, and anyone else gets a 404 before it runs. A
    # service-key call arrives with no __volcano_auth, so one here means the
    # function was opened to users by mistake.
    if event.get("__volcano_auth") is not None:
        return {"statusCode": 403, "body": json.dumps({"error": "internal only"})}

    order_id = event["order_id"]

    # Nobody to impersonate here, so it connects with full access.
    with psycopg.connect(os.environ["DATABASE_URL"]) as db, db.cursor() as cur:
        cur.execute(
            "select line_total_cents from order_lines where order_id = %s",
            (order_id,),
        )
        lines = [Decimal(row[0]) / 100 for row in cur.fetchall()]

        total = sum(lines)
        cur.execute(
            "insert into invoices (order_id, total_cents) values (%s, %s) returning id",
            (order_id, int(total * 100)),
        )
        invoice_id = cur.fetchone()[0]

    return {"statusCode": 201, "body": json.dumps({"invoice_id": str(invoice_id)})}
```

### Config

```yaml
version: 1

# A declared variables list is fully synced: project variables missing from
# it are deleted on apply.
variables:
  - name: STRIPE_API_KEY
    value: ${STRIPE_API_KEY}
  - name: SMTP_HOST
    value: smtp.example.com
  - name: INVOICE_FROM
    value: billing@example.com

functions:
  - name: checkout
    visibility: authenticated   # signed-in shoppers call it from the browser
  - name: invoicing
    visibility: private         # service keys and schedulers only
  - name: notifications
    visibility: private

# Secrets interpolate from the environment, so nothing sensitive is committed.
# Preview the diff before applying it:
#   volcano cloud config deploy --dry-run
```

### Deploy

```bash
volcano use my-backend

volcano cloud databases migration up --all -d app
volcano cloud functions deploy --all
volcano cloud config deploy --dry-run
volcano cloud config deploy

# Redeploy a single service after a fix
volcano cloud functions deploy -f checkout

# Build failures and runtime errors are separate streams
volcano cloud functions logs checkout --type build
volcano cloud functions logs checkout --type runtime --follow
```

## What disappears

- **0 servers to operate**: No cluster, no images, no autoscaling rules, no pooler in front of the database.
- **8 regions with no routing to write**: One hostname resolves to the closest region automatically.
- **3 languages in one backend**: Node, Python and Ruby side by side, each service using whichever fits.

## Outcomes

- **Nothing to operate**: No cluster, no images, no autoscaling rules to tune.
- **Independent services**: Each one deploys, logs and fails on its own.
- **Scaling you do not tune**: Traffic goes up, Volcano runs more copies. No rules to write.

## Built on Volcano

- [The services](https://volcano.dev/features/edge-functions): Functions in Node, Python or Ruby, deployed per service, with build and runtime logs kept separate.
- [Shared state](https://volcano.dev/features/postgres): PostgreSQL 16 through 18 reachable over the wire protocol, from your services and from psql alike.
- [Callers](https://volcano.dev/features/authentication): Sessions verified before your handler runs, arriving on the event as a real user.
- [Work that outlives a request](https://volcano.dev/features/durable-functions): Durable functions for the multi-step jobs a request should not wait on, checkpointed so they resume after a failure.

## Frequently asked questions

### How long can a request take?

Up to 30 seconds per invocation on Hobby or 180 seconds on Superagent. The plan's ceiling applies to every function in the project rather than being configured per function.

### Can different services use different languages?

Yes. The runtime comes from each function's entrypoint file, so a Node checkout service and a Python invoicing service live in the same project and share the same database.

### How do services talk to each other?

Over HTTP, or through the SDK with volcano.functions.invoke. An internal service left private, the default, answers only your service keys and schedulers, so a browser gets a 404 whether it carries the anon key or a user's session, while your own calls still get through.

### Where do secrets live?

In project variables, declared in volcano-config.yaml with values interpolated from the environment and injected at runtime. Nothing sensitive is committed.

### What about a long-running worker?

There is no always-on process. Run long work as a durable function, which checkpoints each step and resumes after a failure, or as a scheduled function that claims a batch each run. Both survive restarts better than a daemon holding state in memory.

### How do I debug a failing deploy?

volcano cloud functions logs \<name\> --type build shows what happened during packaging, and --type runtime --follow streams live invocations once it is deployed.

## Get started

Ready to deploy your backend? Create a free account at https://volcano.dev/signup, or have your coding agent follow the setup playbook at https://volcano.dev/setup.md.
