Workspace Import and Packaging Guide¶
Liferay Docker Manager (LDM) offers a portable mechanism to export, share, and import full development stacks—including code, configurations, database snapshots, and volume assets. This guide explains how to package your workspace and import it on any machine.
Workspace Source vs. Hydrated Packages (.ldmp)¶
To avoid confusion, it is important to distinguish between developing inside a Liferay Workspace and importing a pre-built LDM Package (.ldmp):
A. Raw Liferay Workspaces (Source Code)¶
- What it is: A standard directory structure containing Java source files, client extension source code (
client-extension.yaml), and Gradle scripts. - How it works: You link it with LDM via
ldm link /path/to/workspace. - Deployment: LDM runs
gradlew deployto compile Java bundles and package client extensions into zip archives, then hot-deploys them into Liferay's/deploydirectory. - Best For: Active developer code writing and file editing.
B. Hydrated LDM Packages (.ldmp)¶
- What it is: A portable compressed archive containing pre-built assets, database snapshots, and volume directories.
- How it works: You import it with
ldm import my-project.ldmp. - Deployment: The database schema and custom assets are already compiled and pre-hydrated inside Liferay's internal directories. No Gradle compilation or local Java SDK is needed to start running.
- Best For: Mirrored backups, distributing quickstart demos, and deploying final pre-configured states.
1. Unified Import Workflow (ldm import)¶
The ldm import command is a polymorphic entry point. It automatically detects the type of source you provide and configures the environment accordingly.
Supported Source Types¶
Important
As of v2.15.16, ldm import is restricted to data packages only (.ldmp archives and remote URLs). To clone a Git repository or link a local workspace, use ldm clone or ldm link respectively.
| Source Type | Command | Behavior & Details |
|---|---|---|
Local LDM Package (.ldmp) |
ldm import ~/Downloads/my-project.ldmp |
Extracts the archive, restores the database snapshot and named volumes, writes metadata, and boots the Docker stack. |
Remote .ldmp URL |
ldm import https://example.com/assets/my-project.ldmp |
Downloads the remote archive, verifies the .sha256 signature if available, and performs a local package import. |
| GitHub Release Package | ldm import https://github.com/my-org/my-repo |
Automatically queries the GitHub Release API. If a .ldmp asset exists, it downloads and restores it directly. |
| Git Repository URL | ldm clone https://github.com/my-org/my-repo.git |
Clones the repository to a local folder and sets up a standard workspace with hot-reload mounts. |
| Local Liferay Workspace | ldm link /path/to/local/liferay-workspace |
Maps folders, executes gradlew build (if --build is specified), and configures the container paths. |
| Local Cloud Workspace | ldm link /path/to/local/lcp-workspace |
Detects LCP.json, resolves the nested liferay/ folder structure, and configures environment variables and properties. |
Site Initializers Are Held Back¶
ldm import deploys every built client extension it finds into
osgi/client-extensions/ — with one exception. A site-initializer client
extension is staged under <project>/.ldm/deferred-client-extensions/ instead,
and deployed once the portal reports healthy.
A site initializer present at the first boot of a fresh database dies inside
SiteInitializerClientExtension.addingBundle with an NPE from
PortalImpl.getCanonicalURL, logs STARTED regardless, and silently creates
no site. Detection is by the Liferay-Client-Extension-Site-Initializer header
in the artifact's WEB-INF/liferay-plugin-package.properties. See
Client Extensions
for the full mechanism and the manual deployment command.
Manifest Verification¶
Every .ldmp carries a meta manifest describing the package. It used to be
checked only when the package came from a GitHub Release; a local file or a
.ldmp URL was read with a tolerant parser and any problem with it was passed
over silently (LDM-#1621). It is now checked on every input, though not
every check behaves the same way, because a local file has less context around
it than a release asset does.
| Condition | Local file / .ldmp URL |
GitHub Release asset |
|---|---|---|
| Manifest is missing | Treated as a plain workspace archive, not a package | Refused |
| Manifest cannot be parsed | Refused (exit 1) |
Refused |
db_type names an unknown engine |
Refused (exit 1) |
Refused |
| Claims client extensions or OSGi modules but lists none, and ships some | Listing recovered from the package contents, with a warning | Same |
| Claims client extensions or OSGi modules but lists none, and ships none | Warning; the import continues | Refused |
github_repository absent |
Warning; the import continues | Refused |
github_repository names a different repository |
Reported with --info/--verbose; the import continues |
Refused |
Two notes on the differences:
- The origin checks cannot apply to a local file. They exist to catch a release asset claiming to come from a repository other than the one it was downloaded from. A file you chose yourself has no fetch origin for the manifest to contradict, so there is nothing to compare against — the declaration is reported as unverified rather than trusted or refused.
- A parse failure is refused because it is not recoverable by carrying on.
The manifest is where
taganddb_typecome from. An unreadable one used to import "successfully" while silently dropping both, so the database dump was restored into whichever engine the defaults happened to pick. If you hit this, fix themetafile inside the archive or rebuild the package withldm packageon this version of LDM. - "Cannot be parsed" now includes content that is not metadata at all.
The manifest may be either JSON or the legacy flat
key+=+valueformat, and until LDM-#1629 anything that was neither — most importantly a saved HTTP error page, from a download that 404'd and was written without a status check — was read as an empty manifest rather than a broken one. On the GitHub Release path that surfaced as the misleadingSecurity Violation: Manifest is missing 'github_repository'. It is now refused as the parse failure it is, and the file is no longer overwritten by LDM's format auto-upgrade, so the archive still contains the evidence. Ametafile that is genuinely empty — zero bytes, or nothing but blank lines and#comments — is still read as empty, not refused.
What the consumer's machine decides, not the package¶
Some settings describe the machine LDM is running on rather than the package,
and import regenerates those rather than carrying the publisher's across.
jdbc.default.url and virtual.hosts.valid.hosts have always worked this way.
Search topology joins them (LDM-#1773).
So do the database and search modes. A project created by an .ldmp import
(ldm quickstart, or ldm import of a GitHub release) picks them from your
defaults, exactly as ldm run does, and says which it got on its first boot.
Until LDM-#2267 such a project carried no record of the LDM version that made
it. LDM therefore treated it as predating shared infrastructure, and it always
got an isolated database and sidecar search, whatever your defaults said.
Importing over an existing project leaves its modes alone, because its data
already lives in them.
ldm quickstart --replace (or answering yes to its prompt) is different,
because the replacement deletes that data (LDM-#2275). A replaced project that
records no LDM version, which is any project made by a quickstart before
LDM-#2267, gets its modes from your defaults as a new project would. One that
records a version keeps its modes. Either way, the next boot says which modes
it got and whether they were kept.
Two routes used to carry the publisher's search settings in and win:
| Route | Where it lands | Why it won |
|---|---|---|
custom_env in the package manifest |
appended to the Liferay service environment | Compose resolves a duplicated key to the later entry, and custom_env is appended after LDM's own search block |
the packaged portal-ext.properties |
layer 2 and layer 5 of the properties cascade | module.framework.properties.* is the highest-precedence route into that configuration |
A package built on a --sidecar machine therefore made a shared-search project
start an embedded Elasticsearch inside the Liferay container as well. Its
sidecar JVM defaults to -Xmx2g, so a stack sized for a 3 GB ceiling really
asked for 5 GB — enough to be OOM-killed on an 8 GB machine, which is how this
was found.
On import, LDM now removes its own search settings from both and says what it removed:
⚠️ Discarded 2 search setting(s) from the package's portal-ext.properties;
this machine resolved search_mode 'shared'.
- module.framework.properties.com.liferay.portal.search.elasticsearch7
.configuration.ElasticsearchConfiguration.operationMode=EMBEDDED
- ...sidecarHttpPort=9201
The keys it owns are everything beginning LIFERAY_ELASTICSEARCH in the
environment, and everything beginning
module.framework.properties.com.liferay.portal.search.elasticsearch in the
properties. Everything else a publisher sets is left exactly as they wrote it,
comments and ordering included.
For publishers: you no longer need to sanitise search settings out of a
package — but if you set one deliberately, the announcement is where you will
see it discarded. Nothing else about custom_env changed.
2. Packaging Workspaces (ldm package)¶
The ldm package command bundles a project's files, configurations, database, and volume assets into a single portable .ldmp tarball, complete with a .sha256 signature.
Step-by-Step: Creating an .ldmp Package¶
You can create .ldmp packages locally using three main packaging strategies:
Method A: Fresh Snapshot & Package (Standard)¶
If your Liferay Docker stack is currently running and contains the data state you want to distribute, you can build a new snapshot and package it in a single command:
- Ensure your container stack is active:
ldm status - Run the package command:
This command automatically triggers a fresh snapshot creation (ldm snapshot), generates the package tarball, calculates its SHA-256 hash, and saves both files into the ./dist folder.
Method B: Package the Latest Existing Snapshot¶
If you already took a snapshot earlier (e.g. using ldm snapshot) and want to package it without waiting to take a new one:
- Run the package command with the
--use-latestflag:
LDM will query your project's snapshots/ directory, locate the most recently created snapshot directory, and package it directly.
Method C: Package a Specific Target Snapshot¶
If you want to package a specific snapshot directory from your historical backups:
- List all available snapshots to find the target name:
(e.g., output shows 20260626_114522)
- Target the specific snapshot name using the
--snapshotparameter:
LDM will bundle only that specific snapshot, ignoring all other snapshot directories.
Integrity Verification (Checksum Files)¶
Every time you package a workspace, LDM automatically writes a companion .sha256 checksum file (e.g. my-project.ldmp.sha256).
To verify the integrity of your .ldmp package manually on the target machine:
When importing, LDM automatically looks for this checksum file to perform the check for you.
3. Scaffolding CI/CD Pipelines (ldm system init-ci)¶
To automate the generation of .ldmp packages on code changes or releases, you can scaffold a GitHub Actions workflow inside your Git repository.
Trigger Presets¶
release(Default): Triggers only when a GitHub Release is published. Useful for production-grade builds.tag: Triggers whenever a tag starting withv*is pushed.push: Triggers on every commit pushed to themasterbranch.manual: Only runs when triggered manually via the GitHub Actions tab (workflow_dispatch).
Workflow Output¶
This scaffolds a .github/workflows/ldm-package-release.yml file which:
- Provisions a clean Ubuntu environment.
- Installs LDM from PyPI.
- Boots up your local Liferay environment.
- Waits for all OSGi modules and Client Extensions to deploy cleanly.
- Bundles the database and volume into a
.ldmppackage. - Automatically attaches the package and its checksum as GitHub Release assets.
Last Updated: 2026-10-21 | Last Reviewed: 2026-10-21