---
title: 'Tutorial: Secure OpenClaw with CloudConnexa (Docker — Advanced)'
canonical: https://openvpn.net/cloud-docs/tutorials/use-case-tutorials/remote-access---ztna/tutorial--secure-openclaw-with-cloudconnexa--docker---advanced/
product: CloudConnexa Docs
description: Deploy OpenClaw securely with Docker and CloudConnexa using a private tunnel, HTTPS reverse proxy, and hardened configuration without exposing public ports.
---

# Tutorial: Secure OpenClaw with CloudConnexa (Docker — Advanced)

## Overview

This guide shows how to securely deploy OpenClaw using Docker and make it accessible through CloudConnexa without exposing any public ports.

This setup uses:

- **CloudConnexa Host Connector** — provides private, always-on access.
- **Caddy reverse proxy** — enables HTTPS access.
- **Docker Compose** — runs OpenClaw and supporting services.
- **Security hardening** — based on OpenClaw best practices.

OpenClaw runs inside a container and is only accessible through CloudConnexa.

## Architecture

```
[Authorized CloudConnexa Users]
        ↓ (Tunnel)
[CloudConnexa]
        ↓
[Host Connector on your VM] ← always-on tunnel, stable IP (e.g., 100.96.x.x)
        ↓
[Caddy Reverse Proxy] ← HTTPS/TCP443, shared network namespace with OpenClaw gateway
        ↓
[OpenClaw Gateway] ← localhost:18789 inside container, token auth + device pairing
        ↓
[Claude / OpenAI / ... APIs]
```

## Before you begin

Ensure you have the following:

- A Linux VM (Ubuntu 22.04/24.04 recommended) with at least 2 GB RAM (4 GB recommended) and 10 GB disk space.
- Root or sudo access.
- A CloudConnexa account.
- AI provider API key (OpenAI, Anthropic, etc.)

## Step 1: Install Docker

1. Install Docker Engine using the official documentation: [Docker Install](https://docs.docker.com/engine/install/).
2. Verify the installation:

   ```
   docker --version
   docker compose version
   ```

   - Expected results:

     ```
     docker --version
     # Should show: Docker version 20.10+ or higher

     docker compose version
     # Should show: Docker Compose version v2.x.x or higher
     ```

## Step 2: Create a host in CloudConnexa

The CloudConnexa host connector installs directly on your application server, enabling access to applications running on that machine.

1. Sign in to the CloudConnexa Administration Portal using your WPC's **Cloud ID** address (for example, `yourcompany.openvpn.com`).
2. Navigate to **Hosts → Hosts**.
3. Click **Add Host**.
4. Configure:
   - Name: `OpenClaw`
   - Domain Name: `openclaw.local` (CloudConnexa will automatically resolve this domain to the Connector's tunnel IP for users connected to the WPC.)
   - Connector name
   - Region
5. Click **Finish**.

   > **Important**
   >
   > The domain name you choose here (e.g., `openclaw.local`) will be used throughout this guide. If you use a different domain, replace `openclaw.local` in all subsequent steps.

## Step 3: Deploy the host connector

1. Open the new Host, and click the **Connectors** tab.
2. Click **Deploy → Deploy Connector**.
3. Select your Linux distribution running on your VM.
4. Run the provided script on your VM's terminal.
5. Paste the token shown under "Generate Token" (located below the script).
6. Click **Next**.
7. Verify that the Connector shows "Connected."
8. Record the tunnel IP shown next to the Connector (e.g., `100.96.1.50`) for a later step.

## Step 4: Configure applications (optional)

Define which services are exposed so that granular access control can be configured.

1. Open the new Host, and click the **Applications** tab.
2. Click **Add Application**.
3. Add an application for HTTPS:
   - **Name:** OpenClaw HTTPS
   - **Protocol:** TCP
   - **Port:** 443
4. (Optional) Add an application for HTTP (useful for `curl` testing from the tunnel):
   - **Name:** OpenClaw HTTP
   - **Protocol:** TCP
   - **Port:** 80
5. Save and ensure the appropriate Access Groups have permission to reach these Applications.

## Step 6: Prepare the project directory

1. Prepare the project directory on Docker:

   ```
   mkdir -p ~/openclaw-docker
   cd ~/openclaw-docker
   mkdir -p config workspace
   ```
2. Set ownership to uid 1000 (the `node` user inside the container) and lock down permissions:

   ```
   sudo chown -R 1000:1000 config workspace
   chmod 700 config workspace
   ```

   Keep `~/openclaw-docker/config/` and `~/openclaw-docker/workspace/` at `700` to prevent other users or processes from reading tokens and credentials.

## Step 7: Create the Caddyfile

Caddy shares the gateway container's network namespace, so it can reach the gateway at `localhost:18789`. It serves two listeners:

- **Port 80 (HTTP):** For local `curl` testing from the host.
- **Port 443 (HTTPS):** For browser access through CloudConnexa. Uses `tls internal` to auto-generate a self-signed certificate for the hostname.

  > **Note**
  >
  > The OpenClaw Control UI requires a secure context (HTTPS or localhost) for device identity, so this isn't optional for remote access.

1. Create the Caddyfile in the project directory:

   ```
   tee Caddyfile > /dev/null << 'EOF'
   :80 {
       reverse_proxy localhost:18789
   }

   openclaw.local {
       tls internal
       reverse_proxy localhost:18789
   }
   EOF
   ```

   Replace `openclaw.local` with your Host's domain name if different.

## Step 8: Create docker-compose.yml

This Compose file is built from the official OpenClaw Docker documentation and the upstream `docker-compose.yml` in the OpenClaw repository, with Caddy added as a reverse proxy.

1. Detect the CloudConnexa tunnel IP and store it for use in the compose file:

   ```
   TUNNEL_IP=$(sudo openvpn3 sessions-list | grep 'tun' | awk -F ':' '{print $3}') && \
   TUNNEL_IP=$(ip -4 addr show $TUNNEL_IP | grep -oP '(?:\b\.?(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){4}' | head -1)

   echo "Detected tunnel IP: $TUNNEL_IP"
   ```
2. Verify this matches the tunnel IP shown in the CloudConnexa Admin portal ([Step 3: Deploy the host connector](https://openvpn.net/cloud-docs/tutorials/use-case-tutorials/remote-access---ztna/tutorial--secure-openclaw-with-cloudconnexa--docker---advanced/#step-3--deploy-the-host-connector-321478)). If it doesn't, set it manually:

   ```
   TUNNEL_IP='100.96.1.50'
   ```
3. Create the `docker-compose.yml` file using the detected tunnel IP:

   ```
   tee docker-compose.yml > /dev/null << EOF
   services:
     openclaw-gateway:
       image: ghcr.io/openclaw/openclaw:latest
       container_name: openclaw-gateway
       restart: unless-stopped
       init: true
       ports:
         - "127.0.0.1:80:80"        # HTTP — local access for curl testing
         - "$TUNNEL_IP:443:443"     # HTTPS — CloudConnexa tunnel access
       volumes:
         - ./config:/home/node/.openclaw
         - ./workspace:/home/node/.openclaw/workspace
       environment:
         HOME: /home/node
         TERM: xterm-256color
       healthcheck:
         test:
           [
             "CMD",
             "node",
             "-e",
             "fetch('http://127.0.0.1:18789/healthz').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
           ]
         interval: 30s
         timeout: 5s
         retries: 5
         start_period: 20s

     openclaw-cli:
       image: ghcr.io/openclaw/openclaw:latest
       container_name: openclaw-cli
       profiles: ["cli"]
       network_mode: "service:openclaw-gateway"
       cap_drop:
         - NET_RAW
         - NET_ADMIN
       security_opt:
         - no-new-privileges:true
       stdin_open: true
       tty: true
       init: true
       entrypoint: ["node", "dist/index.js"]
       volumes:
         - ./config:/home/node/.openclaw
         - ./workspace:/home/node/.openclaw/workspace
       environment:
         HOME: /home/node
         TERM: xterm-256color
         BROWSER: echo
       depends_on:
         - openclaw-gateway

     caddy:
       image: caddy:latest
       container_name: caddy
       restart: unless-stopped
       network_mode: "service:openclaw-gateway"
       volumes:
         - ./Caddyfile:/etc/caddy/Caddyfile:ro
         - caddy_data:/data
       depends_on:
         - openclaw-gateway

   volumes:
     caddy_data:
   EOF
   ```

   > **Tip**
   >
   > **What's happening here and why:**
   >
   > - **Security:** Uses official GHCR non-root images (`node` user) and CLI hardening (`cap_drop`, `no-new-privileges`).
   > - **Networking:** Caddy and CLI share the Gateway's network namespace via `network_mode`. This allows localhost communication without exposing internal ports to the host or public internet.
   > - **Restricted Access:** Ports are bound strictly to `127.0.0.1` (local testing) and `$TUNNEL_IP` (CloudConnexa). No public `0.0.0.0` binding.
   > - **Persistence:** `caddy_data` volume preserves SSL certificates and CA state.
   > - **Health:** Standardized `/healthz` probes ensure automated container recovery.

## Step 9: Run onboarding and start services

### 9.1 Run onboarding

1. Run the onboarding wizard:

   ```
   docker compose run --rm openclaw-cli onboard
   ```
2. Follow the prompts to:
   - Select your AI provider.
   - Enter your API key.
   - Set onboarding mode to Manual.
   - Use default values for Config handling.
   - Set Gateway mode to Local.
   - Leave Gateway bind as default (loopback).

     > **Note**
     >
     > You don't need to set the gateway bind to the CloudConnexa tunnel IP. The gateway stays on loopback inside the container. Caddy shares the same network namespace and can reach it directly. Caddy then serves HTTPS on port 443, which is bound to the tunnel IP on the host.

### 9.2 Retrieve the gateway token

The gateway token is displayed at the end of the onboarding wizard. Copy it for use later.

If you need to find it again:

1. Run:

   ```
   openclaw dashboard --no-open
   ```
2. Copy the token value from:

   ```
   Dashboard URL: http://127.0.0.1:18789/#token=
   ```

### 9.3 Start all services

Start the OpenClaw gateway and Caddy reverse proxy.

1. Start all services defined in the Docker Compose file and run them in the background:

   ```
   docker compose up -d
   ```
2. Verify the services are running:

   ```
   docker compose ps
   ```

   - Expected output:

     ```
     NAME                IMAGE                              STATUS
     caddy               caddy:latest                       Up
     openclaw-gateway    ghcr.io/openclaw/openclaw:latest   Up (healthy)
     ```

### 9.4 Test local access

Verify that Caddy is correctly proxying requests to the OpenClaw gateway.

1. Confirm the reverse proxy is working:

   ```
   curl -s http://127.0.0.1/
   ```

   You should see the OpenClaw Control UI HTML (starting with `<!doctype html>`).

   > **Note**
   >
   > The `#token=...` part of the dashboard URL is a browser-side fragment. It's not sent to the server, so you don't need it for a `curl` test. You'll use the full URL with the token when accessing the dashboard from a browser.

## Step 10: Apply security hardening

This step applies recommended security settings for running OpenClaw behind CloudConnexa and a reverse proxy.

### 10.1 Enable gateway authentication (token mode)

Gateway auth is required by default; if no token is configured, the gateway refuses WebSocket connections (fail-closed).

1. Verify the current authentication mode:

   ```
   Gateway auth is required by default; if no token is configured, the gateway refuses WebSocket connections (fail-closed).
   ```

   Expected result: `token`.
2. If not set, enable token authentication:

   ```
   docker compose run --rm openclaw-cli config set gateway.auth.mode token
   ```

### 10.2 Configure trusted proxy

Since Caddy is acting as a reverse proxy, OpenClaw must trust it to correctly identify client requests.

1. Allow traffic forwarded from localhost (Caddy) and ensure client IPs and requests are handled correctly:

   ```
   docker compose run --rm openclaw-cli config set gateway.trustedProxies '["127.0.0.1"]' --strict-json
   ```

### 10.3 Configure allowed origins

The Control UI rejects requests from origins not in its allowlist.

1. Configure the allowed HTTPS domain and tunnel IP used by CloudConnexa:

   ```
   TUNNEL_IP=$(sudo openvpn3 sessions-list | grep 'tun' | awk -F ':' '{print $3}') && \
   TUNNEL_IP=$(ip -4 addr show $TUNNEL_IP | grep -oP '(?:\b\.?(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){4}' | head -1)

   openclaw config set gateway.controlUi.allowedOrigins "[\"https://$TUNNEL_IP\",\"https://openclaw.local\"]" --strict-json
   ```

   Replace `openclaw.local` with your Host domain if different.

### 10.4 (Optional) Disable mDNS discovery

The gateway broadcasts its presence via mDNS on the local network by default, which can leak operational details like filesystem paths and SSH port availability.

1. Disable local network discovery:

   ```
   docker compose run --rm openclaw-cli config set discovery.mdns.mode off
   ```

### 10.5 (Optional) Enable log redaction

Redaction prevents sensitive information from appearing in logs.

1. Mask sensitive tool output in logs:

   ```
   docker compose run --rm openclaw-cli config set logging.redactSensitive tools
   ```

### 10.6 Apply configuration changes

1. Restart all services to apply the updated settings:

   ```
   docker compose down && docker compose up -d
   ```

   > **Note**
   >
   > Caddy and the CLI share the gateway's network namespace, so restarting just the gateway would take them down too.

## Step 11: (Optional) Configure firewall hardening

Docker-published container ports bypass the host's INPUT iptables rules. Even though we bind ports to specific IPs, it's good practice to enforce rules in the DOCKER-USER chain as defense-in-depth.

1. Add firewall rules that restrict container traffic, allow localhost and CloudConnexa tunnel traffic, and block other inbound connections:

   ```
   sudo tee -a /etc/ufw/after.rules > /dev/null << 'EOF'
   *filter
   :DOCKER-USER - [0:0]
   -A DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
   -A DOCKER-USER -s 127.0.0.0/8 -j RETURN
   -A DOCKER-USER -s 100.96.0.0/11 -j RETURN
   -A DOCKER-USER -m conntrack --ctstate NEW -j DROP
   -A DOCKER-USER -j RETURN
   COMMIT
   EOF
   ```
2. Reload firewall rules:

   ```
   sudo ufw reload
   sudo iptables -S DOCKER-USER
   ```

   > **Note**
   >
   > If you also need local network access (e.g., for SSH management), add `10.0.0.0/8`, `172.16.0.0/12`, or `192.168.0.0/16` as needed.
3. Verify rules (optional) by displaying active Docker firewall rules:

   ```
   sudo iptables -S DOCKER-USER
   ```

## Step 12: Access OpenClaw remotely

1. Connect to CloudConnexa.
2. In a web browser, navigate to:

   ```
   https://openclaw.local/#token=<your-token>
   ```

   Replace `openclaw.local` with your Host domain if different.

   Replace `<your-token>` with the gateway token from [Step 1: Install OpenClaw](https://openvpn.net/cloud-docs/tutorials/use-case-tutorials/remote-access---ztna/tutorial--secure-openclaw-on-a-linux-server-using-cloudconnexa-and-caddy/#step-1--install-openclaw).
3. Accept the certificate warning. (Caddy uses a self-signed internal certificate.)

## Step 13: Approve the device

On first connection from a new browser, the Control UI will show "pairing required". This is expected; the gateway requires device approval as an additional layer of security on top of token authentication.

1. Connect to your server.
2. List pending device requests:

   ```
   openclaw devices list
   ```
3. Note the device ID you'll need to approve.
4. Approve the device:

   ```
   openclaw devices approve <requestId>
   ```

   Replace `<requestId>` with the ID from the device list.
5. Refresh the browser.
6. The Control UI should now load.

   > **Note**
   >
   > Each browser/device needs approval once. After approval, it's remembered across sessions.

## Managing your deployment

### Start / Stop

```
docker compose up -d          # Start all services
docker compose stop           # Stop (keep data)
docker compose down           # Stop and remove containers
```

> **Important**
>
> Always use `docker compose down && docker compose up -d` instead of `docker compose restart openclaw-gateway` when restarting. Caddy and the CLI share the gateway's network namespace, so restarting just the gateway takes them all down.

### View logs

```
docker compose logs openclaw-gateway       # Gateway logs
docker compose logs caddy                  # Caddy logs
```

### Update OpenClaw

Always back up before updating.

```
docker compose pull                        # Pull latest images
docker compose up -d --force-recreate      # Recreate containers
```

### Run security audit

```
docker compose run --rm openclaw-cli security audit --deep
```

## Troubleshooting

### Control UI requires device identity

#### Cause

You're accessing OpenClaw over HTTP or using a hostname that doesn't match your Caddy configuration.

#### Resolution

- Always access the dashboard using HTTPS:

  ```
  https://openclaw.local
  ```
- Don't use `http://` or the raw tunnel IP.
- Verify the hostname in your Caddy file matches your CloudConnexa domain.
- Verify your browser accepts the self-signed certificate.

### Origin not allowed

#### Cause

The request origin isn't included in the allowed origins configuration.

#### Resolution

Update the allowed origins settings.

### Pairing required

#### Cause

This is expected behavior when connecting from a new device or browser.

#### Resolution

Follow the steps to approve the device.

### Gateway not accessible via CloudConnexa

#### Cause

The Connector, routing, or access permissions may not be correctly configured.

#### Resolution

1. Verify the **Host Connector** shows "Connected" in the CloudConnexa admin portal.
2. Test connectivity from a device connected to the WPC:

   ```
   curl -sk https://openclaw.local/
   ```
3. Confirm that your user/device is assigned to an Access Group that has permission to reach the OpenClaw application.

### Caddy reverse proxy not working

#### Cause

Caddy may not be correctly configured or running.

#### Resolution

1. Verify the Caddyfile exists and is correct:

   ```
   cat Caddyfile
   ```
2. Text connectivity from inside the container:

   ```
   docker compose exec caddy curl http://localhost:18789/healthz
   ```

   Expected output:

   ```
   {"ok":true,"status":"live"}
   ```
3. Restart all services:

   ```
   docker compose down && docker compose up -d
   ```

### Permission errors (EACCES)

#### Cause

Incorrect file ownership or permissions for mounted volumes.

#### Resolution

1. Reset permissions:

   ```
   sudo chown -R 1000:1000 config/ workspace/
   chmod 700 config/ workspace/
   ```

## Related Resources

- OpenClaw Docker installation: [Docker - OpenClaw](https://docs.openclaw.ai/install/docker)
- OpenClaw Security documentation: [Security - OpenClaw](https://docs.openclaw.ai/gateway/security)
- OpenClaw upstream docker-compose.yml: [openclaw/docker-compose.yml at main · openclaw/openclaw](https://github.com/openclaw/openclaw/blob/main/docker-compose.yml)
- OpenClaw upstream Dockerfile: [openclaw/Dockerfile at main · openclaw/openclaw](https://github.com/openclaw/openclaw/blob/main/Dockerfile)
- CloudConnexa Hosts documentation: [About Hosts](https://openvpn.net/cloud-docs/owner/hosts/hosts---hosts/about-hosts.md)
- CloudConnexa Host Connectors: [About Host Connectors](https://openvpn.net/cloud-docs/owner/hosts/hosts---connectors/about-host-connectors.md)
- Docker Engine installation: [Install](https://docs.docker.com/engine/install/)
- Caddy documentation: [Welcome - Caddy Documentation](https://caddyserver.com/docs/)

---

Prev: [Tutorial: Secure OpenClaw on a Linux Server Using CloudConnexa and Caddy](https://openvpn.net/cloud-docs/tutorials/use-case-tutorials/remote-access---ztna/tutorial--secure-openclaw-on-a-linux-server-using-cloudconnexa-and-caddy.md)
Next: [SaaS Security](https://openvpn.net/cloud-docs/tutorials/use-case-tutorials/saas-security.md)
Index: https://openvpn.net/cloud-docs/llms.txt
