Skip to content

Self-Hosted Puter on custom domain via Cloudflare Tunnel (no subdomain mode) #2770

@FixleCH

Description

@FixleCH

Summary

Successfully deployed a self-hosted Puter instance on a custom domain (example.com) using Cloudflare Tunnel and Apache2 as reverse proxy, with path-based API routing (no api. subdomain required).

This was made possible thanks to the new centralized .env configuration system introduced in:


Environment

  • OS: Ubuntu 24 (bare metal)
  • Node.js: v24.14.1
  • Puter: v2.5.1
  • Reverse Proxy: Apache2
  • Tunnel: Cloudflare Tunnel (cloudflared)
  • Domain: custom domain via Cloudflare DNS

Working Configuration

.env (Puter root)

PUTER_DOMAIN=example.com
SSL_ENABLED=true
PUTER_API_ROUTING=path
PORT=4100
PUBLIC_PORT=443
NODE_ENV=production
SERVER_ID=puter-hnow
CONTACT_EMAIL=admin@example.com

volatile/config/config.json

{
    "domain": "example.com",
    "protocol": "https",
    "http_port": 4100,
    "pub_port": 443,
    "experimental_no_subdomain": true,
    "api_base_url": "https://example.com",
    "nginx_mode": true,
    "env": "production"
}

Apache2 VirtualHost (/etc/apache2/sites-available/puter-hnow.conf)

<VirtualHost *:80>
    ServerName example.com

    ProxyPreserveHost On
    ProxyRequests Off

    RewriteEngine On
    RewriteCond %{HTTP:Upgrade} =websocket [NC]
    RewriteRule /(.*) ws://127.0.0.1:4100/$1 [P,L]

    ProxyPass        / http://127.0.0.1:4100/
    ProxyPassReverse / http://127.0.0.1:4100/

    RequestHeader set Host "example.com"
    RequestHeader set X-Forwarded-Proto "https"
    RequestHeader set X-Forwarded-Port "443"

    ErrorLog  ${APACHE_LOG_DIR}/puter-hnow-error.log
    CustomLog ${APACHE_LOG_DIR}/puter-hnow-access.log combined
</VirtualHost>

Cloudflare Tunnel (/etc/cloudflared/puter-hnow-config.yml)

tunnel: <TUNNEL_ID>
credentials-file: /home/user/.cloudflared/<TUNNEL_ID>.json

ingress:
  - hostname: example.com
    service: http://localhost:4100
    originRequest:
      noTLSVerify: true
      httpHostHeader: example.com

  - service: http_status:404

Key Findings & Gotchas

1. config.json takes precedence over .env on re-runs

The .env parser applies configuration at runtime but does not overwrite the existing config.json. After the first run, config.json is cached with default values (puter.localhost). Solution: delete config.json and let Puter regenerate it, then verify with:

curl -s http://localhost:4100/ -H "Host: yourdomain.com" | grep "api_origin"

2. Apache must forward the Host header explicitly

Without RequestHeader set Host "yourdomain.com", Puter receives Host: localhost and returns a 400 Bad Request for all static assets (/dist/bundle.min.css, etc.).

3. Browser cache must be cleared after config changes

Even with correct server-side config, stale api_origin values from old sessions caused the frontend to still point to api.puter.com. Full cache clear (including localStorage) is required.

4. Cloudflare Tunnel must use a dedicated config file

If you already have other Cloudflare Tunnels running (e.g. for other domains), create a separate tunnel and a separate config file with its own systemd service:

cloudflared tunnel create puter-hnow
# Then run as separate service:
sudo systemctl start cloudflared-puter-hnow

5. experimental_no_subdomain: true is required for path-based API

This flag tells Puter not to use api.yourdomain.com and instead serve the API from the main domain.


Related Issues


Credit

Configuration system made possible by PRs:

Image

Look at the url

Fixle

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions