Skip to main content
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:
specific.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.
  • endpoint - defines a network endpoint (see below).
  • port - reference to the auto-assigned port; pass it to your server.
See Builds for the build block.

Endpoints

Endpoints define how a service is accessible over the network.

Single endpoint

specific.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.
Public services are served from a managed *.spcf.app URL by default. To attach your own domain, see Custom domains.

Multiple named endpoints

A service can expose multiple endpoints on different ports, each independently public or internal:
specific.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
  }
}
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:
specific.hcl
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.
specific.hcl
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:
specific.hcl
service "worker" {
  build = build.worker
  command = "./worker"

  env = {
    API_URL = service.api.private_url
  }
}
Available service reference attributes:
AttributeDescription
service.<name>.private_urlInternal URL without scheme (for example, localhost:3001 in dev). Only reachable by other services.
service.<name>.hostHost only.
service.<name>.portPort only.
service.<name>.public_urlPublic 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.
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:
env = {
  API_URL = "https://${service.api.public_url}"
}

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

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:
specific.hcl
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.
specific.hcl
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:
specific.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; 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.
specific.hcl
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:
specific.hcl
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 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:
specific.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.

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.
specific.hcl
service "worker" {
  build = build.worker
  command = "npm run worker"

  env = {
    DATABASE_URL = postgres.main.url
  }
}
For work on a schedule, use 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:
specific.hcl
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.
specific.hcl
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 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:
specific.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
  }
}

Production

Once deployed, stream logs, inspect metrics, and scale services from the dashboard. For querying logs and metrics from the CLI, see Observability.