Tutorial: Secure OpenClaw with CloudConnexa (Docker — Advanced)
Deploy OpenClaw securely with Docker and CloudConnexa using a private tunnel, HTTPS reverse proxy, and hardened configuration without exposing public ports.
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
Install Docker Engine using the official documentation: Docker Install.
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.
Navigate to Hosts → Hosts.
Click Add Host.
Configure:
Name:
OpenClawDomain Name:
openclaw.local(CloudConnexa will automatically resolve this domain to the Connector's tunnel IP for users connected to the WPC.)Connector name
Region
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, replaceopenclaw.localin all subsequent steps.
Step 3: Deploy the host connector
Open the new Host, and click the Connectors tab.
Click Deploy → Deploy Connector.
Select your Linux distribution running on your VM.
Run the provided script on your VM's terminal.
Paste the token shown under "Generate Token" (located below the script).
Click Next.
Verify that the Connector shows "Connected."
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.
Open the new Host, and click the Applications tab.
Click Add Application.
Add an application for HTTPS:
Name: OpenClaw HTTPS
Protocol: TCP
Port: 443
(Optional) Add an application for HTTP (useful for
curltesting from the tunnel):Name: OpenClaw HTTP
Protocol: TCP
Port: 80
Save and ensure the appropriate Access Groups have permission to reach these Applications.
Step 6: Prepare the project directory
Prepare the project directory on Docker:
mkdir -p ~/openclaw-docker cd ~/openclaw-docker mkdir -p config workspace
Set ownership to uid 1000 (the
nodeuser 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/at700to 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
curltesting from the host.Port 443 (HTTPS): For browser access through CloudConnexa. Uses
tls internalto 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.
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.
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"Verify this matches the tunnel IP shown in the CloudConnexa Admin portal (Step 3: Deploy the host connector). If it doesn't, set it manually:
TUNNEL_IP='100.96.1.50'
Create the
docker-compose.ymlfile 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: EOFWhat's happening here and why:
Security: Uses official GHCR non-root images (
nodeuser) 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 public0.0.0.0binding.Persistence:
caddy_datavolume preserves SSL certificates and CA state.Health: Standardized
/healthzprobes ensure automated container recovery.
Step 9: Run onboarding and start services
9.1 Run onboarding
Run the onboarding wizard:
docker compose run --rm openclaw-cli onboard
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:
Run:
openclaw dashboard --no-open
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.
Start all services defined in the Docker Compose file and run them in the background:
docker compose up -d
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.
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 acurltest. 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).
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.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.
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.
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.local1\"]" --strict-jsonReplace
openclaw.localwith 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.
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.
Mask sensitive tool output in logs:
docker compose run --rm openclaw-cli config set logging.redactSensitive tools
10.6 Apply configuration changes
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.
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
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, or192.168.0.0/16as needed.Verify rules (optional) by displaying active Docker firewall rules:
sudo iptables -S DOCKER-USER
Step 12: Access OpenClaw remotely
Connect to CloudConnexa.
In a web browser, navigate to:
https://openclaw.local1/#token=<your-token>2
Replace
openclaw.localwith your Host domain if different.Replace
<your-token>with the gateway token from Step 1: Install OpenClaw.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.
Connect to your server.
List pending device requests:
openclaw devices list
Note the device ID you'll need to approve.
Approve the device:
openclaw devices approve <requestId>1
Replace
<requestId>with the ID from the device list.Refresh the browser.
The Control UI should now load.
Note
Each browser/device needs approval once. After approval, it's remembered across sessions.
Managing your deployment
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.
docker compose logs openclaw-gateway # Gateway logs docker compose logs caddy # Caddy logs
Always back up before updating.
docker compose pull # Pull latest images docker compose up -d --force-recreate # Recreate containers
docker compose run --rm openclaw-cli security audit --deep
Troubleshooting
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.
Cause
The request origin isn't included in the allowed origins configuration.
Resolution
Update the allowed origins settings.
Cause
This is expected behavior when connecting from a new device or browser.
Resolution
Follow the steps to approve the device.
Cause
The Connector, routing, or access permissions may not be correctly configured.
Resolution
Verify the Host Connector shows "Connected" in the CloudConnexa admin portal.
Test connectivity from a device connected to the WPC:
curl -sk https://openclaw.local/
Confirm that your user/device is assigned to an Access Group that has permission to reach the OpenClaw application.
Cause
Caddy may not be correctly configured or running.
Resolution
Verify the Caddyfile exists and is correct:
cat Caddyfile
Text connectivity from inside the container:
docker compose exec caddy curl http://localhost:18789/healthz
Expected output:
{"ok":true,"status":"live"}Restart all services:
docker compose down && docker compose up -d
Cause
Incorrect file ownership or permissions for mounted volumes.
Resolution
Reset permissions:
sudo chown -R 1000:1000 config/ workspace/ chmod 700 config/ workspace/