Moodle Playground Blueprints¶
Experimental
Blueprint support is experimental and currently implements a documented subset of steps. Unsupported steps fail clearly and unsafe steps are disabled by default. The blueprint runner never replaces the existing environment-variable configuration — it is an additional, declarative provisioning layer for repeatable development, QA, CI and demo scenarios.
alpine-moodle can apply Moodle Playground-compatible
blueprint.json files after Moodle has been installed or upgraded. A single
declarative blueprint can therefore describe one Moodle scenario and run in two
complementary, sibling runtimes:
- Moodle Playground — the browser/WASM runtime for ephemeral QA, demos, shareable reproductions and fast validation. No server required; perfect for sharing a link with a reviewer.
- alpine-moodle — this Docker runtime for local development, CI, plugin development, integration testing, persistence and real server-side behaviour (cron, mail, database, file system).
The two projects are siblings, not competitors: author a blueprint once, run it in the browser for a quick look, and run the same file in Docker when you need a real, persistent Moodle.
How it works¶
A new startup hook, rootfs/docker-entrypoint-init.d/03-apply-blueprint.sh,
runs after 02-configure-moodle.sh (i.e. after Moodle is installed or
upgraded). When any blueprint variable is set it calls the runner:
The runner follows the same four phases as WordPress Playground: declaration → resource resolution → validation → step execution, executing steps sequentially and failing fast on the first error.
Environment variables¶
| Variable | Description | Default |
|---|---|---|
MOODLE_BLUEPRINT |
Path to a blueprint JSON file inside the container | empty |
MOODLE_BLUEPRINT_URL |
Remote URL to a blueprint JSON file | empty |
MOODLE_BLUEPRINT_BUNDLE |
Path to a local bundle directory or ZIP | empty |
MOODLE_BLUEPRINT_FORCE |
Reapply even if already applied | false |
MOODLE_BLUEPRINT_ON_ERROR |
abort or warn |
abort |
MOODLE_BLUEPRINT_ALLOW_REMOTE_RESOURCES |
Allow URL resources | true |
MOODLE_BLUEPRINT_ALLOW_UNSAFE_STEPS |
Allow unsafe steps if implemented | false |
MOODLE_BLUEPRINT_MAX_RESOURCE_SIZE |
Max remote resource size | 50M |
MOODLE_BLUEPRINT_ON_ERROR=abortfails container startup if blueprint application fails;warnprints a warning and continues startup.- Secrets (passwords, tokens) are never written to the logs.
Blueprint sources¶
Set one of the following. If several are set, this precedence applies:
MOODLE_BLUEPRINT_BUNDLEMOODLE_BLUEPRINTMOODLE_BLUEPRINT_URL
# 1. Local blueprint file
MOODLE_BLUEPRINT=/blueprints/demo.blueprint.json
# 2. Remote blueprint file (http/https only)
MOODLE_BLUEPRINT_URL=https://example.com/demo.blueprint.json
# 3. Local bundle directory
MOODLE_BLUEPRINT_BUNDLE=/blueprints/demo-bundle/
# 4. Local bundle ZIP
MOODLE_BLUEPRINT_BUNDLE=/blueprints/demo-bundle.zip
The selected source is printed in the logs.
Blueprint format¶
{
"$schema": "https://raw.githubusercontent.com/ateeducacion/moodle-playground/main/assets/blueprints/blueprint-schema.json",
"preferredVersions": {
"php": "8.3",
"moodle": "5.0"
},
"landingPage": "/course/view.php?id=2",
"constants": {
"ADMIN_USER": "admin"
},
"resources": {
"pluginZip": {
"url": "https://example.com/plugin.zip"
}
},
"steps": [
{
"step": "setConfig",
"name": "debug",
"value": 32767
}
]
}
The parser:
- loads JSON and fails clearly on syntax errors;
- requires
stepsto be an array, each entry carrying a non-emptystepname; - substitutes
{{KEY}}placeholders fromconstantsthroughout the blueprint; - resolves resource references like
@pluginZip; - preserves unknown top-level fields (e.g.
runtime) without failing; - fails clearly on unknown or unsupported step types, reporting the failing step index and name.
preferredVersions and landingPage are advisory
The Docker image selects its Moodle/PHP version at build time, so
preferredVersions is treated as a hint. landingPage is logged as a hint;
the Docker runtime does not auto-navigate a browser.
Resources¶
Declare named resources at the top level and reference them from steps with
@name, or inline a descriptor directly in a step. Supported descriptors:
{ "url": "https://example.com/file.zip" }
{ "resource": "url", "url": "https://example.com/file.zip" }
{ "base64": "..." }
{ "data-url": "data:text/plain;base64,..." }
{ "literal": "text or JSON-compatible value" }
{ "bundled": "plugins/my-plugin.zip" }
{ "resource": "bundled", "path": "/plugins/my-plugin.zip" }
bundled paths are resolved relative to the extracted bundle directory. The
browser-only vfs resource type is rejected with a clear message. Resolved
resources are cached for the duration of a single run.
Supported steps¶
| Step | Status | Notes |
|---|---|---|
setConfig |
supported | Uses Moodle CLI (admin/cli/cfg.php) |
setConfigs |
supported | Loops over setConfig (values/configs) |
setAdminAccount |
supported | Password not logged |
installMoodlePlugin |
supported | ZIP resources, safe extraction |
installTheme |
supported | ZIP resources, enforces theme_* |
setTheme |
supported | Sets Moodle theme config |
createCategory |
supported | Idempotent (by idnumber or name+parent) |
createCourse |
supported | Idempotent by shortname |
createUser |
supported | Idempotent by username |
createUsers |
supported | Loops over createUser |
enrolUser |
supported | Manual enrolment, idempotent |
installMoodle |
no-op | Moodle is installed by the container startup |
login |
no-op | Browser-only; not applicable server-side |
restoreCourse |
planned | Recognised; fails clearly until implemented |
runPhpCode |
disabled | Unsafe |
runPhpScript |
disabled | Unsafe |
writeFile |
disabled | Unsafe by default |
unzip |
disabled | Unsafe by default |
Other recognised Moodle Playground steps (e.g. addModule, createRole,
installLanguagePack, bulk variants) are reported as planned and fail
clearly rather than being silently ignored. Truly unknown steps fail with an
"unknown step type" error.
Step examples¶
Idempotency¶
After a blueprint is applied successfully, a marker is written under:
The hash is computed over the normalised blueprint content (key order and whitespace independent). On the next start:
- if the marker exists and
MOODLE_BLUEPRINT_FORCEis nottrue, the runner printsBlueprint already applied: <hash>and exits successfully without reapplying; - if
MOODLE_BLUEPRINT_FORCE=true, it reapplies.
Hash limitation
The hash covers the declarative blueprint JSON only. It does not include
the bytes of bundled or remote resources, so changing a referenced resource
without editing the blueprint will not change the hash. Use
MOODLE_BLUEPRINT_FORCE=true to force reapplication in that case.
Individual steps are also idempotent where it matters (categories by
idnumber/name, courses by shortname, users by username, enrolments are not
duplicated), so reapplying a blueprint converges rather than duplicating data.
Security¶
The runner enforces safe defaults in a central SecurityPolicy:
- no
evaland no arbitrary code execution; - shelling out uses
escapeshellarg()— no command interpolation of user input; - passwords are never logged;
- remote resources respect
MOODLE_BLUEPRINT_ALLOW_REMOTE_RESOURCESand are bounded byMOODLE_BLUEPRINT_MAX_RESOURCE_SIZE(http/https only); - ZIP extraction is performed entry-by-entry with ZIP-slip protection; symlink entries are never honoured;
- bundle resource paths cannot escape the bundle directory (no
.., no absolute paths,__MACOSXignored); - plugin installs are restricted to allowlisted Moodle directories;
- unsafe steps are disabled by default and are not implemented in this version.
Bundles¶
A bundle is a self-contained directory or ZIP containing blueprint.json plus
the resources it references (inspired by WordPress Playground Blueprint
Bundles). blueprint.json may sit at the bundle root or exactly one directory
deep; the __MACOSX folder is ignored and multiple candidates are an error.
{
"resources": {
"examplePlugin": {
"bundled": "plugins/mod_example.zip"
}
},
"steps": [
{
"step": "installMoodlePlugin",
"source": "@examplePlugin"
}
]
}
Remote bundle URLs are future work
MOODLE_BLUEPRINT_BUNDLE accepts a local directory or ZIP only.
Downloading a remote bundle ZIP is not implemented yet; use
MOODLE_BLUEPRINT_URL for a remote single-file blueprint, or fetch the
bundle into the container yourself.
docker-compose example¶
services:
moodle:
image: erseco/alpine-moodle:latest
ports:
- "8080:8080"
environment:
MOODLE_DATABASE_TYPE: sqlite3
MOODLE_USERNAME: admin
MOODLE_PASSWORD: ChangeMe123!
MOODLE_EMAIL: admin@example.com
MOODLE_SITENAME: "Blueprint Demo"
MOODLE_BLUEPRINT: /blueprints/demo.blueprint.json
volumes:
- moodledata:/var/www/moodledata
- ./demo.blueprint.json:/blueprints/demo.blueprint.json:ro
volumes:
moodledata:
A copy-pasteable demo.blueprint.json lives in
docs/examples/.
Manual validation¶
The blueprint runs after Moodle installation/upgrade. To try it end-to-end:
docker build -t alpine-moodle-blueprint-test .
docker run --rm \
-p 8080:8080 \
-e MOODLE_DATABASE_TYPE=sqlite3 \
-e MOODLE_USERNAME=admin \
-e MOODLE_PASSWORD=ChangeMe123! \
-e MOODLE_EMAIL=admin@example.com \
-e MOODLE_SITENAME="Blueprint Demo" \
-e MOODLE_BLUEPRINT=/blueprints/demo.blueprint.json \
-v "$PWD/docs/examples/demo.blueprint.json:/blueprints/demo.blueprint.json:ro" \
alpine-moodle-blueprint-test
You can also validate a blueprint without applying it (no Moodle bootstrap):
docker run --rm \
-e MOODLE_BLUEPRINT=/blueprints/demo.blueprint.json \
-v "$PWD/docs/examples/demo.blueprint.json:/blueprints/demo.blueprint.json:ro" \
alpine-moodle-blueprint-test moodle-blueprint validate
Tests¶
The runner ships with dependency-free PHP unit tests plus lint/shell checks:
This lints every PHP file, syntax-checks the entrypoint hook, and runs unit tests for the parser, security policy, resource resolver, archive safety, bundle detection, step registry and idempotency markers.