# Services

Services define how to run the code and connect it to other parts of the infrastructure, both in production and in local development. All configuration for the app is handled through environment variables, which the code should read from.

## Dynamic services

Example of running publicly exposed API and frontend web services:

```hcl
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. In development, this sets the working directory for the spawned process. In production, the working directory is set by the build's `root` in the Dockerfile.
- `endpoint` - Defines a network endpoint (see Endpoints below)
- `port` - Reference to the auto-assigned port (pass this to your server)

## Endpoints

Endpoints define how a service is accessible over the network.

### Single endpoint (most common)

```hcl
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

### Multiple named endpoints

A service can expose multiple endpoints on different ports:

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

  endpoint "main" {
    public = true
  }

  endpoint "admin" {}

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

When using multiple endpoints, you must 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
service "worker" {
  build = build.worker
  command = "./worker"

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

## Private inter-service communication

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

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

  endpoint {
    public = true
  }

  env = {
    PORT = port
  }
}

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

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

Available service reference attributes:

- `service.<name>.private_url` - Internal URL without scheme (e.g., `localhost:3001`). Does NOT include `http://` or `https://`.
- `service.<name>.host` - Host only (e.g., `localhost`)
- `service.<name>.port` - Port only (e.g., `3001`)
- `service.<name>.public_url` - Public URL without scheme (e.g., `my-app.specific.app` in production, `localhost:3001` in dev). Does NOT include `http://` or `https://`. Only available for endpoints with `public = true`.

**Important:** `public_url` and `private_url` do NOT include a scheme. In production, services are served over HTTPS. In local development, services use plain HTTP. When you need to construct a full URL, use string interpolation with the correct scheme for each environment. Use the `dev { env = {} }` block to override with `http://` locally:

```hcl
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}"
    }
  }
}
```

For services with multiple named endpoints, you must specify the endpoint:

```hcl
env = {
  MAIN_URL = service.api.endpoint.main.private_url
  ADMIN_URL = service.api.endpoint.admin.private_url
  PUBLIC_API = service.api.endpoint.main.public_url
}
```

### Public inter-service communication

The private communication does not work in the case of a frontend or client service needing to speak with a backend service, as `private_url` is not externally reachable. In that case, the backend service MUST expose a public endpoint. The frontend or client service can then reference that endpoint using `public_url`:

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

  endpoint {
    public = true
  }

  env = {
    PORT = port
  }
}

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}"
    }
  }
}
```

## Monorepo support

For 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
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:

```hcl
service "docs" {
  root = "packages/docs"

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

## Workers

Background processes that don't expose HTTP endpoints.

```hcl
build "worker" {
  base = "node"
  command = "npm run build"
}

service "worker" {
  build = build.worker
  command = "npm run worker"
}
```

## Pre-existing images

Services can use a pre-existing container image instead of a build. This is useful for running third-party services or any pre-built container:

```hcl
service "redis" {
  image = "redis:7"
  command = "redis-server --port $PORT"

  endpoint {}

  env = {
    PORT = port
  }
}
```

- `image` - Container image reference (e.g., `redis:7`, `ghcr.io/org/image:tag`)
- `image` and `build` are mutually exclusive — use one or the other
- Services with `image` skip the build step entirely
- Deploy hooks (`pre_deploy`, `post_deploy`) work with image-based services
- Use `dev.command` for local development:

```hcl
service "redis" {
  image = "redis:7"
  command = "redis-server --port $PORT"

  endpoint {}

  env = {
    PORT = port
  }

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

You can mix build-based and image-based services in the same project.

## Environment variables

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

  endpoint {
    public = true
  }

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

### String interpolation

You can embed references inside strings to compose values:

```hcl
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 — they must be used as standalone values.

Since `public_url` and `private_url` do not include a scheme, you should use `https://` in the top-level `env` (for production) and override with `http://` in the `dev { env = {} }` block (for local development). The `dev` env block is merged with the top-level env, so you only need to override the variables that differ.

## Service dev configuration

Override how a service runs in development. If the referenced build has no `dev` block, it is skipped.

```hcl
build "nextjs" {
  base = "node"
  command = "npm run build"
  # No dev block = skipped in development
}

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
  }
}
```

## Dev-only services

Services that only run during local development, not deployed to production. Useful for running local versions of external cloud services like Temporal, LocalStack, or mock servers.

A service is dev-only if it has a `dev.command` but no top-level `command`:

```hcl
service "temporal" {
  dev {
    command = "temporal server start-dev"
  }
}

service "worker" {
  build = build.worker
  command = "node worker.js"

  env = {
    TEMPORAL_URL = secret.temporal_url  # Production URL from secret
  }

  dev {
    command = "node --watch worker.js"
    env = {
      TEMPORAL_URL = service.temporal.private_url  # Override with local URL in dev
    }
  }
}

secret "temporal_url" {}
```

Dev-only services:

- Cannot have a `build` reference (they run arbitrary commands)
- Are automatically excluded from production deployment
- Can define endpoints that other services reference during development

## Deploy hooks

Run commands before or after deploying a service.

**Database migrations should use `pre_deploy` hooks.** This ensures migrations complete before the new code starts, and if they fail, the deployment is automatically aborted.

```hcl
build "api" {
  base = "node"
  command = "npm run build"
}

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 (e.g., database migrations)
- `post_deploy` - Runs after the service is deployed (e.g., cache warming)

Hooks run as Kubernetes Jobs using the same container and environment variables as the service. If a hook fails, the deployment is aborted.

### Database migrations

Always add a `pre_deploy` hook for database migrations. The hook runs **before** new service instances start, ensuring:

- The database schema is updated before new code runs
- If migrations fail, the deployment is aborted (no broken state)
- Migrations have access to `DATABASE_URL` and other service env vars

In development, use `specific exec ...` to run migrations/push database changes whenever the schema is changed. Do so automatically unless the user instructs otherwise. For example: `specific exec api -- npm run db:push`.

## Serving static files

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

```hcl
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
  }
}
```

---

Related topics:

- Run `specific docs builds` for build configuration
- Run `specific docs exec` for running one-off commands during development
- Run `specific docs postgres` for database configuration
