Skip to content

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 deploy to compile Java bundles and package client extensions into zip archives, then hot-deploys them into Liferay's /deploy directory.
  • 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.

ldm import <source-path-or-url>

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 tag and db_type come 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 the meta file inside the archive or rebuild the package with ldm package on 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 + = + value format, 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 misleading Security 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. A meta file 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)

Added in v2.11.31

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:

  1. Ensure your container stack is active: ldm status
  2. Run the package command:
ldm package --output ./dist

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:

  1. Run the package command with the --use-latest flag:
ldm package --use-latest --output ./dist

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:

  1. List all available snapshots to find the target name:
ldm snapshots

(e.g., output shows 20260626_114522)

  1. Target the specific snapshot name using the --snapshot parameter:
ldm package --snapshot 20260626_114522 --output ./dist

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:

# Verify using system checksum utilities
shasum -a 256 -c ./dist/my-project.ldmp.sha256

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.

ldm system init-ci [project] [--trigger release|tag|push|manual] [--repo owner/repo]

Trigger Presets

  • release (Default): Triggers only when a GitHub Release is published. Useful for production-grade builds.
  • tag: Triggers whenever a tag starting with v* is pushed.
  • push: Triggers on every commit pushed to the master branch.
  • 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:

  1. Provisions a clean Ubuntu environment.
  2. Installs LDM from PyPI.
  3. Boots up your local Liferay environment.
  4. Waits for all OSGi modules and Client Extensions to deploy cleanly.
  5. Bundles the database and volume into a .ldmp package.
  6. Automatically attaches the package and its checksum as GitHub Release assets.

Last Updated: 2026-10-21 | Last Reviewed: 2026-10-21