mirror of
https://github.com/Dokploy/templates.git
synced 2026-07-15 14:52:05 -05:00
feat: add Frappe LMS template
Adds a Frappe LMS (Frappe Learning) template based on the erpnext-v16 blueprint pattern (configurator + create-site + workers + scheduler + websocket + mariadb + 3x redis). Pins ghcr.io/frappe/lms:v2.52.0, the latest upstream image that ships the lms app (v2.52.1+ images are currently published without any apps baked in). The image does not bundle the payments app, so create-site clears lms's required_apps before install; paid-course checkout is unavailable, everything else works. The backend is gated on a create-site completion marker so gunicorn workers never serve (and cache) a half-installed site during the 3-6 minute first boot. Closes #517 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
313
blueprints/frappe-lms/docker-compose.yml
Normal file
313
blueprints/frappe-lms/docker-compose.yml
Normal file
@@ -0,0 +1,313 @@
|
||||
x-custom-image: &custom_image
|
||||
image: ${IMAGE_NAME:-ghcr.io/frappe/lms}:${VERSION:-v2.52.0}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: always
|
||||
|
||||
services:
|
||||
backend:
|
||||
<<: *custom_image
|
||||
entrypoint: ["bash", "-c"]
|
||||
command:
|
||||
- >
|
||||
until [[ -f "sites/${SITE_NAME}/.create-site-done" ]]; do
|
||||
echo "Waiting for site ${SITE_NAME} to be created (first boot takes a few minutes)";
|
||||
sleep 5;
|
||||
done;
|
||||
exec /home/frappe/frappe-bench/env/bin/gunicorn --chdir=/home/frappe/frappe-bench/sites --bind=0.0.0.0:8000 --threads=4 --workers=2 --worker-class=gthread --worker-tmp-dir=/dev/shm --timeout=120 --preload frappe.app:application
|
||||
volumes:
|
||||
- sites:/home/frappe/frappe-bench/sites
|
||||
healthcheck:
|
||||
test: ["CMD", "wait-for-it", "127.0.0.1:8000"]
|
||||
interval: 2s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
start_period: 600s
|
||||
|
||||
frontend:
|
||||
<<: *custom_image
|
||||
command:
|
||||
- nginx-entrypoint.sh
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_started
|
||||
websocket:
|
||||
condition: service_started
|
||||
environment:
|
||||
BACKEND: backend:8000
|
||||
FRAPPE_SITE_NAME_HEADER: ${FRAPPE_SITE_NAME_HEADER:-$$host}
|
||||
SOCKETIO: websocket:9000
|
||||
UPSTREAM_REAL_IP_ADDRESS: 127.0.0.1
|
||||
UPSTREAM_REAL_IP_HEADER: X-Forwarded-For
|
||||
UPSTREAM_REAL_IP_RECURSIVE: "off"
|
||||
volumes:
|
||||
- sites:/home/frappe/frappe-bench/sites
|
||||
healthcheck:
|
||||
test: ["CMD", "wait-for-it", "127.0.0.1:8080"]
|
||||
interval: 2s
|
||||
timeout: 30s
|
||||
retries: 30
|
||||
|
||||
queue-default:
|
||||
<<: *custom_image
|
||||
command:
|
||||
- bench
|
||||
- worker
|
||||
- --queue
|
||||
- default
|
||||
volumes:
|
||||
- sites:/home/frappe/frappe-bench/sites
|
||||
healthcheck:
|
||||
test: ["CMD", "wait-for-it", "redis-queue:6379"]
|
||||
interval: 2s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
depends_on:
|
||||
configurator:
|
||||
condition: service_completed_successfully
|
||||
|
||||
queue-long:
|
||||
<<: *custom_image
|
||||
command:
|
||||
- bench
|
||||
- worker
|
||||
- --queue
|
||||
- long
|
||||
volumes:
|
||||
- sites:/home/frappe/frappe-bench/sites
|
||||
healthcheck:
|
||||
test: ["CMD", "wait-for-it", "redis-queue:6379"]
|
||||
interval: 2s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
depends_on:
|
||||
configurator:
|
||||
condition: service_completed_successfully
|
||||
|
||||
queue-short:
|
||||
<<: *custom_image
|
||||
command:
|
||||
- bench
|
||||
- worker
|
||||
- --queue
|
||||
- short
|
||||
volumes:
|
||||
- sites:/home/frappe/frappe-bench/sites
|
||||
healthcheck:
|
||||
test: ["CMD", "wait-for-it", "redis-queue:6379"]
|
||||
interval: 2s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
depends_on:
|
||||
configurator:
|
||||
condition: service_completed_successfully
|
||||
|
||||
scheduler:
|
||||
<<: *custom_image
|
||||
healthcheck:
|
||||
test: ["CMD", "wait-for-it", "redis-queue:6379"]
|
||||
interval: 2s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
command:
|
||||
- bench
|
||||
- schedule
|
||||
depends_on:
|
||||
configurator:
|
||||
condition: service_completed_successfully
|
||||
volumes:
|
||||
- sites:/home/frappe/frappe-bench/sites
|
||||
|
||||
websocket:
|
||||
<<: *custom_image
|
||||
healthcheck:
|
||||
test: ["CMD", "wait-for-it", "websocket:9000"]
|
||||
interval: 2s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
command:
|
||||
- node
|
||||
- /home/frappe/frappe-bench/apps/frappe/socketio.js
|
||||
depends_on:
|
||||
configurator:
|
||||
condition: service_completed_successfully
|
||||
volumes:
|
||||
- sites:/home/frappe/frappe-bench/sites
|
||||
|
||||
configurator:
|
||||
<<: *custom_image
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: ${CONFIGURE:-0}
|
||||
restart_policy:
|
||||
condition: none
|
||||
entrypoint: ["bash", "-c"]
|
||||
command:
|
||||
- >
|
||||
echo "[configurator] starting";
|
||||
if [[ $${REGENERATE_APPS_TXT} == "1" ]]; then
|
||||
echo "[configurator] regenerating sites/apps.txt";
|
||||
ls -1 apps > sites/apps.txt;
|
||||
fi;
|
||||
existing_db_host=$$(grep -hs '^' sites/common_site_config.json | jq -r ".db_host // empty" 2>/dev/null || true);
|
||||
if [[ -n "$${existing_db_host}" ]]; then
|
||||
echo "[configurator] common_site_config.json already configured, skipping";
|
||||
exit 0;
|
||||
fi;
|
||||
echo "[configurator] applying bench global config";
|
||||
bench set-config -g db_host $$DB_HOST;
|
||||
bench set-config -gp db_port $$DB_PORT;
|
||||
bench set-config -g redis_cache "redis://$$REDIS_CACHE";
|
||||
bench set-config -g redis_queue "redis://$$REDIS_QUEUE";
|
||||
bench set-config -g redis_socketio "redis://$$REDIS_SOCKETIO";
|
||||
bench set-config -gp socketio_port $$SOCKETIO_PORT;
|
||||
echo "[configurator] done";
|
||||
environment:
|
||||
DB_HOST: "${DB_HOST:-db}"
|
||||
DB_PORT: "3306"
|
||||
REDIS_CACHE: redis-cache:6379
|
||||
REDIS_QUEUE: redis-queue:6379
|
||||
REDIS_SOCKETIO: redis-socketio:6379
|
||||
SOCKETIO_PORT: "9000"
|
||||
REGENERATE_APPS_TXT: "${REGENERATE_APPS_TXT:-0}"
|
||||
volumes:
|
||||
- sites:/home/frappe/frappe-bench/sites
|
||||
|
||||
create-site:
|
||||
<<: *custom_image
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: ${CREATE_SITE:-0}
|
||||
restart_policy:
|
||||
condition: none
|
||||
entrypoint: ["bash", "-c"]
|
||||
command:
|
||||
- >
|
||||
wait-for-it -t 300 $${DB_HOST}:$${DB_PORT};
|
||||
wait-for-it -t 120 redis-cache:6379;
|
||||
wait-for-it -t 120 redis-queue:6379;
|
||||
export start=`date +%s`;
|
||||
until [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".db_host // empty"` ]] && \
|
||||
[[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_cache // empty"` ]] && \
|
||||
[[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_queue // empty"` ]];
|
||||
do
|
||||
echo "Waiting for sites/common_site_config.json to be created";
|
||||
sleep 5;
|
||||
if (( `date +%s`-start > 120 )); then
|
||||
echo "could not find sites/common_site_config.json with required keys";
|
||||
exit 1
|
||||
fi
|
||||
done;
|
||||
echo "sites/common_site_config.json found";
|
||||
[[ -d "sites/${SITE_NAME}" ]] && echo "${SITE_NAME} already exists" && touch sites/${SITE_NAME}/.create-site-done && exit 0;
|
||||
sed -i "s/^required_apps.*/required_apps = []/" apps/lms/lms/hooks.py;
|
||||
bench new-site --mariadb-user-host-login-scope='%' --admin-password=$${ADMIN_PASSWORD} --db-root-username=root --db-root-password=$${DB_ROOT_PASSWORD} $${INSTALL_APP_ARGS} $${SITE_NAME} || exit 1;
|
||||
if [[ $${ENABLE_SCHEDULER} == "1" ]]; then
|
||||
bench --site $${SITE_NAME} enable-scheduler;
|
||||
else
|
||||
echo "Skipping scheduler enable";
|
||||
fi;
|
||||
bench --site $${SITE_NAME} clear-cache;
|
||||
touch sites/$${SITE_NAME}/.create-site-done;
|
||||
volumes:
|
||||
- sites:/home/frappe/frappe-bench/sites
|
||||
environment:
|
||||
SITE_NAME: ${SITE_NAME}
|
||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
|
||||
DB_HOST: ${DB_HOST:-db}
|
||||
DB_PORT: "${DB_PORT:-3306}"
|
||||
DB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
|
||||
INSTALL_APP_ARGS: ${INSTALL_APP_ARGS}
|
||||
ENABLE_SCHEDULER: "${ENABLE_SCHEDULER}"
|
||||
|
||||
migration:
|
||||
<<: *custom_image
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: ${MIGRATE:-0}
|
||||
restart_policy:
|
||||
condition: none
|
||||
entrypoint: ["bash", "-c"]
|
||||
command:
|
||||
- >
|
||||
curl -f -H "Host: ${SITE_NAME}" http://frontend:8080/api/method/ping || { echo "Site not reachable yet, skipping migration"; exit 0; };
|
||||
bench --site all set-config -p maintenance_mode 1;
|
||||
bench --site all set-config -p pause_scheduler 1;
|
||||
bench --site all migrate;
|
||||
bench --site all set-config -p maintenance_mode 0;
|
||||
bench --site all set-config -p pause_scheduler 0;
|
||||
volumes:
|
||||
- sites:/home/frappe/frappe-bench/sites
|
||||
|
||||
db:
|
||||
image: mariadb:${DB_VERSION:-11.8.6}
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: ${ENABLE_DB:-0}
|
||||
restart_policy:
|
||||
condition: always
|
||||
healthcheck:
|
||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
start_period: 120s
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
command:
|
||||
- --character-set-server=utf8mb4
|
||||
- --collation-server=utf8mb4_unicode_ci
|
||||
- --skip-character-set-client-handshake
|
||||
environment:
|
||||
- MARIADB_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
|
||||
volumes:
|
||||
- db-data:/var/lib/mysql
|
||||
|
||||
redis-cache:
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: always
|
||||
image: redis:6.2-alpine
|
||||
volumes:
|
||||
- redis-cache-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
redis-queue:
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: always
|
||||
image: redis:6.2-alpine
|
||||
volumes:
|
||||
- redis-queue-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
redis-socketio:
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: always
|
||||
image: redis:6.2-alpine
|
||||
volumes:
|
||||
- redis-socketio-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
redis-cache-data:
|
||||
redis-queue-data:
|
||||
redis-socketio-data:
|
||||
sites:
|
||||
driver_opts:
|
||||
type: "${SITE_VOLUME_TYPE}"
|
||||
o: "${SITE_VOLUME_OPTS}"
|
||||
device: "${SITE_VOLUME_DEV}"
|
||||
17
blueprints/frappe-lms/instructions.md
Normal file
17
blueprints/frappe-lms/instructions.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Frappe LMS (Frappe Learning)
|
||||
|
||||
## Initial setup
|
||||
|
||||
The first deployment creates the Frappe site and installs the LMS app, which can take **3-6 minutes** after all containers are up. The site is ready when the domain returns the Frappe login page.
|
||||
|
||||
## Default credentials
|
||||
|
||||
- **Username:** `Administrator`
|
||||
- **Password:** the value of the `ADMIN_PASSWORD` environment variable (auto-generated, check the Environment tab of the service)
|
||||
|
||||
After logging in, the learning portal is available at `/lms` and the Frappe admin backend at `/app`.
|
||||
|
||||
## Notes
|
||||
|
||||
- The template pins `ghcr.io/frappe/lms:v2.52.0`, the latest upstream image that ships the LMS app (newer upstream tags are currently published without the app baked in).
|
||||
- The upstream image does not include the `payments` app, so paid-course checkout features are unavailable. Free courses, batches, quizzes and certifications work normally.
|
||||
BIN
blueprints/frappe-lms/lms-logo.png
Normal file
BIN
blueprints/frappe-lms/lms-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
19
blueprints/frappe-lms/meta.json
Normal file
19
blueprints/frappe-lms/meta.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"id": "frappe-lms",
|
||||
"name": "Frappe LMS",
|
||||
"version": "v2.52.0",
|
||||
"description": "Easy to use, open source learning management system to structure courses and reach out to learners, built on the Frappe Framework.",
|
||||
"logo": "lms-logo.png",
|
||||
"links": {
|
||||
"github": "https://github.com/frappe/lms",
|
||||
"docs": "https://docs.frappe.io/learning",
|
||||
"website": "https://frappe.io/learning"
|
||||
},
|
||||
"tags": [
|
||||
"lms",
|
||||
"learning",
|
||||
"education",
|
||||
"courses",
|
||||
"frappe"
|
||||
]
|
||||
}
|
||||
30
blueprints/frappe-lms/template.toml
Normal file
30
blueprints/frappe-lms/template.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
db_root_password = "${password:32}"
|
||||
admin_password = "${password:32}"
|
||||
|
||||
[config]
|
||||
[[config.domains]]
|
||||
serviceName = "frontend"
|
||||
port = 8_080
|
||||
host = "${main_domain}"
|
||||
|
||||
[config.env]
|
||||
SITE_NAME = "${main_domain}"
|
||||
ADMIN_PASSWORD = "${admin_password}"
|
||||
DB_ROOT_PASSWORD = "${db_root_password}"
|
||||
MIGRATE = "1"
|
||||
ENABLE_DB = "1"
|
||||
DB_HOST = "db"
|
||||
CREATE_SITE = "1"
|
||||
CONFIGURE = "1"
|
||||
REGENERATE_APPS_TXT = "1"
|
||||
INSTALL_APP_ARGS = "--install-app lms"
|
||||
ENABLE_SCHEDULER = "1"
|
||||
IMAGE_NAME = "ghcr.io/frappe/lms"
|
||||
VERSION = "v2.52.0"
|
||||
FRAPPE_SITE_NAME_HEADER = ""
|
||||
DB_VERSION = "11.8.6"
|
||||
SITE_VOLUME_TYPE = ""
|
||||
SITE_VOLUME_OPTS = ""
|
||||
SITE_VOLUME_DEV = ""
|
||||
Reference in New Issue
Block a user