> ## Documentation Index
> Fetch the complete documentation index at: https://docs.specific.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Services

> Define how your code runs and how it connects to everything else, in development and production.

Services define how to run your code and connect it to other parts of your infrastructure, both in production and in local development. All configuration reaches your app through environment variables, and the same `specific.hcl` drives `specific dev` locally and `specific deploy` in production.

A typical setup with a public frontend and API:

```hcl specific.hcl theme={null}
build "web" {
  base = "node"
  command = "npm run build"
}

service "web" {
  build = build.web
  command = "npm start"

  endpoint {
    public = true
  }

  env = {
    PORT = port
    API_URL = "https://${service.api.public_url}"
  }

  dev {
    env = {
      API_URL = "http://${service.api.public_url}"
    }
  }
}

build "api" {
  base = "go"
  command = "go build -o api"
}

service "api" {
  build = build.api
  command = "./api"

  endpoint {
    public = true
  }

  env = {
    PORT = port
  }
}
```

* `command` - command to start the server.
* `root` - working directory for service commands, relative to `specific.hcl`. Defaults to the referenced build's `root`, or `"."` if no build.
* `endpoint` - defines a network endpoint (see below).
* `port` - reference to the auto-assigned port; pass it to your server.

See [Builds](/guides/builds) for the `build` block.

## Endpoints

Endpoints define how a service is accessible over the network.

### Single endpoint

```hcl specific.hcl theme={null}
service "api" {
  build = build.api
  command = "./api"

  endpoint {
    public = true
  }

  env = {
    PORT = port
  }
}
```

* `public = true` makes the endpoint publicly accessible via HTTPS.
* Omitting `public` (or `public = false`) keeps the endpoint internal-only.

Public services are served from a managed `*.spcf.app` URL by default. To attach your own domain, see [Custom domains](/guides/domains).

### Multiple named endpoints

A service can expose multiple endpoints on different ports, each independently public or internal:

```hcl specific.hcl theme={null}
service "api" {
  build = build.api
  command = "./api"

  endpoint "main" {
    public = true
  }

  endpoint "admin" {}

  env = {
    MAIN_PORT = endpoint.main.port
    ADMIN_PORT = endpoint.admin.port
  }
}
```

With multiple endpoints, use `endpoint.<name>.port` instead of `port` to reference each endpoint's port.

### Implicit endpoints

If a service uses `port` in its env vars but has no explicit endpoint blocks, an implicit internal endpoint is created:

```hcl specific.hcl theme={null}
service "worker" {
  build = build.worker
  command = "./worker"

  env = {
    PORT = port  # Creates an implicit internal endpoint
  }
}
```

### Health checks

Add a `health_check` block inside an `endpoint` to tell Specific how to verify the service is healthy. The platform sends an HTTP `GET` to the given path on the endpoint's port; a `2xx` response means healthy.

```hcl specific.hcl theme={null}
service "api" {
  build = build.api
  command = "./api"

  endpoint {
    public = true
    health_check {
      path = "/healthz"
    }
  }

  env = {
    PORT = port
  }
}
```

* `path` - the HTTP path to probe (required, must start with `/`).
* Keep the handler cheap: don't query the database or call other services. A constant `200 OK` is sufficient. If your service already serves a root path that always returns `2xx` (for example, a static site), point `health_check` at `/`.
* Only one `health_check` per service is used. If multiple endpoints declare one, the first one wins.
* Background workers (services without endpoints) don't support `health_check` yet - only HTTP-serving endpoints do.

In production, health checks control rollouts and restarts:

* During a deploy, the new instance doesn't receive traffic until its health check passes. The old version stays up until then, so users never hit a half-started process.
* If the health check starts failing after the deploy is live, the service is restarted automatically. Traffic is drained from the unhealthy instance before the restart.
* If the health check never passes during a deploy (typically a misconfigured `path`), the deploy fails fast with a `health_check_failed` error pointing at the probe URL.

## Connecting services

### Private communication

Services reference each other's endpoints with `private_url` to communicate over a private network:

```hcl specific.hcl theme={null}
service "worker" {
  build = build.worker
  command = "./worker"

  env = {
    API_URL = service.api.private_url
  }
}
```

Available service reference attributes:

| Attribute                    | Description                                                                                                                              |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `service.<name>.private_url` | Internal URL without scheme (for example, `localhost:3001` in dev). Only reachable by other services.                                    |
| `service.<name>.host`        | Host only.                                                                                                                               |
| `service.<name>.port`        | Port only.                                                                                                                               |
| `service.<name>.public_url`  | Public URL without scheme (`my-app.spcf.app` in production, `localhost:3001` in dev). Only available for endpoints with `public = true`. |

For services with multiple named endpoints, specify the endpoint: `service.api.endpoint.main.private_url`.

<Note>
  `public_url` and `private_url` do **not** include a scheme. Production services are served over HTTPS; local development uses plain HTTP. Compose the full URL with the right scheme per environment, using the `dev` block to override locally:

  ```hcl theme={null}
  env = {
    API_URL = "https://${service.api.public_url}"
  }

  dev {
    env = {
      API_URL = "http://${service.api.public_url}"
    }
  }
  ```
</Note>

### Public communication

Private URLs aren't reachable from a browser. When a frontend needs to call a backend, the backend must expose a public endpoint, and the frontend references it with `public_url`:

```hcl specific.hcl theme={null}
service "api" {
  build = build.api
  command = "./api"

  endpoint {
    public = true
  }

  env = {
    PORT = port
    CORS_ORIGIN = "https://${service.web.public_url}"
  }

  dev {
    env = {
      CORS_ORIGIN = "http://${service.web.public_url}"
    }
  }
}
```

## Environment variables

All connection details (database URLs, storage credentials, secrets) are injected as environment variables. The same references resolve to the correct values in both local development and production.

```hcl specific.hcl theme={null}
service "api" {
  build = build.api
  command = "./api"

  endpoint {
    public = true
  }

  env = {
    PORT = port
    NODE_ENV = "production"
    DATABASE_URL = postgres.main.url
  }
}
```

### String interpolation

Embed references inside strings to compose values:

```hcl specific.hcl theme={null}
service "worker" {
  build = build.worker
  command = "./worker"

  env = {
    QUERY_URL  = "http://${service.api.private_url}/api/query"
    CUSTOM_DB  = "host=${postgres.main.host} port=${postgres.main.port}"
    PUBLIC_API = "https://${service.api.public_url}/v1"
  }

  dev {
    env = {
      PUBLIC_API = "http://${service.api.public_url}/v1"
    }
  }
}
```

All resource references (services, postgres, redis, storage, port, endpoint) can be used inside interpolated strings. Secret and config references cannot be interpolated; use them as standalone values.

## Local development

### Dev commands

The `dev` block overrides how a service runs during `specific dev`. Use it for hot-reload servers, watch modes, or anything optimized for a fast feedback loop. If the referenced build has no `dev` block, the build step is skipped in development.

```hcl specific.hcl theme={null}
service "web" {
  build = build.nextjs
  command = "npm start"

  endpoint {
    public = true
  }

  env = {
    PORT = port
  }

  dev {
    command = "npm run dev"  # Handles building + serving with hot reload
  }
}
```

The `dev.env` block is merged with the top-level `env`, so you only override the variables that differ.

### Dev-only services

A service with a `dev.command` but no top-level `command` only runs in local development and is excluded from deployment. Useful for local versions of external services like mock servers:

```hcl specific.hcl theme={null}
service "mock-api" {
  dev {
    command = "npx json-server --watch db.json --port $PORT"
  }

  endpoint {}

  env = {
    PORT = port
  }
}
```

Dev-only services cannot have a `build` reference, and can define endpoints that other services reference during development.

See [Local development](/concepts/local-development) for ports, logs, isolated instances, and tunnels.

## Monorepos

Use `root` on builds to set the working directory. Services inherit the build's root, so commands like `npm start` run from the correct directory:

```hcl specific.hcl theme={null}
build "backend" {
  base = "node"
  root = "packages/backend"
  command = "npm run build"
}

service "backend" {
  build = build.backend
  command = "node dist/index.js"

  dev {
    command = "npm run dev"
  }
}
```

Dev-only services can set their own `root`.

## Workers

Services without any endpoint act as background workers. They deploy like other services but don't receive HTTP traffic, which is common for queue consumers and background processing.

```hcl specific.hcl theme={null}
service "worker" {
  build = build.worker
  command = "npm run worker"

  env = {
    DATABASE_URL = postgres.main.url
  }
}
```

For work on a schedule, use [Crons](/guides/crons) instead.

## Pre-existing images

Services can run a pre-built container image instead of a build. `image` and `build` are mutually exclusive, and image-based services skip the build step entirely:

```hcl specific.hcl theme={null}
service "redis" {
  image = "redis:7"
  command = "redis-server --port $PORT"

  endpoint {}

  env = {
    PORT = port
  }

  dev {
    command = "redis-server --port $PORT"
  }
}
```

* `image` - container image reference (for example, `redis:7` or `ghcr.io/org/image:tag`).
* Use `dev.command` to run the equivalent process locally.
* Deploy hooks work with image-based services, and you can mix build-based and image-based services in one project.

## Deploy hooks

Run commands before or after deploying a service. Hooks run as one-off jobs with the same container and environment variables as the service. If a hook fails, the deployment is aborted.

```hcl specific.hcl theme={null}
service "api" {
  build = build.api
  command = "node dist/index.js"

  endpoint {
    public = true
  }

  env = {
    PORT = port
    DATABASE_URL = postgres.main.url
  }

  pre_deploy {
    command = "npm run db:migrate"
  }

  post_deploy {
    command = "npm run cache:warm"
  }
}

postgres "main" {}
```

* `pre_deploy` - runs before the service is deployed. **Database migrations belong here**: the schema is updated before new code runs, and a failed migration aborts the deployment instead of leaving a broken state.
* `post_deploy` - runs after the service is deployed (cache warming, notifications).

In development, run migrations with [`specific exec`](/reference/cli#specific-exec) whenever the schema changes, for example `specific exec api -- npm run db:push`.

## Graceful shutdown

When a service is redeployed or stopped, Specific sends your app a `SIGTERM` signal and waits a short grace period before forcibly stopping it. Handle `SIGTERM` to finish in-flight requests and close connections cleanly.

For the signal to reach your code, `command` should run your app as a process that receives signals. Running your server directly (`node server.js`, `./api`) is the most reliable; wrappers and process managers can swallow the signal.

## Serving static files

To serve static files, run a web server as a service. For example, with `npx serve`:

```hcl specific.hcl theme={null}
build "spa" {
  base = "node"
  command = "npm run build"
}

service "frontend" {
  build = build.spa
  command = "npx serve dist -l $PORT"

  endpoint {
    public = true
  }

  env = {
    PORT = port
  }

  dev {
    command = "npm run dev"  # Use your framework's dev server
  }
}
```

## Production

Once deployed, stream logs, inspect metrics, and scale services from the [dashboard](https://dashboard.specific.dev). For querying logs and metrics from the CLI, see [Observability](/guides/observability).
