DEV Community

Vasyl Herman
Vasyl Herman

Posted on

Setting Up a Custom Caddy Reverse Proxy for OpenClaw on macOS

Setting Up a Custom Caddy Reverse Proxy for OpenClaw on macOS

A step-by-step guide for macOS users running OpenClaw to set up a custom Caddy server as a reverse proxy — covering mise installation, building Caddy with xcaddy and custom plugins, configuring GeoIP filtering with basic auth, and running it as a launchd service.

1. Installing mise

mise (formerly rtx) is a polyglot runtime manager. Install it and activate it in your shell:

curl https://mise.run | sh
export PATH="$HOME/.local/bin:$PATH"
eval "$(mise activate bash)"
Enter fullscreen mode Exit fullscreen mode

For other shells, see the mise documentation.

2. Installing xcaddy with mise

xcaddy is the tool for building Caddy with custom modules (GeoIP, rate limiting, etc.). You can install it via mise's github backend, which pulls binaries directly from GitHub releases.

# Pin to a specific version
mise use -g github:caddyserver/xcaddy@0.4.5

# Or use the latest version
mise use -g github:caddyserver/xcaddy
Enter fullscreen mode Exit fullscreen mode

The github backend offers:

  • Provenance verification — validates release authenticity
  • Download progress reports — visual feedback during install

Verify the installation:

xcaddy version
Enter fullscreen mode Exit fullscreen mode

3. Finding Custom Caddy Plugins

Before building, you need to know which plugins are available. Caddy provides two official resources:

Modules Directory

https://caddyserver.com/docs/modules/

The complete registry of all registered Caddy modules. Each entry shows the module ID, description, and links to its source repository. Modules marked as non-standard are community plugins that need to be added via xcaddy. Use your browser's "Find in page" for quick lookups.

Download Page

https://caddyserver.com/download

An interactive build page that lists all available plugins with checkboxes. You can:

  • Browse and search all available plugins by name or category
  • Select plugins you need and download a pre-built binary directly
  • Copy the xcaddy build command with all your selected --with flags

Tip: Use the modules directory to research what's available, then use the download page to either grab a pre-built binary or copy the exact xcaddy build command.

4. Getting the GeoLite2 Database

The GeoIP filtering requires a MaxMind GeoLite2 ASN database file. Sign up for a free account at maxmind.com, then download the GeoLite2-ASN.mmdb file from your account dashboard under Download Databases.

# Place the database where your Caddyfile can reference it
mv GeoLite2-ASN.mmdb ~/GeoLite2-ASN.mmdb
Enter fullscreen mode Exit fullscreen mode

Tip: MaxMind also provides geoipupdate — a tool that keeps your database files up to date automatically. Install with brew install geoipupdate and configure it with your MaxMind license key.

5. Building Caddy with Custom Modules

With xcaddy installed, build a custom Caddy binary with the modules you need:

xcaddy build v2.11.2 \
  --with github.com/porech/caddy-maxmind-geolocation@v1.0.3
Enter fullscreen mode Exit fullscreen mode

This produces a caddy binary in the current directory with MaxMind GeoIP support baked in.

Move it to your PATH:

mv caddy /usr/local/bin/caddy
Enter fullscreen mode Exit fullscreen mode

6. Running Caddy as a macOS Service

Create a launchd plist to run Caddy automatically on boot:

tee ~/Library/LaunchAgents/com.caddyserver.caddy.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.caddyserver.caddy</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/caddy</string>
        <string>run</string>
        <string>--config</string>
        <string>/Users/username/.config/caddy/Caddyfile</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/Users/username/.config/caddy/caddy-stdout.log</string>
    <key>StandardErrorPath</key>
    <string>/Users/username/.config/caddy/caddy-stderr.log</string>
</dict>
</plist>
EOF
Enter fullscreen mode Exit fullscreen mode

Managing the Service

# Start Caddy service
launchctl load -w ~/Library/LaunchAgents/com.caddyserver.caddy.plist

# Stop Caddy service
launchctl unload -w ~/Library/LaunchAgents/com.caddyserver.caddy.plist

# Reload config without restarting the service
caddy reload --config ~/.config/caddy/Caddyfile
Enter fullscreen mode Exit fullscreen mode

7. Production Caddyfile: GeoIP, Basic Auth, and Reverse Proxy

Generating a Password Hash

Caddy's basic_auth directive requires a bcrypt-hashed password. Generate one with:

caddy hash-password
Enter fullscreen mode Exit fullscreen mode

This will prompt you to enter and confirm a password, then output a bcrypt hash like:

$2a$14$Mpdg/pw/CBar4/YMFRcwbuN2gWsOQgaCowWDHPHsc402vF18TRGRK
Enter fullscreen mode Exit fullscreen mode

Use this hash in your Caddyfile's basic_auth block.

Example Caddyfile

Here's a real-world Caddyfile serving two sites with:

  • MaxMind GeoIP ASN filtering — only allow traffic from specific ISPs/networks
  • Basic authentication — password-protect the proxy
  • Reverse proxying — forward traffic to backend services
  • Separate handling for webhook endpoints (no auth required)
site1.example.com {
    log {
        output file /Users/username/.config/caddy/site1.example.com-caddy.log
    }

    @allowed {
        maxmind_geolocation {
            db_path "/Users/username/GeoLite2-ASN.mmdb"
            allow_asn 48323 21497 59497 58309
        }
    }

    handle /googlechat {
        reverse_proxy 192.168.111.98:18789
    }

    handle {
        basic_auth @allowed {
            admin $2a$14$...
        }
        reverse_proxy @allowed 192.168.111.98:18789
        respond 501
    }
}

site2.example.com {
    log {
        output file /Users/username/.config/caddy/site2.example.com-caddy.log
    }

    @allowed {
        maxmind_geolocation {
            db_path "/Users/username/GeoLite2-ASN.mmdb"
            allow_asn 48323 21497 59497 58309
        }
    }

    basic_auth @allowed {
        admin $2a$14$...
    }
    reverse_proxy @allowed 127.0.0.1:3644
    respond 501
}
Enter fullscreen mode Exit fullscreen mode

How the Request Flow Works

  1. GeoIP check (@allowed matcher) — only ASNs 48323, 21497, 59497, and 58309 can proceed past basic auth. All others get the respond 501.
  2. Basic auth — only requests matching @allowed are prompted for credentials.
  3. Reverse proxy — authenticated + allowed requests are forwarded to the backend.
  4. Fallback — anything that doesn't match gets a 501 response.

The Webhook Exception

The /googlechat path is handled separately with its own handle block, bypassing auth. This is essential for incoming webhooks that can't provide credentials.

Quick Reference

# Install mise
curl https://mise.run | sh
export PATH="$HOME/.local/bin:$PATH"
eval "$(mise activate bash)"

# Install xcaddy
mise use -g github:caddyserver/xcaddy@0.4.5

# Build custom Caddy
xcaddy build v2.11.2 \
  --with github.com/porech/caddy-maxmind-geolocation@v1.0.3

# Install the binary
mv caddy /usr/local/bin/caddy

# Generate password hash for basic_auth
caddy hash-password

# macOS service management
launchctl load -w ~/Library/LaunchAgents/com.caddyserver.caddy.plist
launchctl unload -w ~/Library/LaunchAgents/com.caddyserver.caddy.plist

# Manage Caddyfile
caddy validate --config ~/.config/caddy/Caddyfile
caddy reload --config ~/.config/caddy/Caddyfile
caddy fmt --overwrite ~/.config/caddy/Caddyfile
Enter fullscreen mode Exit fullscreen mode

Useful links:

Top comments (0)