Sharing & Tunnels: Exposing Liferay to the Internet¶
LDM provides a unified interface (ldm share) to securely expose your local Liferay environment or containers to the public internet. This is essential for:
- Testing external webhooks (e.g., payment gateways, external CI pipelines).
- Integrating with SaaS services that require a callback URL.
- Sharing your local development environment with colleagues or clients.
Sharing Providers¶
LDM supports two distinct tunneling providers:
1. lfr-tunnel (Default)¶
lfr-tunnel is a lightweight, host-side Go client that routes traffic from public wildcard subdomains (*.lfr-demo.se and *.lfr-demo.online) directly to your local ports.
- Pros: Runs natively on the host, lowest CPU overhead, supports wildcard routing.
- Requirements: LDM automatically downloads and updates the binary on the first invocation.
Warning
EDR / SentinelOne Quarantine: Because lfr-tunnel (host-side) is a Go binary downloaded dynamically to the host, corporate endpoint protection tools (such as SentinelOne) may flag it as an untrusted threat and quarantine it. This will prevent LDM from starting the tunnel and cause the error: β Failed to verify lfr-tunnel installation after download.
The Solution: If you encounter EDR interference, use the lfr-tunnel-docker (Zero-Install) provider instead. It runs the exact same client inside the isolated project Docker Compose network, bypassing host-level EDR checks entirely. Set it via --share-provider lfr-tunnel-docker on startup:
2. lfr-tunnel-docker (Zero-Install)¶
Runs the exact same lfr-tunnel client, but completely isolated as a service sidecar inside your project's docker-compose.yml stack.
- Pros: 100% immune to SentinelOne/EDR host-level alerts that block unknown Go binaries on macOS/Windows. Runs in sync with the project's lifecycle (
ldm run/ldm stop), requiring zero manual background process management. - Resource Constraints: Optimized to consume absolute minimal resources (CPU is restricted to a maximum of
0.10cores with0.05reservations, and memory is bound to a maximum of50Mwith20Mreservations). - Direct Tomcat Bypass: Directly connects to the
liferaycontainer internally on Tomcat's port8080(rather than routing through the Traefik proxy), which resolves Host header/Virtual Host url conflicts for external redirect services.
Conceptual Flow¶
Public User
β (HTTPS via leased subdomain: e.g., my-subdomain.lfr-demo.se)
βΌ
VPS Server Gateway (lfr-tunneld)
β (WebSockets over TCP)
βΌ
lfr-tunnel Client (Docker Sidecar Service in Project Network)
β (Bypasses local Nginx/Traefik proxy completely)
βΌ (Internal Docker Network Routing)
liferay Container (Tomcat Port 8080)
3. ngrok¶
ngrok runs as a sidecar container inside your project's Docker Compose stack, creating a secure tunnel to ngrok's edge servers.
- Pros: Fully self-contained, isolated within the project stack.
- Requirements: Requires a free ngrok account and Auth Token.
Direct CLI Usage (ldm share)¶
You can manage tunnels at any time using the ldm share command group.
Start a Tunnel¶
To share a running project context:
- If
--provideris omitted, LDM defaults tolfr-tunnel. - If
--domainis omitted, the client picks a gateway itself by latency and fails over automatically if that gateway goes down. Naming a known gateway with--domainpins it, which disables both β useful when you need a specific region, but the tunnel will stay down for the duration if that gateway stops. LDM warns when a run is pinned. - If
--subdomainis omitted, it defaults to the subdomain carried by--domain(see What--domainaccepts below), and failing that to the project name. - LDM prints the address it is about to claim before it connects, and where each half of it came from:
βΉ Public URL: https://peters.lfr-demo.se
βΉ subdomain 'peters' from the command line, domain 'lfr-demo.se' from --domain
The second line matters more than it looks. The resolution order is a flag,
then the project's meta, then ~/.ldmrc, then a prompt, then LDM's
default -- and two of those are written by LDM itself, so a value can be in
play that nobody remembers choosing. A domain arriving from a machine-wide
pin now says so:
- If
--portsis omitted, the tunnel client auto-discovers what to expose: running Docker containers first, then common Liferay ports, then the client-extension ports declared by the workspace. Passing--portsnarrows it to exactly what you name, so omit it unless you need that.
What --domain accepts¶
--domain takes three different kinds of value, and LDM tells them apart
rather than requiring you to know which is which:
| You pass | LDM reads it as | Result |
|---|---|---|
lfr-demo.se |
a known gateway base domain | <subdomain>.lfr-demo.se, gateway pinned |
peters.lfr-demo.se |
a host on a known base domain | subdomain peters on lfr-demo.se, gateway pinned |
dev.example.com |
a custom vanity domain | <subdomain>.dev.example.com, no pin |
default |
the standard gateway | <subdomain>.lfr-demo.online, ignoring any stored domain |
A custom vanity domain only affects the public URL; the client always dials a real Liferay gateway, and the domain must be registered and approved in the Liferay Tunnel portal before it will resolve.
Asking for the standard gateway¶
The domain is sticky. LDM records it in the project's meta and in
~/.ldmrc, so a custom domain set once is reused by every later
ldm share start for that project -- including one where you only passed
--subdomain:
Pass default (or standard) to ask for the standard gateway instead:
This also replaces the domain stored for that project, so subsequent runs
use the standard gateway too. It does not alter ~/.ldmrc; the project's own
value takes precedence over it either way.
When the domain you asked for did not take¶
An unregistered custom domain does not fail -- the gateway routes you on the standard domain instead. LDM announces a public URL before the tunnel starts and resolves the real one afterwards, so when those differ it now says so:
β
π Public Tunnel Active: https://peters.lfr-demo.se
β οΈ The tunnel came up on https://peters.lfr-demo.se, not the
https://peters.dev.example.com announced above. Use the address on this line.
π‘ Next step: 'dev.example.com' is a custom domain, and the gateway routes one
only once it has been registered and approved in the Liferay Tunnel portal.
Previously both URLs were printed and neither was reconciled with the other, so the first -- which does not resolve -- was indistinguishable from fact (LDM-#2191).
If you already have the URL, pass it whole and let LDM take it apart:
--url is shorthand for --subdomain plus --domain; pass one or the other,
not both. If --subdomain and the subdomain implied by --domain disagree,
LDM refuses rather than picking one for you.
Where the known base domains come from¶
LDM ships with lfr-demo.online and lfr-demo.se as a seed, and extends
it with whatever the gateway says it answers on. On a real ldm share start
LDM reads supported_domains from the gateway's unauthenticated
/api/version endpoint and caches it for 24 hours, so a domain added to the
hosted gateways is picked up without an LDM release.
Three properties are deliberate:
- The seed is extended, not replaced. The advertised list is the set of names a gateway answers on and carries no preference; the seed's order decides which gateway LDM defaults to. The live gateways advertise the seed pair in the opposite order, so substituting one for the other would quietly change your default gateway.
--dry-runnever fetches. The list is refreshed only on a real run, soldm share start --dry-runresolves a subdomain, a domain and a public URL with no client, no token and no network.- A gateway that advertises nothing changes nothing. An older gateway, or one whose operator has declined to publish the list, simply leaves the seed in place.
tunnel_base_domains, a JSON list in ~/.ldmrc, still replaces the lot
and suppresses the fetch entirely. That remains the mechanism for a
self-hosted Liferay Tunnel gateway, whose names LDM has no way to discover --
the fetch goes to the gateway the seed names, or to an explicitly pinned one
(LFT_SERVER_URL).
Note
Answering the interactive "Choose sharing domain" prompt stores your answer
in ~/.ldmrc as a machine-wide default, and ldm share start then
copies the resolved value into each project's meta. Resolution order is
the flag, then the project meta, then ~/.ldmrc -- so clearing the global
value alone will not change a project that has already been shared once.
The provenance line printed beside the public URL names whichever of these
answered, so you do not have to work it out.
Check Tunnel Status¶
To inspect the active tunnel and retrieve its public URL:
ldm share status reports the same provenance as ldm share start, because
it is where you look when an address has already surprised you:
β Subdomain: peters
β Address from: subdomain 'peters' from the live tunnel, domain 'lfr-demo.se' from ~/.ldmrc
The subdomain is attributed to whichever answered -- the live tunnel, the
project's meta, or the project name -- and the domain to the first of the
flag, the project's meta or ~/.ldmrc that holds one.
status resolves nothing and writes nothing. It will not prompt for a domain
the way share start does, and will not persist one to ~/.ldmrc, so reading
the status can never change what the next share does.
Stop a Tunnel¶
To terminate the active sharing session:
Automatic Run Integration (ldm run --share)¶
Instead of starting the tunnel manually, you can tell LDM to automatically boot the sharing tunnel as soon as your Liferay container becomes healthy using the --share flag:
Sharing without reconfiguring (ldm start --share)¶
ldm run reconfigures the project before booting it -- it regenerates the
compose file, re-resolves dependency versions and re-applies configuration.
For a project that is already configured and simply needs to be up and shared,
that is more than you asked for, and anything that fails in the reconfigure
stops you sharing at all.
ldm start boots the existing containers without reconfiguring, and takes the
same share flags:
Sharing is auxiliary to the start: the project comes up, and then it is
shared. ldm start --share is ldm start followed by ldm share start --
it saves the second command rather than changing what either one does, and
the tunnel opens only once every container has started.
--share cannot be combined with --all: a tunnel leases one subdomain and
forwards to one target, and the refusal arrives before anything is started.
Default Subdomain¶
Without --share-subdomain, LDM derives one from the project name. A DNS label may contain only lowercase ASCII letters, digits and hyphens, so the derived value is normalised accordingly -- it is not simply the project name:
| Project | Derived subdomain |
|---|---|
my-project |
my-project |
My_Project |
my-project |
SaarbrΓΌcken |
saarbruecken |
poc.client |
poc-client |
Underscores and dots become hyphens, the result is lowercased and truncated to 63 characters, and leading or trailing hyphens are removed. If nothing usable remains, no default is sent and the provider assigns one -- pass --share-subdomain explicitly if you need a specific name.
This is stricter than the sanitisation applied to Docker container names, which permits _, . and mixed case.
Expose Alias (Ngrok Legacy)¶
For backward compatibility, the --expose flag remains supported and behaves as an alias for --share --share-provider ngrok:
Toggling Routing Modes (ldm config ssl-mode)¶
When developing with LDM, you may want to toggle your project network routing between:
- Local Hosts-based SSL Mode: Testing offline using a local custom domain name (e.g.
https://my-project.localmapped in/etc/hostswith certificates managed viamkcert). - Public Share Tunnel Mode: Exposing Liferay publicly via a leased subdomain on a secure tunnel gateway (e.g.
https://my-project.lfr-demo.online).
Manually updating Liferay virtual hosts, rebuilding project properties, and updating environment config files inside your client extensions can be error-prone. LDM automates this using the ssl-mode command:
ldm config ssl-mode <hosts | share> [project] [--subdomain <subdomain>] [--domain <domain>] [--no-restart]
Modes¶
1. Hosts Mode (hosts)¶
- Switches the project's metadata
sslparameter totrue. - Sets the project's
host_nameto<project_name>.local(or retains your custom local domain). - Surgically searches for and updates all local
.envfiles (e.g.,LIFERAY_URL,LIFERAY_PORTAL_URL,AICA_LIFERAY_URL) to point tohttps://<project_name>.local. - Rebuilds project properties so Liferay aligns its internal virtual host settings.
2. Share Mode (share)¶
- Switches the project's metadata
sslparameter tofalse(since SSL termination happens at the public tunnel edge). - Sets
host_nametolocalhost. - Configures your optional
--subdomain(defaults to project name) and--domain(defaults tolfr-demo.online). - Surgically searches for and updates all local
.envfiles to point to the public tunnel URL (e.g.https://<subdomain>.<domain>). - Rebuilds project properties so Liferay maps internal endpoints to the public gateway URL.
Warning
Container Stack Downtime Warning:
Changing routing modes modifies Traefik reverse-proxy bindings, project metadata, and portal-ext.properties. By default, if the project container stack is currently running, changing the routing mode will briefly stop and restart the container stack to apply the changes (causing a short downtime of the Liferay instance).
If you prefer to change the configuration offline or manually manage the container lifecycle later, pass the --no-restart flag:
Authentication for Liferay Tunnel (lfr-tunnel)¶
The Liferay Tunnel (lfr-tunnel or lfr-tunnel-docker) requires a Client Token to authenticate with the server gateway. LDM looks for this token in the following order of priority:
- Environment Variable:
LFT_CLIENT_TOKEN(highest priority). Useful for CI/CD or automated scripts:
- Token File: A flat text file containing only the token saved at
~/.lfr-tunnel/token. - LDM Global Config: Stored in
~/.ldmrcunder thelfr_tunnel_tokenkey. - Interactive Prompt: If no token is found, LDM will prompt you to enter it interactively and save it to your
~/.ldmrcglobal config for future commands.
Securing the Liferay Tunnel Personal Access Token (PAT)¶
Because LFT_CLIENT_TOKEN grants access to tunnel your local environments, it should be stored securely on your machine.
Option 1: The Restricted Secrets File (Recommended)¶
This approach stores the token in a startup configuration file but restricts its POSIX/Windows ACL permissions so other users or unauthorized local processes cannot read it.
On Linux / macOS (Zsh & Bash)¶
- Create the restricted directory and file:
(The chmod 600 command ensures only the owner can read or write to this file).
- Add the token variable to the file:
- Source it on shell startup:
Add this to the bottom of your ~/.zshrc or ~/.bashrc:
On Windows (PowerShell)¶
- Create the secret file and restrict its ACLs:
Run these commands in PowerShell to create the file and strip away all permissions except for your explicit user account:
# Create the directory and file
New-Item -ItemType Directory -Path "$HOME\.config\lfr" -Force
$SecretFile = New-Item -ItemType File -Path "$HOME\.config\lfr\secrets.ps1" -Force
# Restrict permissions so ONLY you can access it
$Acl = Get-Acl $SecretFile.FullName
$Acl.SetAccessRuleProtection($true, $false) # Break inheritance
$User = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$Rule = New-Object System.Security.AccessControl.FileSystemAccessRule($User, "FullControl", "Allow")
$Acl.AddAccessRule($Rule)
Set-Acl $SecretFile.FullName $Acl
- Add the token to the file:
Set-Content -Path "$HOME\.config\lfr\secrets.ps1" -Value '$env:LFT_CLIENT_TOKEN="your_actual_token_here"'
- Load the secret automatically on startup:
Open your PowerShell profile (create it if missing via New-Item -Type File -Path $PROFILE -Force):
Add this line to it:
Option 2: The OS Credential Manager Route (Alternative)¶
This approach stores the token in the OS native credential store (encrypted at rest).
On Linux (Secret Service API / GNOME Keyring)¶
- Store the token:
secret-tool store --label="LFT Token" lfr tunnel_token
# You will be securely prompted to enter the token
- Retrieve it automatically on shell startup:
Add this to your ~/.bashrc or ~/.zshrc:
On Windows (Credential Manager)¶
- Store the token:
- Retrieve it in your
$PROFILE:
Since retrieving generic credentials in plain text via PowerShell can be verbose, the Restricted Secrets File approach (Option 1) is generally recommended for Windows CLI environments.
Authentication for Ngrok¶
Ngrok requires an Auth Token to bind custom host headers and use HTTPS upstreams. The first time you use ngrok, LDM will prompt you for your Auth Token:
- Sign up or log in at dashboard.ngrok.com.
- Navigate to Getting Started -> Your Authtoken.
- Paste the token into the LDM prompt.
LDM will save this token securely in your global configuration (~/.ldmrc), so you will never be prompted for it again across any of your projects.
Tunnel Inspector Dashboard¶
The Liferay Tunnel container exposes a local web-based Inspector Dashboard. This dashboard allows you to view active tunnels, request/response traffic, and WebSocket latency.
Accessing the Dashboard¶
When running with lfr-tunnel-docker as your provider, LDM automatically maps port 4040 of the sidecar container to your host machine:
- Dashboard URL: http://localhost:4040
Custom Inspector Host Binding (LFT_INSPECTOR_BIND)¶
By default, when running inside Docker, the tunnel client binds to 0.0.0.0:4040 to ensure port forwarding works out-of-the-box. When running natively outside Docker, it binds only to 127.0.0.1:4040 for local security.
You can customize the network interfaces the dashboard binds to by setting the LFT_INSPECTOR_BIND environment variable in your local .env file:
# Bind the inspector dashboard to loopback only (inside container)
LFT_INSPECTOR_BIND=127.0.0.1
# Bind to all interfaces (default inside Docker)
LFT_INSPECTOR_BIND=0.0.0.0
Remote Compute Node Tunneling (Zero Inbound Port Security)¶
When deploying LDM projects to remote cloud compute nodes (e.g. AWS EC2, DigitalOcean, or Azure VMs) via Multi-Node Orchestration (ldm target), running an outbound tunnel sidecar provides a powerful security advantage:
Closing Inbound HTTP/HTTPS Firewall Ports¶
- Outbound Connection: The
lfr-tunnel-dockersidecar container running on the remote compute node establishes an outbound encrypted WebSocket connection to a public gateway (*.lfr.live/*.lfr-demo.online). - Firewall Lockdown: Because traffic flows through an outbound socket, you do NOT need to open Inbound HTTP (80) or HTTPS (443) ports in your cloud Security Group / Firewall.
- Control Plane Only: The compute node's Security Group only requires Inbound Port 22 (SSH) open (restricted to your developer IP / VPN) for orchestrator CLI commands (
ldm target).
# Run project on remote compute node with outbound tunneling enabled
ldm run my-project --target prod-aws --share --share-provider lfr-tunnel-docker --share-subdomain my-remote-demo
Handling SaaS Callbacks (The Host Header)¶
By default, the sharing tunnels rewrite the Host header to match your project's --host-name (e.g., forge.demo). This ensures that Traefik knows how to route the request to your specific Liferay container.
However, because Liferay "thinks" it is running at forge.demo, it may dynamically generate callback URLs or pagination links using forge.demo instead of your public tunnel domain (e.g., custom-sub.lfr-demo.se). Since external SaaS services cannot resolve forge.demo, those links will fail.
How to Fix This¶
If your SaaS integration requires Liferay to generate fully qualified URLs (e.g., OAuth redirects), you must temporarily tell Liferay to treat the public tunnel URL as its primary domain:
- Log into Liferay as an administrator.
- Go to Control Panel -> Instance Settings -> Instance Configuration.
- Under Virtual Host, enter your public tunnel domain (e.g.,
custom-sub.lfr-demo.seora1b2c3d4.ngrok.app). - Save the configuration.
Liferay will now generate all callback URLs using the public tunnel address.
π οΈ LDM Sharing Command-Line Switches¶
When starting or running projects via LDM, you can configure the sharing behavior using the following CLI flags:
1. Stack Execution Flags (ldm run)¶
--share: Enables automatic sharing. Starts a tunnel when the Liferay container reaches a healthy state.--share-provider <lfr-tunnel | lfr-tunnel-docker | ngrok>: Selects the sharing engine. (Defaults tolfr-tunnelhost mode).--share-subdomain <name>: Specifies a custom subdomain (e.g.,pjrtest). If omitted, defaults to the project name.--share-image <image>: Overrides the sidecar Docker image (useful for testing custom tunnel builds).--share-inspector: Maps the tunnel's local web dashboard to host port4040(allows visiting http://localhost:4040).
2. Standalone Share Flags (ldm share start)¶
--provider <lfr-tunnel | lfr-tunnel-docker | ngrok>: Explicitly selects the provider.--subdomain <name>: Requests a specific subdomain.--domain <domain>: A known gateway base domain, a host on one, or a custom vanity domain -- see What--domainaccepts above.--url <url>: The public URL to claim, e.g.https://peters.lfr-demo.se. Shorthand for--subdomainand--domaintogether.--ports <ports>: Overrides target downstream port mapping (e.g.--ports 8080,3000).--image <image>: Overrides sidecar Docker image.--inspector: Exposes the Web Inspector Dashboard on host port4040.
π Advanced lfr-tunnel Binary Switches (Go Client Native Flags)¶
If you are running the lfr-tunnel Go executable directly or writing custom scripts outside LDM's standard workflow, the native client binary supports the following switches:
1. Connection & Routing¶
-server <url>: Specifies thelfr-tunneldgateway VPS URL (e.g.-server https://tunnel.lfr-demo.se).-token <auth-token>: Authenticates client credentials against the gateway.-subdomain <prefix>: Requests a prefix for public wildcard access (e.g.-subdomain pjrtest).-ports <port,port,...>: Comma-separated downstream local ports to bind/expose (e.g.,-ports 8080,3000).-target-host <host/ip>: Points the tunnel upstream destination to a specific IP or hostname (defaults tolocalhost).-preserve-host: Ingests incoming Host headers down the socket directly instead of rewriting them to target host.
2. Dashboard & Security¶
-inspector-port <port>: Sets a custom port to run the web inspector dashboard on (defaults to4040).- Example:
lfr-tunnel -ports 8080 -inspector-port 4045runs dashboard on http://localhost:4045. -basic-auth <user:pass>: Activates basic authentication on the public HTTPS endpoints to protect your shared sandbox environment from unauthorized public access.-rate-limit <req/sec>: Restricts the maximum throughput routed down the tunnel to protect local servers (0 = unlimited).-add-header "Name: Value": Automatically injects standard HTTP headers into downstream packets (e.g.-add-header "X-Bypass-CORS: true").
3. Background Daemon Control¶
-background: Fork/daemonize the client process into the background. Output logs are redirected to~/.lfr-tunnel/client.logand the process ID is tracked in~/.lfr-tunnel/lfr-tunnel.pid.-status: Introspects and outputs the running status/PID of the background daemon.-stop: Sends a shutdown signal (SIGINT) to clean up and close the background tunnel daemon.-upgrade: Triggers a self-upgrade sequence downloading the latest release binary.
Last Updated: 2026-10-19 | Last Reviewed: 2026-10-19