CLI Core Commands¶
Display a tabulated overview of all initialized LDM sandbox environments.
ldm list
ldm ls
# Machine-readable output for scripts/automation, instead of the color-coded
# table -- a stable JSON array with one object per project (project, version,
# target, status, running_containers, total_containers, db_unhealthy, url,
# seeded, path, last_seen).
ldm list --json
The Status column reflects more than just whether the container process is alive: a project whose container is running but has failed its own Docker HEALTHCHECK (e.g. Postgres crash-looping after an out-of-disk PANIC) is reported as Unhealthy rather than Running, and a project whose app container is healthy but whose DB container has failed its healthcheck is reported as Running (DB unhealthy). This does not perform a live HTTP probe -- for that, use ldm wait after starting a project.
Not Created (LDM-#1870) is distinct from Stopped: it means the project's meta and volumes exist, but no container matching it exists at all -- typically because the containers were removed directly (e.g. docker rm) rather than through LDM. Stopped still has a container to start; Not Created does not, so ldm start refuses it (see below) and ldm run must be used to recreate the stack. Both --json and the formatted table use the same Not Created string in the status field/column; running_containers/total_containers are 0/0 in this state, same as for a project with zero matched containers under the older binary behavior -- a consumer that already branches on those counts rather than the status string is unaffected by this new value.
run (alias: up)¶
Initialize and start a project stack. If the project is already initialized, it will print an informational message and provide a 5-second window to safely abort (CTRL+C) before reconfiguring.
# Run with a specific tag and virtual hostname
ldm run --tag 2024.q4.0 --host-name demo.local
# Automatically grab the latest Quarterly Release
ldm run demo --tag-latest --release-type qr
# Run with a resource-optimized JVM profile (great for laptops with less RAM)
ldm run my-project --lean
# Inject an environment variable directly
ldm run my-project --env LIFERAY_COMPANY_DEFAULT_WEB_ID=my-domain.com
# Repeat --env for more than one variable
ldm run my-project --env A=1 --env B=2
# Enable specific Liferay feature flags
ldm run demo --feature LPS-122920 dev beta
# Run on a custom port
ldm run my-project --port 8081
# Using the alias
ldm up demo
# Initialize with "Confidence Booster" samples
ldm run demo --samples
# Auto-open the browser after startup
ldm run demo --open
# Start an ngrok tunnel to expose Liferay to the public internet
ldm run demo --expose
# Boot a 2-node Liferay cluster in one command
ldm run demo --scale liferay=2
# Boot a scaled stack and open the browser
ldm run demo --scale liferay=2 --open
# Interactive run (will prompt for version and project name)
# Prompts are automatically pre-filled using the Cascading Defaults system
ldm run
--open Switch¶
Use --open to automatically launch the Liferay URL in your system browser once the instance is ready. This is equivalent to running ldm browser immediately after startup, but in a single command.
--scale Switch¶
Use --scale SERVICE=N to boot a scaled stack without having to run ldm scale as a separate step. Multiple services can be scaled at once:
Tag Discovery & Release Types¶
When you do not name a tag with -t/--tag, LDM discovers one from the container
registry. --tag-latest, --release-type <channel>, --tag-prefix <prefix> and the
interactive Release type (lts|u|qr|nightly|master|latest), prefix, or specific tag
prompt all feed the same resolver.
| Channel | Means | Resolves to (verified 2026-09-10) |
|---|---|---|
lts |
Long-Term Support quarterly (the default when nothing is specified) | 2026.q1.12-lts |
qr |
Quarterly Release โ the newest quarterly, LTS or not | 2026.q3.2 |
latest / any |
The newest release of any family, quarterly preferred | 2026.q3.2 |
u |
The legacy 7.4.13-uNNN update line |
7.4.13-u152 |
nightly / master |
The floating nightly tag, not a timestamped build | 7.4.13.nightly |
a prefix (e.g. 2025.q1) |
The newest patch within that prefix | 2025.q1.27-lts |
# The newest quarterly release, whatever quarter that currently is
ldm run demo --tag-latest --release-type qr
# The newest patch of a specific quarter
ldm run demo --tag-prefix 2025.q1
Important
--portal has only one usable channel. liferay/portal publishes no quarterly,
LTS (*-lts), update (*-u), *-qr or nightly tags at all โ its releases are named
7.4.3.132-ga132. Only latest/any (or an explicit -t) can resolve for a Portal
CE project; every other release type correctly reports that nothing was found.
Results are cached in ~/.liferay_docker_cache.json for 24 hours per
(repository, release type, prefix) combination. Pass --refresh to force a live
lookup โ worth knowing, because a cached answer for one channel can make an unrelated
broken channel look healthy (LDM-#1647).
A withdrawn tag is skipped rather than offered. liferay/dxp publishes a
7.4.13-u999 placeholder whose only image the registry reports as inactive, and on a
version sort it beats the real newest update release โ so the winner of a legacy
*-uNNN lookup is checked against the registry before being used (LDM-#1649). The check
fails open: an unreachable registry keeps the candidate rather than discarding it.
If the registry cannot be reached at all, discovery falls back to
releases.json, which covers every
quarterly and GA release but not nightlies โ an unpublished build is not a release
(LDM-#1648).
Workspace Product Pin¶
If the project is linked to a workspace whose gradle.properties sets
liferay.workspace.product, LDM compares that pin against the tag it resolved and
warns when they disagree (LDM-#1658):
โ ๏ธ The linked workspace pins liferay.workspace.product=dxp-2026.q3.0
(Docker tag 2026.q3.0), but this run resolved 2026.q3.2.
Interactively LDM then offers the pinned tag, defaulting to yes. With
-y/--non-interactive it warns and proceeds, so existing automation is
unaffected.
This matters more than a version mismatch usually does. A shared OSGi
fragment/override bundle carries Import-Package ranges bound to one product
line, and bnd copies a declared range into the manifest verbatim โ so a bundle
built for dxp-2026.q3.0 does not resolve on 2026.q3.2. The failure shows up
as an unresolved bundle in the OSGi log at boot, a long way from the tag decision
that caused it.
LDM stays quiet when:
-t/--tagwas given โ an explicit tag is a decision, not an accident;- the workspace has no
liferay.workspace.product; - the pin and the resolved tag are the same release.
dxp-2026.q1.7and2026.q1.7-ltscount as agreement.
ldm set-version <product-key> writes the pin; both a plain workspace
(gradle.properties at the root) and a Liferay Cloud workspace
(liferay/gradle.properties) are recognised.
Compatibility Claims
¶
A pin states a version but makes no claim. tag in an .ldmp manifest โ and
liferay.workspace.product in a workspace โ is a fact about how the package was
built; every consumer reads it as a claim about what it supports. Those
are different, and a consumer deviating from the pin could not tell whether they
were doing something the publisher tested and rejected, something nobody has
tried, or something fine (LDM-#1791).
Important
There is no such thing as a "stale" pin. A package legitimately pins an older line because a later one was tried and failed, or because nobody has had time to qualify one. The pin is the statement of what is known to work.
A package may therefore carry a separate compatibility claim, recording what
it has actually been tested on. Every .ldmp published before v2.22.0 carries
none, and that absence is the default: no claim.
Whenever ldm run settles on a tag it says which of the three applies:
โน Liferay tag 2026.q1.7-lts is declared verified by the package
(evidence: https://โฆ/actions/runs/123, tested 2026-09-17, with LDM 2.22.0).
โน The package declares 2026.q1.7-lts verified; this run resolved
2026.q1.12-lts, unverified -- nothing has been tested for it.
โน Liferay tag 2026.q1.12-lts carries no compatibility claim: the package
records the tag it was built with, not one it has been tested against.
The third line is the default, and it is printed rather than left silent deliberately. LDM-#1782 was silence being read as success.
The declared ceiling, and --ignore-verified-ceiling¶
Declaring a refuted tag โ one that was tried and failed โ sets a ceiling at
the highest verified tag. A refutation is a tested fact, so LDM refuses to boot
above it and exits 1:
โ The package declares 2026.q1.7-lts as its verified ceiling, and
2026.q1.12-lts is above it.
2026.q1.12-lts was tried and it failed (evidence: โฆ, tested 2026-09-17).
A pin records the version that has been TESTED -- this one is a tested
negative result, not a package lagging behind.
Pass --ignore-verified-ceiling to proceed anyway. The override is
recorded in the project metadata.
Packages get fixed, and a consumer may know more than the publisher did, so
--ignore-verified-ceiling proceeds anyway. It is accepted by every command
that boots a project โ run/up, init, import, link, clone,
init-from, restore and quickstart โ because the ceiling is enforced in the
run pipeline they all share. It
is never silent: LDM warns, and writes a compatibility_override record into the
project metadata naming the tag, the ceiling it crossed, the evidence and the
timestamp.
Nothing is refused that LDM cannot reason about. A tag with no version in it โ
nightly, master โ orders against nothing and is announced as unknown, never
refused. A verified claim with no refutation is a point claim and sets no
ceiling at all: promoting it to a refusal would invent a tested fact that nobody
tested.
See package for how a publisher declares a claim.
--vanilla Switch
¶
Bypasses downloading the pre-warmed database seed from GitHub releases. Spawns the Liferay project stack with a pristine, empty database.
--vanilla is an intent flag: nothing pre-populated. It implies --no-seed, refuses to combine with --samples or --snapshot, and wipes persisted OSGi state. --no-seed remains the narrower mechanism flag โ skip the pre-warmed seed only โ and still combines freely with --samples/--snapshot.
init¶
Initialize project scaffolding (creating .liferay-docker.meta, portal-ext.properties, etc.) without actually starting the Docker containers. Accepts many of the same configuration flags as run.
External Database Integration¶
LDM supports connecting your local Liferay instance to an external database (e.g., a shared development database or a standalone local database server) instead of running a database container within the project's Docker Compose stack.
To initialize a project with an external database:
LDM will launch an interactive wizard to gather your JDBC connection details (Host, Port, Database Name, Username, Password) and generate the necessary properties in your portal-ext.properties. The database service container is entirely omitted from the generated stack.
Name the engine. external is a mode -- it says who runs the database, not which one it is -- so pairing it with --db postgresql or --db mysql lets LDM resolve the JDBC driver and the per-tag Hibernate dialect for your server, exactly as it does for a database it runs itself.
Note
--db external still works and is the older spelling of the same thing (LDM-#1511). LDM infers the engine from the JDBC URL scheme you supply -- jdbc:postgresql:// means PostgreSQL, jdbc:mysql:// and jdbc:mariadb:// mean MySQL -- and writes the driver and dialect accordingly. Where the scheme names an engine LDM does not support (an Oracle URL, say), it falls back to writing only the URL, username and password, which is what it always did. Existing projects whose meta records db_type: "external" are read forward the same way and keep booting unchanged.
SSL Defaults (New Projects)¶
LDM uses smarter defaults for SSL based on your hostname. When a custom --host-name is used, SSL is enabled by default to support modern Liferay features like Client Extensions.
Tip
SSL Hostname Prompt: If you explicitly pass the --ssl flag without providing a --host-name, LDM will interactively prompt you for a custom virtual hostname. This allows LDM to attempt to inject the custom domain into your /etc/hosts file automatically. (In non-interactive mode -y, it safely defaults to localhost).
| Command | Host Name | SSL Default | Access URL |
|---|---|---|---|
ldm run |
localhost |
False |
http://localhost:8080 |
ldm run --ssl |
Prompts User | True |
https://<prompted-host> |
ldm run --host-name my.local |
my.local |
True |
https://my.local |
ldm run --no-ssl |
localhost |
False |
http://localhost:8080 |
ldm run --host-name my.local --no-ssl |
my.local |
False |
http://my.local:8080 |
๐ก๏ธ Modern Liferay & JDK 17+ Standards¶
LDM automatically hardens modern environments (DXP 2024+ and modern Quarterly Releases) to ensure stable startup:
- JVM Module Exports: Automatically injects mandatory
--add-opensflags for JDK 17+ (coveringjava.net,java.lang.reflect,security, and more). - Hardened MySQL 8.4 (LTS):
- Standardized on the MariaDB JDBC Driver and
MariaDB103Dialectto mirror Liferay Cloud (LXC) environments. - Forces
mysql_native_passwordauthentication for CI compatibility. - Includes performance-optimized connection parameters (e.g.,
rewriteBatchedStatements,prepStmtCacheSize). - Redline Configuration: Explicitly sets
hibernate.dialectandjdbc.default.*properties inportal-ext.propertiesto ensure reliable interpretation of mixed-case keys (likedriverClassName). - Prioritizes
LIFERAY_JDBC_DEFAULT_*environment variables ONLY for runtime user overrides; LDM baseline always usesportal-ext.properties. - Proactive Boot Sequencing: Configures
depends_onwith healthchecks to ensure Liferay only starts once the database is fully ready to accept connections.
link (alias: init-from
)
¶
Initialize a project from a source workspace and establish a persistent link. This command records the workspace path in the project metadata and automatically starts the monitor process to sync your code changes in real-time. If a Liferay Cloud Workspace is detected, it will also launch an interactive wizard to hydrate the data from the remote environment.
Note
ldm init-from is a supported legacy alias for ldm link and will continue to work. New scripts and documentation should use ldm link.
# ldm link <source_path> [project_name] [--host-name custom.local]
ldm link ~/repos/my-workspace my-project --host-name forge.demo
# Link with the latest tag and disable CAPTCHAs for CI testing
ldm link ~/repos/my-workspace my-ci-project -y --tag-latest --no-captcha
# Manually bind a Liferay Cloud project ID to the local workspace
ldm link ~/repos/my-workspace my-project --cloud-project lctintranet
# Record the link without starting the file watcher, and without booting
ldm link ~/repos/my-workspace my-project --no-monitor --no-run
# Legacy alias (still works)
ldm init-from ~/repos/my-workspace my-project
Note
ldm link normally ends by starting a file watcher, which runs until Ctrl-C.
--no-monitor records the link and returns instead; the link is persisted
either way, so ldm monitor -p <project> attaches later with no path argument.
--no-run sets the project up without booting it. Together they make
ldm link scriptable (LDM-#1689).
Liferay Cloud workspaces¶
A Liferay Cloud (LCP) repository keeps its Liferay Workspace under liferay/
and its standalone services -- any sibling directory holding both an LCP.json
and a Dockerfile -- beside it. ldm link resolves that layout, imports the
workspace code from liferay/, and copies each standalone service into
<project>/services/. The infrastructure directories (backup, ci,
database, search, webserver) are skipped.
The Liferay Cloud project ID is resolved in this order:
--cloud-project, if given;- the
idfield in the repository-rootLCP.json/lcp.json; - an interactive prompt, defaulting to the repository directory name.
With -y/--non-interactive and no ID available from the first two, LDM
refuses with exit code 2 rather than guessing -- a wrong ID would run
lcp commands against someone else's project.
clone
¶
Clone a remote Git repository and initialize an LDM project from it. Unlike link, which connects to an existing local workspace, clone handles the Git clone step automatically and sets up hot-reload mounts in one command.
# Clone a Git repository and initialize an LDM project
ldm clone https://github.com/my-org/my-repo.git
# Clone into a named project
ldm clone https://github.com/my-org/my-repo.git my-project
# Clone using SSH and specify a host name
ldm clone git@github.com:my-org/my-repo.git my-project --host-name demo.local
# Clone and force a full Gradle build on initialization
ldm clone https://github.com/my-org/my-repo.git --build
import (Data Packages Only)¶
Import a pre-built LDM Package (.ldmp) or remote package URL. As of v2.15.16, ldm import is restricted to data packages only. To clone a Git repository use ldm clone; to link a local workspace use ldm link.
# Import a local .ldmp package
ldm import ~/Downloads/my-project.ldmp
# Import a remote .ldmp package by URL
ldm import https://example.com/assets/my-project.ldmp
# Import from a GitHub Release (auto-detects .ldmp asset)
ldm import https://github.com/my-org/my-repo
# Import using a specific release type filter
ldm import https://github.com/my-org/my-repo --tag-latest --release-type qr
# Manually bind a Liferay Cloud project ID
ldm import ~/Downloads/my-project.ldmp --cloud-project lctintranet
quickstart¶
Bootstrap and start a predefined accelerator demo stack in one command. Downloads target repositories, configures metadata, and automatically starts the environment.
# Bootstrap the AICA (AI Commerce Accelerator) template
ldm quickstart aica
# Bootstrap and expose the stack dynamically using lfr-tunnel
ldm quickstart aica --share --share-subdomain my-custom-demo
# Bootstrap with a custom target directory and project name
ldm quickstart aica --name custom-aica-demo
# Re-run over an existing project. Without --replace quickstart refuses (and
# asks, interactively), because replacing deletes the project's database
# and volumes. A project that records no LDM version (any quickstart project
# made before 2.26.7) gets its database and search modes from your defaults;
# one that records a version keeps the modes it had.
ldm quickstart aica --replace
# Bootstrap with a custom domain and SSL enabled
ldm quickstart aica --host-name aica.demo --ssl
# Bootstrap with a custom name and SSL explicitly disabled
ldm quickstart aica --name aica-http --host-name aica.demo --no-ssl
With --share, the tunnel is started by the run itself once the portal is ready, so its URL arrives at the end of the boot rather than before it.
Custom templates and repository mappings can be configured by defining overrides in ~/.ldm_templates.json. Each key is a template name (matched case-insensitively) and needs both repo and default_name; an entry missing either is ignored. Set "private": true for a repository that needs your GitHub token. A key named aica replaces the built-in.
An unrecognized name is refused with the list of templates available, built-in and custom.
package¶
Package a project snapshot (code elements, database, and volumes) into a portable LDM package (.ldmp archive) alongside a SHA-256 checksum signature (.ldmp.sha256).
# Package the current project
ldm package
# Package a specific project, outputting to a custom directory
ldm package my-project -o /tmp/packages
# Package using a specific repository manifest identifier and the latest snapshot
ldm package my-project --repo my-owner/my-repo --use-latest
# Override target host-name and SSL defaults in the package manifest
ldm package my-project --host-name custom.demo --ssl
ldm package my-project --host-name custom.demo --no-ssl
Declaring What Was Tested
¶
The manifest's tag says how the package was built. These three flags say
what it has been tested on โ see Compatibility Claims under
run for how a consumer then reads it.
| Flag | Meaning |
|---|---|
--verified <tag> |
This package has been tested against this Liferay tag and it worked. |
--refuted <tag> |
This tag was tried and it failed. Declares the verified ceiling. |
--evidence <ref> |
URL or reference to whatever produced that result. Mandatory for either. |
# A point claim: tested here, nothing said about anything else
ldm package my-project \
--verified 2026.q1.7-lts \
--evidence https://github.com/my-org/my-repo/actions/runs/123
# A ceiling: verified at q1.7, and q1.12 was tried and failed
ldm package my-project \
--verified 2026.q1.7-lts \
--refuted 2026.q1.12-lts \
--evidence https://github.com/my-org/my-repo/issues/1782
--evidence is not optional politeness. A claim that LDM could mint without
anything having run becomes noise within a month, so a claim without it is
refused with exit code 1, before any snapshot is taken. LDM never manufactures
a claim of its own โ a package that declares nothing keeps the default of "no
claim", which is what every package published before v2.22.0 already carries.
The claim is written into the manifest under compatibility, with a tested_on
date and the LDM version that recorded it (a January claim is weaker in
December), and each entry has a platform slot for the day claims become
architecture-qualified โ 2026.q1.7-lts has booted green on aarch64 while
failing on an x86_64 runner, so that dimension is real even though LDM does
not yet read it.
Data Management Commands¶
LDM includes powerful commands for managing your project's database, OSGi state, and Elasticsearch indices. For full details on the following commands, please see the Data Management Guide.
snapshot/restore: Backup and recover exact project states.package: Export a project snapshot into a portable.ldmppackage.hydrate: Create or restore a project from a local Liferay Cloud backup.cloud-fetch: Sync an existing local project directly with a live Liferay Cloud (LCP) environment.reset/re-seed: Surgically clear data folders or completely wipe a project back to its original vanilla state.
monitor¶
Restarts the background watch process for a project linked to a Liferay workspace. This command can only be used for projects created with ldm link (or the legacy ldm init-from). It automatically syncs built artifacts (.jar, .war, .zip) whenever they are updated in the workspace.
wait¶
Blocks the terminal until the Liferay project has fully started, initialized, and is ready to accept HTTP traffic.
ldm wait [project]
# Example:
ldm wait demo
# Probe a URL of your choosing instead of the derived one
ldm wait demo --node node-b --probe-url https://my-project.demo
Choosing the URL to probe (--probe-url)¶
By default the probe follows the target: against a remote node it dials that
node's address, which is what stops it checking 127.0.0.1 on your own
machine while the containers run elsewhere.
That default is right and is unchanged. But LDM cannot infer the correct target in every topology. With SSL on a remote node the certificate is issued for the project's host name while the derived URL dials the node's address, so the probe and the certificate disagree by construction โ and if you reach the stack through an SSH tunnel, the address LDM can see is not the one your client uses at all.
--probe-url replaces the derived URL verbatim. Nothing is rewritten: not the
scheme, not the host, not the port. Omit it and resolution behaves exactly as
before, on local and remote targets alike.
Note
This affects ldm wait only. ldm run's built-in wait does not make an HTTP
probe โ it watches container health and Liferay's own logs โ so there is
nothing there for this switch to override.
The wait sequence uses a progressively aggressive validation strategy with visual milestone tracking:
- Container Health: Polls the Docker daemon until the
liferaycontainer reaches ahealthystate. - OSGi Subsystem Readiness: Connects to the internal Gogo shell (via telnet) to ensure all core OSGi bundles are completely
Activewithout any unresolved dependencies. - HTTP Server Validation: Repeatedly sends lightweight HTTP probes to Liferay's primary dashboard endpoints (
/c/portal/layoutand/o/api) looking for a200 OK.
This guarantees that scripts or CI/CD pipelines leveraging ldm wait will not proceed until Liferay is strictly capable of serving web requests, mitigating race conditions during automated deployments or integration tests.
logs¶
View real-time logs. Supports filtering by project, specific services, global infrastructure, or individual scaled replicas.
ldm logs [project] [service1] [service2] ...
# Examples:
ldm logs # All logs for current project
ldm logs demo # All logs for 'demo' project
ldm logs -f # Follow logs continuously
ldm logs -n 250 # Show last 250 lines (default: 100)
ldm logs -t # Show timestamps
ldm logs --since 1h # Show logs from the last hour
ldm logs --until 10m # Show logs until 10 minutes ago
ldm logs --no-wait # Tailing usually waits for containers to be ready; use this to tail immediately
ldm logs --export # Export logs to a local file
ldm logs --include-infra # Include global infrastructure logs when viewing/exporting project logs
ldm logs --infra # Show logs for all global infrastructure (ES, Proxy, etc.)
ldm logs --infra es # Show logs only for Global Elasticsearch
ldm logs --infra proxy # Show logs only for Global SSL Proxy
ldm logs demo liferay # Only Liferay logs for 'demo'
ldm logs demo liferay my-ext # Multi-service tailing (all replicas)
Targeting a Specific Scaled Replica (--instance N / -i N)¶
When a service is scaled to multiple replicas (e.g. ldm scale demo liferay=3), ldm logs streams from all instances simultaneously by default. Use --instance N to isolate a single replica:
# Stream logs from replica 2 of the liferay service only
ldm logs demo liferay --instance 2
# Short form, following in real time
ldm logs demo liferay -i 2 -f
# Tail the last 50 lines of replica 3
ldm logs demo liferay -i 3 -n 50
Note
--instance routes to docker logs directly (bypassing Compose) so it targets the exact container. The container name is resolved from project metadata using the standard naming convention {project}-{service}-{index} (e.g. demo-liferay-2). This pattern is stored automatically when you run ldm scale, making subsequent lookups instant.
Tip
If you request an out-of-range instance (e.g. --instance 5 when only 3 replicas are running), LDM will report the valid range and exit cleanly.
start, stop, restart, down (alias: rm)¶
Manage the lifecycle of a project or a specific service.
Note
The start command simply boots existing containers without running configuration or network scaffolding -- it never creates a container. If a project does not exist, start will immediately fail and recommend ldm run. The same applies if the project exists (its meta and volumes are intact) but its containers were removed directly, e.g. via docker rm (LDM-#1870, reported as Not Created by ldm list): rather than letting the underlying docker compose start fail with its own internal wording about a service it never named, ldm start refuses up front and names ldm run, which recreates the containers from the existing volumes without losing data.
ldm start [project] [service] # Start existing containers
ldm stop [project] [service] # Stop containers gracefully
ldm restart [project] [service] # Stop and then start
ldm down [project] [service] # Remove containers (and optionally -v volumes)
ldm rm [project] # Alias for 'down'
start confirms what it did and closes with a next-step hint, rather than succeeding silently (LDM-#1937):
โ
Project 'my-project' started.
๐ก Next step: Run 'ldm status' to view environment status, or 'ldm logs -f' to tail the logs.
stop points back at start, because stop stops containers without removing them -- they still exist, so ldm start is a plain docker compose start rather than a full pipeline run:
๐ก Next step: Run 'ldm start' to start the container again, or 'ldm status' to view environment status.
That hint previously named ldm run. ldm run remains the correct command when containers were removed rather than stopped (the Not Created state described above), but having just stopped them, ldm stop knows that is not the state the project is in.
Options¶
--share(Only forstartandrun): Starts a secure tunnel once the containers are up. Onstartthis is the point of the flag:runreconfigures the project before booting it, which is exactly what someone sharing an already-configured project does not want. Cannot be combined with--all-- a tunnel leases one subdomain and forwards to one target -- and the refusal arrives before any project is started.--share-subdomain,--share-domain,--share-url,--share-provider,--share-image,--share-inspector(Only forstartandrun): As forldm share start's--subdomain,--domain,--url,--provider,--imageand--inspector; see Sharing & Tunnels. Sharing is auxiliary tostart: the project starts, and then it is shared --ldm start --sharesaves you runningldm startandldm share startin sequence.--clean-state(Only forstartandrun): Explicitly wipes the contents of the OSGi state volume before starting the container to remove any stale bundle locks.--fix-permissions(Only forstartandrun): Forces root permission reclamation on bind-mounted host directories. Useful for resolving Liferay lock crashes when running macOS/Windows Docker Desktop against external drives or network shares.--force-recreate(Forrun,start, andrestart): Recreates containers even if their configuration and image haven't changed. Note: Since native Dockerstartdoesn't support recreation, passing this toldm startorldm restartwill smoothly intercept and route toup -d --force-recreateunder the hood.--force-portal-patches(Forrun,start, andrestart): Applies JARs from the project'sportal-patches/directory even when their recordedintroduced_inrelease does not match the release being booted, or when the target JAR is absent from the image. Without it, a release-line mismatch aborts the boot. See Patching Core Portal JARs.--verify-mac(Only forstartandrestart): Also checks the pinned MAC against the node's own interfaces, over SSH. The MAC comparison -- container against configured value -- always runs on these commands and is free, being a localdocker inspect; this adds the extra round trip that answers a different question: "is the configured address one this node actually has?" It is off by default because that round trip was measured turning a 2.9s restart into 4.3s, and because a wrong pin is already caught once atldm target add, where a typo is cheapest to fix. Use it when a node's interfaces may have changed since it was registered.-V,--volumes(Only fordown/rm): Also removes the project's Docker Compose volumes. Implied automatically by--delete.--infra(Only fordown/rm): Also tears down the shared global infrastructure (Proxy, Search, etc.), independently of whatever project is targeted -- can be combined with a project target or passed alone.--clean-hosts(Only fordown/rm): Removes the project's entries from your/etc/hostsfile.-
-d,--delete(Only fordown/rm): Escalates teardown beyond just stopping/removing containers -- drops the project's schema from the shared database (if it uses shared-mode DB), unregisters the project, and permanently deletes its directory from disk. This cannot be undone.ldm rm/ldm downwithout--deleteonly tears down containers and keeps the project registered, so it can beldm runagain later;--deleteis the one-way, destructive option. -
--keep-credentials(Only fordown/rm, with--delete): Keeps database and admin passwords in the~/.ldm/removedarchive that--deletewrites. They are removed by default, with a marker left in place of each value; opting in means the archive holds them in plaintext and securing it becomes yours to do. Set it permanently withldm config set tombstone_keep_credentials true. See Data Management (LDM-#1703).
Interactively, --delete now lists what is about to go and asks before touching anything (LDM-#1703):
โ ๏ธ The following will be permanently removed:
my-project: 1.1 GB, no snapshot -- database state will be lost
โ Permanently delete this project, including containers and volumes? [y/N]:
The prompt defaults to no, and reports whether a snapshot exists -- ldm snapshot is the real undo for database state, and it is the one thing --delete cannot give back. With -y/--non-interactive there is no prompt and behaviour is unchanged, so existing automation is unaffected.
Examples¶
ldm stop --all # Stop all running projects in the workspace
ldm restart --all # Restart all running projects
ldm restart # Full stack restart (graceful stop + run)
ldm down --volumes # Tear down stack and clear all database/data state
ldm down --infra # Also tear down the global infrastructure (Proxy, Search)
ldm down --clean-hosts # Remove project entries from your /etc/hosts file upon deletion
ldm rm --delete # Permanently delete the project's directory, DB schema, and registry entry
status¶
View the status of all projects in the current workspace.
ldm status
# Machine-readable output for scripts/automation -- a stable JSON object
# with "infrastructure" and "projects" arrays; each project entry gains a
# "status" field alongside the existing "running" boolean.
ldm status --json
Tip
Projects marked with a ๐ฑ (seedling) emoji were initialized from a Seeded State, meaning they started with a pre-calculated database and OSGi cache for near-instant boot times.
Like ldm list (see above), ldm status/ldm status --detailed distinguishes Not Created (LDM-#1872) from Stopped: Not Created means no container matching the project exists at all (typically docker rm-ed directly, meta/volumes intact), while Stopped means a container was found but isn't running. Previously both cases fell through to the same bare Stopped label here, unlike ldm list, which already made this distinction (LDM-#1870, #1876) -- this closes that inconsistency. ldm run recreates a Not Created project's containers from the existing volumes; ldm start cannot, for the same reason described under start below.
deploy¶
Hot-deploy built artifacts or rebuild extension images.
ldm deploy [project] [service] --rebuild
# Examples:
ldm deploy # Sync all artifacts and refresh stack
ldm deploy demo my-ext --rebuild # Rebuild and restart one extension
scale¶
Scale services within a project for multi-node simulation and clustering tests.
ldm scale [project] service=count
# Examples:
ldm scale demo liferay=2 # Scale Liferay to 2 nodes (enables clustering)
ldm scale demo my-ext=3 # Scale a client extension to 3 nodes
shell & gogo¶
Jump into a container shell for deep inspection or connect to the OSGi Gogo console for runtime management.
Interactive Shell Examples:
# Enter bash in the Liferay container
ldm shell demo
# Common Shell Tasks (inside container):
# 1. View live Tomcat logs
cd tomcat/logs && tail -f catalina.out
# 2. Check injected environment variables
env | grep LIFERAY
# 3. Verify mounted OSGi configurations
ls osgi/configs
Gogo Shell Examples:
# Connect to the Gogo shell (requires --gogo-port during run)
ldm gogo demo
# Common Gogo Commands:
# 1. List all active bundles
lb
# 2. Check for unresolved dependencies
diag
# 3. List declarative services (SCR)
scr:list
config env (legacy: env)¶
Manage persistent environment variables in project metadata.
ldm config env [project] KEY=VALUE
ldm config env [project] --remove KEY
ldm config env [project] -s liferay KEY=VALUE # Target a specific service instead of global
ldm config env [project] --import # Import variables from a local .env file
ldm config env # Interactive manager (view and edit all)
# Legacy flat form (still works):
ldm env [project] KEY=VALUE
config feature (legacy: feature)¶
Quickly toggle Liferay feature flags without manually editing portal-ext.properties. Requires a project restart to take effect.
ldm config feature [project] --enable LPS-122920
ldm config feature [project] --disable LPS-111111 LPS-222222
# Legacy flat form (still works):
ldm feature [project] --enable LPS-122920
config edit (legacy: edit)¶
Rapidly modify project configuration files in your system's $EDITOR (defaults to vi or notepad).
ldm config edit [project] # Edit .liferay-docker.meta
ldm config edit [project] --target properties # Edit portal-ext.properties
# Legacy flat form (still works):
ldm edit [project]
config log-level (legacy: log-level)¶
Manage Liferay internal logging levels (Log4j2) without restarts.
# List current custom levels
ldm config log-level --list
# Set a specific category to DEBUG
ldm config log-level [project] --bundle portal --category com.liferay.portal --level DEBUG
# Interactive configuration
ldm config log-level
# Legacy flat form (still works):
ldm log-level [project] --list
Global Flags¶
The following flags can be passed to almost any command:
-v,--verbose: Enable verbose debug logging to trace exact shell commands, API calls, and Docker interactions.--info: Show informational logging (a middle tier between standard output and debug).-y,--non-interactive: Accept all defaults and skip confirmation prompts.--upgrade-db: Force-enables Liferay's database auto-upgrade tool on startup (LIFERAY_UPGRADE_PERIOD_DATABASE_PERIOD_AUTO_PERIOD_RUN=true).--no-upgrade-db: Force-disables Liferay's database auto-upgrade tool.--backup-on-upgrade: Force-enables automatic database backup snapshot creation before running version upgrades.--no-backup-on-upgrade: Force-disables automatic database backup snapshot creation before running version upgrades.--tag-prefix: Force specific tag discovery prefix when resolving latest tags.--skip-project: Skips project discovery. Useful for global diagnostics likeldm doctor --skip-project.
Last Updated: 2026-10-23 | Last Reviewed: 2026-10-23