Skip to content

Networking

How your services find each other on a private network, how they find their databases, and what the internet can reach.

The project network

Each project has one private network. Every service and managed database in the project joins it, and they talk to each other over it directly, without going through the public edge. Services in different projects are on different networks and cannot reach each other privately.

What the internet can reach depends on the service's type, which you choose when you create the service:

TypeReachable from the internet
Web serviceYes, over HTTPS at its *.hoststack.dev hostname and any custom domain you attach.
Static siteYes, the same way as a web service.
Private serviceNo. Only other services in the project can call it — for internal APIs and gRPC backends.
Worker, Cron jobNo. They get no public URL.

A web service's hostname looks like web-a1b2.hoststack.dev; outside the Production environment it carries the environment, as in web-staging-a1b2.hoststack.dev.

Call another service

For every other active service in the same project and environment, HostStack sets a variable holding its private address. For a sibling named my-api that listens on port 8080:

bash
HOSTSTACK_SVC_MY_API_INTERNAL_URL=http://hoststack-svc-42:8080   # prefer this one
MY_API_INTERNAL_URL=http://hoststack-svc-42:8080
ABCDEF1234567890_INTERNAL_URL=http://hoststack-svc-42:8080       # from the sibling's svc_… id
typescript
const res = await fetch(`${process.env.HOSTSTACK_SVC_MY_API_INTERNAL_URL}/health`);
HOSTSTACK_SVC_<NAME>_INTERNAL_URL

The name is the sibling's name in upper case, with every character other than a letter or digit turned into _. It sits in a namespace HostStack owns, so it cannot collide with an image's own configuration variables. Prefer it.

<NAME>_INTERNAL_URL

The same address under the shorter name. Its key changes if the sibling is renamed.

<PUBLIC_ID>_INTERNAL_URL

The same address keyed by the sibling's id without the svc_ prefix, in upper case. Its key never changes.

All three hold the same value: http://hoststack-svc-<id>:<port>, using the port the sibling itself listens on. The value never changes, not even on a rename. Inside a variable's value you can also write ${{services.my-api.url}} — see Environment variables.

Hostnames on the network

If you would rather write an address yourself, each service answers on:

  • hoststack-svc-<id> — the numeric id, the form the variables use.
  • hoststack-svc-<svc_id> — its svc_… id, lower case.
  • Its name, lower-cased with punctuation removed, such as my-api. Outside Production the environment is appended: my-api-staging.

Preview deploys get their own names ending in -pr-<number> and never answer on the production names.

Renaming a service

You can rename a service at any time. HostStack then redeploys it and every active sibling in its environment, so the <NAME>_INTERNAL_URL keys follow the new name within a few minutes. The address itself and the <PUBLIC_ID>_INTERNAL_URL key do not change.

Internal URL or public URL

A service can also call a sibling by its public address, such as https://api.example.com. That works, but the request then leaves the project network and comes back in through the public edge, like a visitor's. For a call inside one project and environment, use the *_INTERNAL_URL address instead:

Internal URLPublic URL
PathStraight to the sibling over the project networkOut to the edge and back in, with TLS both ways
Edge rate limitNoneCounts against the limit for its client address: 100 requests a second on average, bursts of 200, per address and domain
Client address the sibling seesThe caller’s address on the project networkA private address, explained below
Reachable fromThe same projectAnywhere, including other projects

Server-side rendering is the usual case. A front end that fetches from its own API on every page view sends every visitor’s request from one address. Over the public URL, that address hits the edge limit, and any per-IP limit in the API, as if it were a single, very busy visitor.

The edge limit is a flood guard sized for websites. An API that authenticates its callers and enforces its own per-key limits can raise it for every route of the service with edgeRateLimitRps in the service config (10 to 5,000 requests a second per address, bursts of twice that; MCP update_service_config edge_rate_limit_rps). Otherwise a customer you sell more than 100 requests a second gets a bare 429 from the edge that your app never sees or meters. It applies on the next deploy.

What your app sees from another service on the same machine

A request to a public URL from a container on the same machine reaches your app with a private client address, in 10.0.0.0/8, 172.16.0.0/12 or 192.168.0.0/16, in X-Real-Ip and X-Forwarded-For. It is the calling container’s address on its own project network, and it changes when that service redeploys. A caller on another machine arrives from that machine’s public address.

Reach a database or other resource

Link a managed resource to a service — on the service's Connections tab, or with hoststack db link db_... --service svc_... — and its connection details are injected on the next deploy, under an alias prefix:

ResourceDefault alias (dashboard)Variables
Postgres, MySQL, MariaDBDATABASE_URL, _HOST, _PORT, _DATABASE, _USERNAME, _PASSWORD
RedisREDISthe same six
MongoDBMONGOthe same six
Object storageS3_ENDPOINT, _REGION, _BUCKET, _ACCESS_KEY_ID, _SECRET_ACCESS_KEY

In the dashboard a second link of the same kind gets DATABASE_2 and so on. hoststack db link instead defaults the alias to the database's name in upper case (app-db becomes APP_DB); pass --alias to choose one. On top of the prefixed variables, the first linked SQL database also sets DATABASE_URL, the first Redis REDIS_URL and the first MongoDB MONGO_URL, the names most frameworks look for. See Managed databases.

Override or switch off an injected variable

Set the same key yourself — on the service or in an env group — and your value wins over the injected one. Set it to an empty value and the variable is not passed to the container at all. This works only for keys HostStack would have injected; an empty variable of your own is passed through as it is.

Clearing it takes two steps, and the second is easy to miss:

  1. Stop the injection

    bash
    hoststack env set svc_... POSTFIX_OUT_INTERNAL_URL=

    Then deploy. Call the sibling through HOSTSTACK_SVC_POSTFIX_OUT_INTERNAL_URL, which is still set.

  2. Remove what an earlier boot already wrote

    Needed whenever the image keeps its configuration on a volume — boky/postfix keeps /etc/postfix on one, so main.cf survives a redeploy. In the service's Terminal tab:

    bash
    postconf -# OUT_INTERNAL_URL
    postfix reload

Step 2 is needed because step 1 worked: with the variable gone, the image never visits that key again, so it never cleans it up either.

Environments share the network

All environments of a project — Production, Staging, previews — are on the same project network. What keeps them apart is naming, not a network boundary:

  • The *_INTERNAL_URL variables only ever point at siblings in the same environment, so a staging service is never handed a production address.
  • Name-based hostnames outside Production carry the environment (my-api-staging), and previews carry -pr-<number>.
  • A hoststack-svc-<id> address is still reachable from every environment in the project.

If two environments must not be able to reach each other at all, put them in separate projects.

What your app receives from the edge

Requests to a web service or static site pass through HostStack's edge, which adds the visitor's details as request headers. Private services are called directly on the project network, so they receive none of these; forward what you need from your public tier.

Visitor country

HostStack looks up each visitor's country from their IP address inside its own infrastructure — the address is never sent to a third party — and adds it to every request under two names:

Hs-Ipcountry

The HostStack header.

Cf-Ipcountry

The same value under Cloudflare's name, so code that already reads CF-IPCountry works unchanged.

The value is an upper-case ISO 3166-1 alpha-2 code such as DK or US. When the country can't be determined — a private address, an unknown range — the header is left out entirely, so check whether it is present rather than for a placeholder value. A country sent by the client itself is removed first, so the header cannot be spoofed.

typescript
app.get('/', (req, res) => {
  const country = req.get('hs-ipcountry'); // "DK", or undefined
  res.send(country ? `Hello from ${country}` : 'Hello');
});

IP-based country lookup is wrong for some VPN, carrier-NAT and satellite users. Use it for analytics, localisation and routing, not as an identity or fraud control. Country data by DB-IP (CC BY 4.0).

Visitor IP address

Read X-Real-Ip, or the left-most entry of X-Forwarded-For. A request from another service on the same machine carries a private address here; see Internal URL or public URL.

Next steps

Was this page helpful?

Essential cookies only — for login sessions. No tracking. Details