---
title: Configuration
description: A Service's environment, secrets, size, scaling, regions, port and health — the spec fields, and what each one decides.
type: how-to
product: hosting
summary: The spec of a Service, field by field, and the two ways to change it.
updated: 2026-10-01
order: 1
---

<Callout tone="note" title="Today this runs on the management API">
A service's variables are changed on the management API: `GET`, `POST` (`{"key","value"}`), `PATCH` and `DELETE` (`?key=`) on `/v1/projects/{id}/services/{name}/env`. Its size and port are `PATCH /v1/projects/{id}/services/{name}` (for example `{"instanceType":"nano"}`). The `sylphx hosting services update` commands below show the resource-model spelling of the same settings. The [Hosting quickstart](/docs/hosting/quickstart) has the full sequence.
</Callout>

Everything a Service is, is its spec. This page is the spec, field by field,
with the reason to touch each one.

## Environment variables

Two fields, and the difference between them is where the value lives:

<PropertyTable
	properties={[
		{
			name: 'env',
			type: 'map<string, string>',
			description: 'Plain environment variables, in the spec and readable by anyone who can read the Service. For a URL, a feature flag, a log level.',
		},
		{
			name: 'secret_env',
			type: 'map<string, string>',
			description: 'Variable name to Secret name. The platform resolves the value at instance start; the value never enters the spec.',
		},
	]}
/>

A secret is created once, and named from as many Services as need it:

```bash
sylphx secrets secrets create --parent orgs/acme/projects/shop/envs/production \
  --meta.display-name SHOP_DATABASE_URL shop-database-url
sylphx secrets secret-versions create \
  --parent orgs/acme/projects/shop/envs/production/secrets/shop-database-url \
  --payload "$DATABASE_URI"
```

```bash
sylphx hosting services update orgs/acme/projects/shop/envs/production/services/shop \
  --spec.secret-env DATABASE_URL=shop-database-url
```

```bash
curl -X PATCH "https://api.sylphx.com/v1/orgs/acme/projects/shop/envs/production/services/shop" \
  -H "Authorization: Bearer $SYLPHX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"service": {"name": "orgs/acme/projects/shop/envs/production/services/shop", "spec": {"secret_env": {"DATABASE_URL": "shop-database-url"}}}}'
```

<Callout tone="warning" title="A secret never goes in env">
`env` is stored in the spec and read back by anyone with `hosting:read` — it
is the wrong place for a password, a token or a connection string with
credentials in it. If the value would be embarrassing in a screenshot, it is
a Secret.
</Callout>

Rotating a secret is a new version of the Secret; the Services bound to it
pick it up on their next instance start. Nothing about it is copied into the
spec, so there is nothing to update twice.

## Where the code comes from

<PropertyTable
	properties={[
		{
			name: 'source.source_link',
			type: 'string',
			description: 'Build from this SourceLink’s repository on every push to branch.',
		},
		{
			name: 'source.image',
			type: 'string',
			description: 'Run an Artifact by digest: registry.sylphx.net/<path>@sha256:<hex>. No build, and branch is ignored.',
		},
		{
			name: 'source.branch',
			type: 'string',
			description: 'The branch that ships to this Service. The SourceLink’s production branch by default.',
		},
		{
			name: 'source.build.root_directory',
			type: 'string',
			description: 'The directory inside the repository to build; the root by default.',
		},
		{
			name: 'source.build.dockerfile',
			type: 'string',
			description: 'A Dockerfile path relative to root_directory. Unset lets the zero-config frontend detect the stack.',
		},
		{
			name: 'source.build.build_class',
			type: 'string',
			description: 'The Build class, sylphx-<os>-<size>; sylphx-linux-standard by default.',
		},
		{
			name: 'command',
			type: 'string',
			description: 'Overrides the image’s command — a worker runs its process here rather than on a port.',
		},
	]}
/>

Everything about the build that is not in the spec is in the repository. A
Service does not carry its own manifest: what it runs is what the source
says, and re-declaring the build in two places is how they drift apart.

## How it is exposed

<PropertyTable
	properties={[
		{
			name: 'port',
			type: 'int32',
			description: 'The port the application listens on. The public edge stays 443.',
		},
		{
			name: 'exposure',
			type: 'ServiceExposure',
			description: 'public (default) puts the Service behind the edge; internal keeps it reachable only from inside the environment.',
		},
		{
			name: 'regions',
			type: 'string[]',
			description: 'Regions to run in; the project’s home region by default.',
		},
	]}
/>

An internal Service is how a backend is reached: it has no public host, and a
Route cannot point at it from the edge. Data stays near its users by putting
the Service where they are — the Region you choose for a database and the
`regions` you choose for the Service that reads it are the same decision made
twice, so make them match.

## Size and scaling

<PropertyTable
	properties={[
		{
			name: 'size',
			type: 'InstanceSize',
			description: 'nano, micro, small, standard, large, xlarge or xxlarge. The plan’s default when unset.',
		},
		{
			name: 'scaling.min_instances',
			type: 'int32',
			description: 'Instances kept running with no traffic. 0 idles to zero and wakes on request.',
		},
		{
			name: 'scaling.max_instances',
			type: 'int32',
			description: 'Instances at most. 0 means the plan’s limit.',
		},
	]}
/>

```bash
sylphx hosting services update orgs/acme/projects/shop/envs/production/services/shop \
  --spec.size small \
  --spec.scaling.min-instances 0 \
  --spec.scaling.max-instances 10
```

Start at zero and raise `min_instances` only if the cold start is worth
paying for: an instance kept idle serves the first request faster and costs
the same as a busy one.

## Probing

<PropertyTable
	properties={[
		{
			name: 'health.protocol',
			type: 'HealthProtocol',
			description: 'http or tcp. Required.',
		},
		{
			name: 'health.path',
			type: 'string',
			description: 'The HTTP path to probe when the protocol is HTTP.',
		},
		{
			name: 'health.startup_timeout',
			type: 'duration',
			description: 'How long a new instance may take to pass its first probe.',
		},
	]}
/>

A `tcp` probe only asks whether something is listening, which is right for a
worker that has no HTTP surface. An `http` probe is the one that can tell a
finished boot from a listening socket.
[Health checks](/docs/hosting/health-checks) has the rest.

## Changing a Service

Every field above changes through `update`, and a change to the spec is a new
generation: the platform builds it, and it ships the way anything else does —
by rolling out. Writing the spec is not the deploy; the rollout is.

<RelatedDocs
	links={[
		{
			href: '/docs/hosting/deploy',
			label: 'Deploys',
			description: 'Waves, previews, rollbacks and custom domains.',
		},
		{
			href: '/docs/hosting/health-checks',
			label: 'Health checks',
			description: 'How an instance earns traffic, and how to serve /healthz.',
		},
		{
			href: '/docs/api/services',
			label: 'The services collection',
			description: 'Every field, every method, with examples.',
		},
	]}
/>
