# Deploying a Private Location

To deploy a Private Location, review the [prerequisites](<https://docs.escape.tech/documentation/platform/private-location/prerequisites/index.md>) and retrieve your API key. Creating, editing, and deleting locations requires the global Admin permission.

To get your API key, go to your [profile page](<https://app.escape.tech/user/profile/>). Now save it in your environment variables as `ESCAPE_API_KEY=...`.

## Choose a Transport

The agent opens a single outbound tunnel to Escape and receives scan jobs through it. Pick the transport your network allows. Both transports carry the same tunnel and authentication, so scans behave identically. In both cases the agent also needs outbound HTTPS `443` to `public.escape.tech`. See [Firewall Configuration](<https://docs.escape.tech/documentation/platform/firewall/#private-location-tunnel>) for the full list of endpoints and how to test them.

**SSH**

The default. Requires outbound TCP `2222` to `private-location.escape.tech`.

**HTTPS**

Requires outbound HTTPS `443` to `location.escape.tech`. Set `ESCAPE_TRANSPORT=auto`.

The agent connects with WebSocket and falls back to HTTP long-polling when your network strips or rejects the WebSocket `Upgrade`. Set `ESCAPE_TRANSPORT=wss` (WebSocket only) or `ESCAPE_TRANSPORT=https` (long-polling only) to force one mode.

- If a TLS-inspecting proxy re-signs the traffic, add its CA as described in [SSL Configuration](<https://docs.escape.tech/documentation/platform/private-location/ssl/index.md>).
- A [frontend proxy](<https://docs.escape.tech/documentation/platform/private-location/proxy/index.md>) only needs to allow `CONNECT` to `location.escape.tech:443`. It doesn't need to support WebSocket.
- Long-polling holds each request open for about 20 seconds. Proxies that buffer responses until they complete break it.

## Using the Escape CLI

If you've installed the [Escape CLI](<https://docs.escape.tech/documentation/automate/cli/index.md>), you can start a Private Location with:

**SSH**

```bash
escape-cli locations start location-name
```

**HTTPS**

```bash
ESCAPE_TRANSPORT=auto escape-cli locations start location-name
```

This starts a new Private Location on your computer and automatically connects it to your Escape account. If the location doesn't exist, it's created automatically. You can see its status on the [Private Locations page](<https://app.escape.tech/organization/private-locations/>).

## Using Helm

```bash
helm repo add escape-cli https://escape-technologies.github.io/cli/
helm repo update
```

**SSH**

```bash
helm install escape-private-location escape-cli/private-location \
  --set ESCAPE_API_KEY="${ESCAPE_API_KEY}"
```

**HTTPS**

```bash
helm install escape-private-location escape-cli/private-location \
  --set ESCAPE_API_KEY="${ESCAPE_API_KEY}" \
  --set 'container.env[0].name=ESCAPE_TRANSPORT' \
  --set 'container.env[0].value=auto'
```

The Helm chart sets `HEALTH_CHECK_PORT=8080`, `ESCAPE_CLI_RESTART_INTERVAL=24h`, and a Kubernetes liveness probe on `GET /health`. The health check is configured by default. See [Environment Variables](<https://docs.escape.tech/documentation/platform/private-location/environment-variables/index.md>) for the full configuration reference.

The chart enables Kubernetes integration and creates cluster-wide read permissions by default. See [Kubernetes Integration and RBAC](<https://docs.escape.tech/documentation/platform/private-location/prerequisites/#kubernetes-integration-and-rbac>). To disable it, add `--set ESCAPE_K8S_INTEGRATION=false`.

For controlled upgrades, pin `container.image` to a released version tag. With the default `escapetech/cli:latest` and `imagePullPolicy: Always`, Helm deployments can pull a newer image when a container restarts, including after the scheduled restart. Docker Compose restarts don't automatically pull a newer image.

## Using Docker

You can start a Private Location with Docker by running:

**SSH**

```bash
docker run \
  -e ESCAPE_API_KEY="${ESCAPE_API_KEY}" \
  -e HEALTH_CHECK_PORT=8080 \
  -p 8080:8080 \
  -it --rm escapetech/cli:latest locations start location-name
```

**HTTPS**

```bash
docker run \
  -e ESCAPE_API_KEY="${ESCAPE_API_KEY}" \
  -e ESCAPE_TRANSPORT=auto \
  -e HEALTH_CHECK_PORT=8080 \
  -p 8080:8080 \
  -it --rm escapetech/cli:latest locations start location-name
```

Set `HEALTH_CHECK_PORT` so the container serves `GET /health`. Orchestrators such as Kubernetes use that endpoint to decide whether the application is healthy and whether the deployment can complete. See [Health Check](<#health-check>) for the response codes and probe timing.

The image entrypoint is `escape-cli`, so you pass only its arguments (`locations start location-name`), never the binary name. Every orchestrator that appends arguments to the entrypoint (Docker, Compose, Kubernetes `args`, Convox `command:`) follows the same rule.

## Using Podman

If you prefer Podman, you can use:

**SSH**

```bash
podman run \
  -e ESCAPE_API_KEY="${ESCAPE_API_KEY}" \
  -e HEALTH_CHECK_PORT=8080 \
  -p 8080:8080 \
  -it --rm escapetech/cli:latest locations start location-name
```

**HTTPS**

```bash
podman run \
  -e ESCAPE_API_KEY="${ESCAPE_API_KEY}" \
  -e ESCAPE_TRANSPORT=auto \
  -e HEALTH_CHECK_PORT=8080 \
  -p 8080:8080 \
  -it --rm escapetech/cli:latest locations start location-name
```

## Using Docker Compose

**SSH**

```yaml
---
services:
  private-location:
    image: escapetech/cli:latest
    restart: always
    command: locations start -v location-name
    ports:
      - "8080:8080"
    environment:
      - ESCAPE_API_KEY=<ESCAPE_API_KEY>
      - HEALTH_CHECK_PORT=8080
```

**HTTPS**

```yaml
---
services:
  private-location:
    image: escapetech/cli:latest
    restart: always
    command: locations start -v location-name
    ports:
      - "8080:8080"
    environment:
      - ESCAPE_API_KEY=<ESCAPE_API_KEY>
      - ESCAPE_TRANSPORT=auto
      - HEALTH_CHECK_PORT=8080
```

`/health` is served on the mapped port and returns `503` until the location is connected.

Compose's `restart: always` only reacts to the container process exiting, never to an unhealthy status.

Poll `/health` and restart the container when it stops returning `200`. Use the address that's reachable from wherever the poller runs:

| Poller location | Address |
| --- | --- |
| The Docker host | `http://localhost:8080/health` |
| Another service on the Compose network | `http://private-location:8080/health` |
| Another host | `http://<docker-host>:8080/health` |

`localhost` only works from the Docker host itself. From another container it resolves to that container, not to the Private Location.

If you want the restart handled for you, deploy on Kubernetes with the liveness probe below, or use the [Helm chart](<#using-helm>).

## Using Convox

Convox maps `command:` to the container arguments appended to the image entrypoint. Use the official image directly with the configuration below.

**SSH**

```yaml
services:
  private-location:
    image: escapetech/cli:latest
    command: locations start location-name
    port: 8080
    health:
      path: /health
      grace: 60
      interval: 30
      timeout: 5
    environment:
      - ESCAPE_API_KEY
      - HEALTH_CHECK_PORT=8080
```

**HTTPS**

```yaml
services:
  private-location:
    image: escapetech/cli:latest
    command: locations start location-name
    port: 8080
    health:
      path: /health
      grace: 60
      interval: 30
      timeout: 5
    environment:
      - ESCAPE_API_KEY
      - ESCAPE_TRANSPORT=auto
      - HEALTH_CHECK_PORT=8080
```

Don't put `escape-cli` in `command:`. The image entrypoint is already `escape-cli`, so `command: locations start location-name` runs `escape-cli locations start location-name`. Adding the binary name would run `escape-cli escape-cli locations start location-name` and fail.

`health` reuses the service `port`, so keep `HEALTH_CHECK_PORT` equal to it. Give the check a `grace` period: `/health` returns `503` until the tunnel is connected, and a grace that's too short makes Convox recycle the container before it ever connects.

Keep the health port accessible only to your health checker, rather than exposing it publicly. The health server also serves unauthenticated `POST /log` when you enable that endpoint.

Set `ESCAPE_API_KEY` as a Convox environment variable (`convox env set ESCAPE_API_KEY=...`) rather than committing it to `convox.yml`.

## Health Check

The Private Location starts an HTTP server only when `HEALTH_CHECK_PORT` is set. If the variable is empty, there's no `/health` endpoint.

The examples use port `8080` for the health server.

Once the server is up, query `http://<host>:<HEALTH_CHECK_PORT>/health`:

| HTTP status | Body | Meaning |
| --- | --- | --- |
| `200` | `OK` | The location is connected to Escape |
| `503` | `Not connected` | The process is running but not connected yet |

For a Kubernetes Deployment that doesn't use the Helm chart, set the same environment variables and probe as the chart:

**SSH**

```yaml
env:
  - name: HEALTH_CHECK_PORT
    value: "8080"
livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 60
  periodSeconds: 60
  failureThreshold: 60
```

**HTTPS**

```yaml
env:
  - name: HEALTH_CHECK_PORT
    value: "8080"
  - name: ESCAPE_TRANSPORT
    value: auto
livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 60
  periodSeconds: 60
  failureThreshold: 60
```

Use `/health` to detect a disconnected location and have your orchestrator restart it. See [Environment Variables](<https://docs.escape.tech/documentation/platform/private-location/environment-variables/#runtime-and-health>) for the restart and health settings.
