# Deploying the Relay client as a container

> Deploy the UiPath Relay client as a container image using Podman or Kubernetes to establish secure outbound tunnels in containerized environments.

Run the Relay client as a container image to establish secure outbound tunnels to Test Cloud from containerized environments. Before you begin, [configure a Relay Group](configuring-relay-group.md) and have the client configuration string ready from the Relay UI.

## Prerequisites

- Container runtime: Podman, Docker, or a Kubernetes cluster.
- Relay client container image: `registry.uipath.com/relay-client:<tag>`.
- A base64-encoded configuration file generated from the Relay UI.
- License agreement acceptance: set `LICENSE_AGREEMENT=accept` as an environment variable, or append `--accept-license-agreement` to the start command.
- (Optional) A custom CA certificate if your organization uses enterprise PKI.

For hardware and network requirements, refer to [Deploying the Relay client](deploying-relay-client.md).

## Step 1: Get the configuration

1. Open the Relay UI dashboard.
2. Create or copy your relay configuration.
3. Save the base64-encoded configuration string provided by the UI.

## Step 2: Configure environment variables

### Custom CA certificate

If your organization uses a corporate or self-signed CA, set the following variables together before starting the container:

| Variable | Purpose | Required |
|---|---|---|
| `RELAY_CUSTOM_CA_PATH` | Path to the custom CA certificate | No |
| `RELAY_CA_BUNDLE_PATH` | Path where the merged CA bundle is written | Yes, if using custom CA |

The Relay client merges the custom CA with the system certificate bundle before establishing any TLS connections. Both variables must be set together.

### Proxy

To route outbound traffic through a proxy:

| Variable | Purpose | Required |
|---|---|---|
| `HTTP_PROXY` and `HTTPS_PROXY` | Proxy URL | No |
| `NO_PROXY` | Comma-separated hostnames, domains, or IP addresses that bypass the proxy | No |

## Step 3: Deploy

### Podman

For high availability, run two containers on separate nodes with distinct names. Replace `<RELAY_ID>` with the actual ID from the Relay UI — for example, run `relay1-<RELAY_ID>` on host1 and `relay2-<RELAY_ID>` on host2.

**Quick start:**

```shell
# Create a config file with the base64-encoded content from the Relay UI
echo "YOUR_BASE64_CONFIG_HERE" > /tmp/relay_config

# Run the container
podman run -it --name relay1-<RELAY_ID> --hostname relay1-<RELAY_ID> --rm \
  --read-only --read-only-tmpfs \
  -v /tmp/relay_config:/relay.config.b64enc:z \
  -e LICENSE_AGREEMENT=accept \
  registry.uipath.com/relay-client:<tag> \
  start --config-file /relay.config.b64enc --accept-license-agreement
```

**With a custom CA certificate:**

```shell
podman run -it --name relay1-<RELAY_ID> --hostname relay1-<RELAY_ID> --rm \
  --read-only --read-only-tmpfs \
  -v /tmp/relay_config:/relay.config.b64enc:z \
  -v /tls/custom-ca.crt:/custom-ca.crt:z \
  -v /tmp/writable:/writable:z \
  -e RELAY_CUSTOM_CA_PATH=/custom-ca.crt \
  -e RELAY_CA_BUNDLE_PATH=/writable/merged-ca.crt \
  registry.uipath.com/relay-client:<tag> \
  start --config-file /relay.config.b64enc --accept-license-agreement
```

**Start command options:**

| Option | Description | Example |
|---|---|---|
| `--config` | Inline base64 configuration string | `--config "base64string..."` |
| `--config-file` | Path to the configuration file | `--config-file /relay.config.b64enc` |
| `--log-level` | Logging verbosity: `trace`, `debug`, `info`, `warn`, or `error` | `--log-level debug` |
| `--heartbeat-interval` | Heartbeat interval in seconds (minimum: 10) | `--heartbeat-interval 10` |
| `--reconnect-interval` | Reconnect interval in seconds (minimum: 1800) | `--reconnect-interval 1800` |

### Kubernetes

**Create secrets:**

```shell
# Configuration secret
kubectl create secret generic relay-config \
  --from-file=relay.conf=/tmp/relay_config

# Custom CA certificate secret (optional)
kubectl create secret generic custom-ca \
  --from-file=custom-ca.crt=./custom-ca.crt

# Headless service for the StatefulSet
kubectl create service clusterip relay-client-<RELAY_ID> --clusterip="None"
```

**Deploy a StatefulSet:**

Use a StatefulSet when hostname restrictions are enforced. StatefulSets provide stable, predictable hostnames (`relay-client-0`, `relay-client-1`, and so on), which the Relay service uses to identify and validate clients.

```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: relay-client-<RELAY_ID>
spec:
  serviceName: relay-client-<RELAY_ID>
  replicas: 2
  selector:
    matchLabels:
      app: relay-client-<RELAY_ID>
  template:
    metadata:
      labels:
        app: relay-client-<RELAY_ID>
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - relay-client-<RELAY_ID>
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: relay
        image: registry.uipath.com/relay-client:<tag>
        args:
        - start
        - --config-file=/config/relay.conf
        - --accept-license-agreement
        - --log-level=info
        - --heartbeat-interval=30
        env:
        - name: RELAY_CUSTOM_CA_PATH
          value: "/tls/custom-ca.crt"
        - name: RELAY_CA_BUNDLE_PATH
          value: "/writable/merged-ca.crt"
        imagePullPolicy: IfNotPresent
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - ALL
          privileged: false
          readOnlyRootFilesystem: true
          runAsGroup: 1001
          runAsNonRoot: true
          runAsUser: 1001
        readinessProbe:
          httpGet:
            path: /healthz
            port: 9090
          initialDelaySeconds: 5
          timeoutSeconds: 1
          periodSeconds: 3
          successThreshold: 1
          failureThreshold: 2
        resources:
          requests:
            cpu: 50m
            memory: 100Mi
        volumeMounts:
        - name: relay-config
          mountPath: /config/relay.conf
          subPath: relay.conf
          readOnly: true
        - mountPath: /writable
          name: writable
        - name: custom-ca
          mountPath: /tls/custom-ca.crt
          subPath: custom-ca.crt
          readOnly: true
      volumes:
      - name: relay-config
        secret:
          secretName: relay-config
      - name: writable
        emptyDir: {}
      - name: custom-ca
        secret:
          secretName: custom-ca
      restartPolicy: Always
      terminationGracePeriodSeconds: 30
```

**Verify the deployment:**

```shell
kubectl get statefulset relay-client-<RELAY_ID>
```

## Operations

### Configuration details

The configuration file must contain the base64-encoded JSON string generated by the Relay UI. On startup, the Relay client reads the configuration, decodes it, validates it, and connects to the specified relay service endpoint.

- **First run:** Configuration is stored encrypted in the data directory.
- **Subsequent runs:** The encrypted configuration is automatically decrypted and used.
- **Config file changes:** Require a container restart to take effect.

### Heartbeat interval

The heartbeat keeps idle TCP connections alive. Lower the interval if your firewall, proxy, or network address translation (NAT) drops idle connections before 30 seconds:

```shell
--heartbeat-interval=30    # Default
--heartbeat-interval=10    # For aggressive firewall or NAT environments
```

### Reconnect interval

Proactive reconnect re-establishes the connection on a fixed schedule. Use this in environments where a proxy or load balancer has an idle-connection timeout:

```shell
--reconnect-interval=0     # Disabled (default)
--reconnect-interval=1800  # Reconnect every 30 minutes (minimum)
```

### Accessing logs

```shell
# Podman
podman logs -f relay1

# Kubernetes (current run)
kubectl logs -f relay-client-0

# Kubernetes (previous run, if the container restarted)
kubectl logs relay-client-0 --previous
```

## Security

Apply the following security settings in your container manifest:

- `readOnlyRootFilesystem: true` — prevents modification of the container filesystem.
- `runAsNonRoot: true` — runs the process as a non-root user.
- `allowPrivilegeEscalation: false` — prevents privilege escalation.
- `capabilities.drop: [ALL]` — drops all Linux capabilities.
- `privileged: false` — disables privileged mode.

Store relay configuration in Kubernetes secrets and use role-based access control (RBAC) to restrict secret access. Do not embed the base64 configuration in the container image or pass it as a plain environment variable.

## Troubleshooting

| Symptom | Cause | Resolution |
|---|---|---|
| `license agreement not accepted` on startup | License flag or variable not set | Add `--accept-license-agreement` to the start command, or set `LICENSE_AGREEMENT=accept` |
| Configuration file not found | Incorrect volume mount path or secret | Run `kubectl describe secret relay-config` and `kubectl describe pod <pod-name>` to verify mounts |
| Cannot connect to relay service | Network or firewall issue | Check pod logs with `kubectl logs <pod-name>` and verify outbound connectivity to `<region>-relay.uipath.com:443` |
| Custom CA merge failed | CA environment variables not both set | Set both `RELAY_CUSTOM_CA_PATH` and `RELAY_CA_BUNDLE_PATH` together |
| Hostname not recognized by relay service | Pod name is random (standalone Pod, not StatefulSet) | Use a StatefulSet instead of a standalone Pod |
| x509 certificate errors | Invalid or inaccessible CA certificate | Verify the certificate format with `openssl x509 -in custom-ca.crt -text -noout` and check file permissions |
