Service-to-service communication: internal addresses and internal-only services.
How services of one application reach each other over the internal address, the RIVERVO_<NAME>_URL variables, services without a public address, several instances, and timeouts and retries with examples for Node.js, Laravel and Python.
Services of one application talk to each other directly, without going through the internet. A site can call its own API, a worker can call an internal service — and this traffic is free: it is not counted as traffic.
The examples below use two web services: web, the site visitors open, with a Public address, and api, which only web and the other services call, set to Internal only.
The internal address
Every web service — with a public address or internal only — has an Internal address of the form http://<service-name>:<port>, for example http://api:8080. It is shown on the service's page and on the application's Overview, with a copy button and the hint «Other services of this application reach it here — no internet, no traffic charges.»
- The port is 8080 for a service built from a repository, or the Port you set for a service from a ready image.
- Only the services of the same application can reach this address. It does not open from the internet or from other applications.
- The address uses
http://. The connection stays inside the application, so there is no certificate to set up.
Variables with the addresses
You don't need to copy the addresses into your code. Every code service of the application — web services, background workers and scheduled jobs — gets a variable with the internal address of each other web service of the application:
RIVERVO_<NAME>_URL=http://<name>:<port>
NAME is the service's name in capital letters, with dashes turned into _:
| Service | Variable |
|---|---|
api | RIVERVO_API_URL=http://api:8080 |
admin-api | RIVERVO_ADMIN_API_URL=http://admin-api:8080 |
web | RIVERVO_WEB_URL=http://web:8080 |
search (from a ready image, port 7700) | RIVERVO_SEARCH_URL=http://search:7700 |
- A service does not get a variable for itself.
- Databases and Valkey keep their own
RIVERVO_<NAME>_URLwith the connection address. Service names are unique within an application, so the names never clash. - The variables are listed on the Variables tab among the variables Set automatically. As with every automatic variable, a variable of your own (or a shared variable) with the same name takes its place — see Environment variables.
- Variables reach a service with its next deploy. When you add a new web service or rename one, the other services get the new or changed variable on their next deploy. Deploy them again so they pick it up, for example with
rivervo deploy <app> --no-build— it starts the running version again with the new variables, without a new build.
Internal-only services
A web service that only other services of the application call — an API behind your site, an admin backend, a search engine — doesn't need to be on the internet. Set it to Internal only:
| Public address | Internal only | |
|---|---|---|
Address on the internet https://<name>.rivervo.app | yes | no |
| Your own domains | yes | no |
| Branch previews | yes | no |
| Internal address for the other services | yes | yes |
| Several instances and automatic scaling, logs, monitoring, health checks | yes | yes |
An internal-only service runs like any other service; only the other services of the same application can reach it.
Choosing when you create a service
When you create a web service — including the first service of a new application, from a repository, a template or a ready image — choose Public address or Internal only. Nothing is selected in advance, so the choice is always yours. Background workers, scheduled jobs and databases never have a public address, so there is no choice for them.
Services created before this choice existed keep their public address.
Switching later
The service → Settings → Public address: turn the switch and confirm. Only an Administrator of the application or the account owner can change it. Nothing restarts.
- Switching to internal only is refused while the service has domains connected — remove them on its Domains tab first. Previews must be turned off first too.
- Turning the public address back on gives the service the same
https://<name>.rivervo.appaddress. The certificate can take about a minute. - Adding a domain to an internal-only service is refused — turn its public address on first.
The Terminal tab works in an internal-only service the same way as in any other.
Asynchronous communication
The calls above wait for an answer. When a service should hand over work and go on — background jobs, events that several services react to — send messages through a queue or a message broker instead: Valkey, NATS or RabbitMQ. See Events and queues.
Several instances
When the service you call runs several instances, its internal name leads to all of its running instances, and connections are spread among them.
A client that keeps its connections open and reuses them (a keep-alive connection pool) may keep sending its requests to the same instance; new connections are spread again. This is normal and needs no setup.
Timeouts and retries
During a deploy, or when automatic scaling stops an instance, a request that is in progress can fail. Two habits make the calls between services reliable:
- A short timeout — a few seconds, so a call never hangs while the instance it went to is stopping.
- One or two retries with a short pause, for requests that are safe to repeat — reading data with
GET. Don't repeat requests that change data (payments, orders) unless your API can tell a repeated request from a new one.
Example: Node.js
const api = process.env.RIVERVO_API_URL
async function getWithRetry(url, attempts = 3) {
for (let i = 1; ; i++) {
try {
const res = await fetch(url, { signal: AbortSignal.timeout(5000) })
if (res.status < 500 || i === attempts) return res
} catch (err) {
if (i === attempts) throw err
}
await new Promise((resolve) => setTimeout(resolve, 200 * i))
}
}
const res = await getWithRetry(`${api}/orders`)
const orders = await res.json()Example: Laravel
Put the address in config/services.php:
'api' => [
'url' => env('RIVERVO_API_URL'),
],Then read it with config(). In production the configuration is cached, and env() called outside the configuration files returns nothing:
use Illuminate\Support\Facades\Http;
$orders = Http::timeout(5)
->retry(2, 200)
->get(config('services.api.url').'/orders')
->json();timeout(5) waits at most 5 seconds; retry(2, 200) makes up to 2 attempts with 200 ms between them.
Example: Python
import os
import requests
API = os.environ["RIVERVO_API_URL"]
orders = requests.get(f"{API}/orders", timeout=5).json()With retries for GET:
import os
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
API = os.environ["RIVERVO_API_URL"]
session = requests.Session()
retry = Retry(total=2, backoff_factor=0.2, allowed_methods=["GET"], status_forcelist=[502, 503, 504])
session.mount("http://", HTTPAdapter(max_retries=retry))
orders = session.get(f"{API}/orders", timeout=5).json()A failed GET — a connection error or a 502, 503 or 504 answer — is repeated up to 2 times, with a short pause that grows each time.
From the command line and Rivervo MCP
rivervo appsand the service details show each web service's internal address and whether it has a public address.rivervo services set-public <service> on|offswitches the public address. It needs the Administrator role and a token with Full access.- In Rivervo MCP the same switch is the
set_public_addresstool.
See Developer tools for tokens and the command line.