mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-04 20:02:07 +00:00
Merge pull request #580 from jcastro/feature/oci-app-catalog-additions
Add KitchenOwl, Krusader, Spoolman, Unbound and Z-Wave JS UI
This commit is contained in:
+15
-1
@@ -8981,5 +8981,19 @@
|
||||
"⚠ Disk data will NOT be erased.": "⚠ Dáta na disku sa NEVYMAŽÚ.",
|
||||
"⚠ Disk will be unmounted and removed from /etc/fstab.": "⚠ Disk sa odpojí a odstráni z /etc/fstab.",
|
||||
"⚠ The /etc/fstab entry will be removed.": "⚠ Záznam v /etc/fstab bude odstránený.",
|
||||
"⚠ The disk will be unmounted.": "⚠ Disk bude odpojený."
|
||||
"⚠ The disk will be unmounted.": "⚠ Disk bude odpojený.",
|
||||
"A validating, recursive DNS resolver for your private network.": "Validujúci rekurzívny DNS resolver pre vašu súkromnú sieť.",
|
||||
"Controller path on the Proxmox host; prefer /dev/serial/by-id/...": "Cesta ku kontroléru na hostiteľovi Proxmox; uprednostnite /dev/serial/by-id/...",
|
||||
"Initial Z-Wave JS UI admin password": "Počiatočné heslo správcu Z-Wave JS UI",
|
||||
"KDE's twin-panel file manager in a browser-based desktop.": "Dvojpanelový správca súborov KDE v pracovnej ploche dostupnej cez webový prehliadač.",
|
||||
"KitchenOwl": "KitchenOwl",
|
||||
"Krusader Web UI": "Webové rozhranie Krusaderu",
|
||||
"Manage a Z-Wave network from a web dashboard and WebSocket gateway.": "Spravujte sieť Z-Wave cez webový panel a bránu WebSocket.",
|
||||
"Open the web address and follow KitchenOwl’s onboarding to create the first account; no default account is configured.": "Otvorte webovú adresu a podľa pokynov KitchenOwl vytvorte prvý účet; predvolený účet nie je nastavený.",
|
||||
"Pass through a Z-Wave USB or serial controller": "Pripojiť USB alebo sériový radič Z-Wave",
|
||||
"Self-hosted grocery list and recipe manager": "Správca nákupných zoznamov a receptov, ktorý si môžete prevádzkovať sami.",
|
||||
"Spoolman web interface": "Webové rozhranie Spoolmanu",
|
||||
"Track 3D printer filament spools and usage.": "Sledujte cievky filamentu pre 3D tlačiareň a ich spotrebu.",
|
||||
"Z-Wave JS UI": "Z-Wave JS UI",
|
||||
"Z-Wave JS UI admin login": "Prihlásenie správcu Z-Wave JS UI"
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
<!-- Generated from oci/catalog/verification.json by .github/scripts/oci_validation.py. Do not edit by hand. -->
|
||||
|
||||
52 of 536 applications in the OCI catalog have been tested for real: 49 in the ProxMenux lab and 3 by the community. The OCI installer shows them as verified, with a ✓ in the lists.
|
||||
52 of 541 applications in the OCI catalog have been tested for real: 49 in the ProxMenux lab and 3 by the community. The OCI installer shows them as verified, with a ✓ in the lists.
|
||||
|
||||
Each test describes the scenario that was run and names the image it ran on. It does not cover every possible configuration of the application, and a newer image has not been tested until someone reports it.
|
||||
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
{
|
||||
"schema_version": "0.5.0",
|
||||
"kind": "proxmenux.oci-template",
|
||||
"id": "image-kitchenowl",
|
||||
"status": "generated-unvalidated",
|
||||
"catalog_ui": {
|
||||
"title": {
|
||||
"en_US": "KitchenOwl"
|
||||
},
|
||||
"tagline": {
|
||||
"en_US": "Self-hosted grocery list and recipe manager"
|
||||
},
|
||||
"description": {
|
||||
"en_US": "Manage shared shopping lists, recipes and meal plans in one self-hosted app."
|
||||
},
|
||||
"category": "productivity",
|
||||
"category_label": "Productivity & Workflows",
|
||||
"author": "Tom Bursch",
|
||||
"developer": "Tom Bursch",
|
||||
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/kitchenowl.webp",
|
||||
"thumbnail": null,
|
||||
"screenshots": [],
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"launch": {
|
||||
"scheme": "http",
|
||||
"port": 80,
|
||||
"path": "/"
|
||||
},
|
||||
"website": "https://kitchenowl.org/",
|
||||
"documentation": "https://docs.kitchenowl.org/self-hosting/",
|
||||
"repository": "https://github.com/TomBursch/kitchenowl",
|
||||
"tips": [
|
||||
"This recipe follows KitchenOwl’s standard official Compose deployment: a web frontend and backend in separate OCI LXCs.",
|
||||
"The upstream sample uses PLEASE_CHANGE_ME for JWT_SECRET_KEY; this recipe generates a unique signing key at install time."
|
||||
],
|
||||
"mini_changelog": [],
|
||||
"display_version": null,
|
||||
"updated_at": "2026-11-01"
|
||||
},
|
||||
"source": {
|
||||
"provider": "tombursch",
|
||||
"repository": "https://github.com/TomBursch/kitchenowl",
|
||||
"revision": "60a012ea5cae829878f78a46a585a7ee530b069a",
|
||||
"image_repository_url": "https://hub.docker.com/r/tombursch/kitchenowl-web",
|
||||
"readme_pushed_at": "2026-11-01T23:22:22Z",
|
||||
"compose_sha256": "f620124c2a4943d4f2465ee304677bc9ed172a088f0fe43f5db7cbc8f6424263",
|
||||
"generated_at": "2026-11-02T20:13:05+00:00",
|
||||
"default_branch": "main",
|
||||
"readme_raw_url": "https://raw.githubusercontent.com/TomBursch/kitchenowl/main/docker-compose.yml"
|
||||
},
|
||||
"container_contract": {
|
||||
"service_name": "front",
|
||||
"container_name": "front",
|
||||
"image": {
|
||||
"reference": "tombursch/kitchenowl-web:latest",
|
||||
"registry": "docker.io",
|
||||
"repository": "tombursch/kitchenowl-web",
|
||||
"tag": "latest",
|
||||
"digest": null,
|
||||
"pull_policy": "resolve-written-reference-to-architecture-digest-at-install"
|
||||
},
|
||||
"environment": [],
|
||||
"volumes": [],
|
||||
"ports": [
|
||||
{
|
||||
"container_port": 80,
|
||||
"published_example": 80,
|
||||
"protocol": "tcp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
}
|
||||
],
|
||||
"related_services": [
|
||||
{
|
||||
"name": "back",
|
||||
"image": "tombursch/kitchenowl-backend:latest"
|
||||
}
|
||||
],
|
||||
"restart": "unless-stopped",
|
||||
"stop_grace_period": null,
|
||||
"original_compose": "services:\n back:\n environment:\n JWT_SECRET_KEY: ${GENERATED_JWT_SECRET_KEY}\n image: tombursch/kitchenowl-backend:latest\n restart: unless-stopped\n volumes:\n - read_only: false\n source: kitchenowl_data\n target: /data\n front:\n depends_on:\n - back\n image: tombursch/kitchenowl-web:latest\n ports:\n - 80:80\n restart: unless-stopped\n volumes: []\nvolumes:\n kitchenowl_data: null\n"
|
||||
},
|
||||
"compose_stack": {
|
||||
"project_name": "kitchenowl",
|
||||
"deployment_model": "one-native-oci-lxc-per-compose-service",
|
||||
"user_experience": "single-application-install",
|
||||
"main_service": "front",
|
||||
"service_count": 2,
|
||||
"services": [
|
||||
{
|
||||
"name": "back",
|
||||
"image": "tombursch/kitchenowl-backend:latest",
|
||||
"is_main": false,
|
||||
"role": "dependency",
|
||||
"vmid_offset": 1,
|
||||
"depends_on": [],
|
||||
"frontend_network": false,
|
||||
"private_network": true,
|
||||
"compose": {
|
||||
"environment": {
|
||||
"JWT_SECRET_KEY": "${GENERATED_JWT_SECRET_KEY}"
|
||||
},
|
||||
"image": "tombursch/kitchenowl-backend:latest",
|
||||
"restart": "unless-stopped",
|
||||
"volumes": [
|
||||
{
|
||||
"source": "kitchenowl_data",
|
||||
"target": "/data",
|
||||
"read_only": false
|
||||
}
|
||||
],
|
||||
"healthcheck": {
|
||||
"test": [
|
||||
"CMD-SHELL",
|
||||
"uwsgi_curl localhost:5000 /api/health/8M4F88S8ooi4sMbLBfkkV7ctWwgibW6V || exit 1"
|
||||
],
|
||||
"interval": "60s",
|
||||
"timeout": "3s",
|
||||
"retries": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "front",
|
||||
"image": "tombursch/kitchenowl-web:latest",
|
||||
"is_main": true,
|
||||
"role": "frontend",
|
||||
"vmid_offset": 0,
|
||||
"depends_on": [
|
||||
"back"
|
||||
],
|
||||
"frontend_network": true,
|
||||
"private_network": true,
|
||||
"compose": {
|
||||
"depends_on": [
|
||||
"back"
|
||||
],
|
||||
"image": "tombursch/kitchenowl-web:latest",
|
||||
"ports": [
|
||||
"80:80"
|
||||
],
|
||||
"restart": "unless-stopped",
|
||||
"volumes": [],
|
||||
"healthcheck": {
|
||||
"test": [
|
||||
"CMD-SHELL",
|
||||
"curl -f http://localhost/ || exit 1"
|
||||
],
|
||||
"interval": "30s",
|
||||
"timeout": "3s",
|
||||
"retries": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"top_level": {
|
||||
"volumes": {
|
||||
"kitchenowl_data": null
|
||||
}
|
||||
},
|
||||
"networking": {
|
||||
"frontend": "selected-proxmox-bridge",
|
||||
"private_required": true,
|
||||
"private_creation": "automatic-create-if-missing",
|
||||
"private_address_allocation": "automatic-static-address-per-service",
|
||||
"service_discovery": "private-addresses-with-compose-service-host-aliases",
|
||||
"dependency_external_access": "disabled-unless-service-publishes-ports",
|
||||
"prompt_user_for_private_network": false
|
||||
},
|
||||
"storage": [
|
||||
{
|
||||
"id": "back-volume-0",
|
||||
"service": "back",
|
||||
"container_path": "/data",
|
||||
"mode": "managed-volume",
|
||||
"user_selectable": true,
|
||||
"backup": true,
|
||||
"shared_with_other_lxc": false,
|
||||
"source_path": null,
|
||||
"source_path_prompt": null
|
||||
}
|
||||
],
|
||||
"orchestration": {
|
||||
"reserve_vmids_atomically": 2,
|
||||
"start_order": [
|
||||
"back",
|
||||
"front"
|
||||
],
|
||||
"stop_order": [
|
||||
"front",
|
||||
"back"
|
||||
],
|
||||
"dependency_readiness": "compose-healthcheck-then-port-or-process-fallback",
|
||||
"rollback_on_failure": "remove-new-rootfs-preserve-created-persistent-volumes"
|
||||
},
|
||||
"installer_inputs": {
|
||||
"prompted": [
|
||||
"stack_name",
|
||||
"base_vmid",
|
||||
"rootfs_storage",
|
||||
"persistent_data_destinations",
|
||||
"frontend_bridge",
|
||||
"frontend_ipv4_mode"
|
||||
],
|
||||
"automatic": [
|
||||
"dependent_vmids",
|
||||
"private_bridge",
|
||||
"private_subnet",
|
||||
"private_service_addresses",
|
||||
"compose_service_aliases",
|
||||
"generated_secrets",
|
||||
"dependency_start_and_stop_order"
|
||||
],
|
||||
"generated_secrets": [
|
||||
{
|
||||
"id": "generated-jwt-secret-key",
|
||||
"strategy": "generate-cryptographically-random-at-install",
|
||||
"bindings": [
|
||||
{
|
||||
"service": "back",
|
||||
"environment_variable": "JWT_SECRET_KEY"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"first_run": {
|
||||
"endpoints": [
|
||||
{
|
||||
"label": "KitchenOwl",
|
||||
"scheme": "http",
|
||||
"port": 80,
|
||||
"path": "/",
|
||||
"source": "upstream-documentation"
|
||||
}
|
||||
],
|
||||
"credentials": []
|
||||
},
|
||||
"proxmox": {
|
||||
"runtime": "native-oci-lxc",
|
||||
"technology_status": "proxmox-technology-preview",
|
||||
"defaults": {
|
||||
"unprivileged": true,
|
||||
"ostype": "auto-from-image",
|
||||
"cores": 2,
|
||||
"memory_mb": 1024,
|
||||
"swap_mb": 512,
|
||||
"rootfs_size_gb": 8,
|
||||
"rootfs_storage": "local-lvm",
|
||||
"volume_storage": "local-lvm",
|
||||
"template_storage": "local",
|
||||
"bridge": "vmbr0",
|
||||
"ipv4": "dhcp",
|
||||
"firewall": true,
|
||||
"host_managed_network": true,
|
||||
"onboot": false,
|
||||
"features": [
|
||||
"nesting=1"
|
||||
],
|
||||
"shutdown_timeout_seconds": 30
|
||||
},
|
||||
"image_metadata_policy": {
|
||||
"entrypoint": "import-from-oci-image",
|
||||
"cmd": "import-from-oci-image",
|
||||
"environment": "import-image-env-then-apply-compose-overrides",
|
||||
"user": "import-from-oci-image",
|
||||
"working_dir": "import-from-oci-image",
|
||||
"stop_signal": "import-from-oci-image"
|
||||
},
|
||||
"adaptations": [
|
||||
{
|
||||
"id": "generated-jwt-signing-key",
|
||||
"upstream_behavior": "The official Compose example uses the placeholder PLEASE_CHANGE_ME for JWT_SECRET_KEY and instructs operators to replace it.",
|
||||
"native_lxc_behavior": "ProxMenux generates a unique cryptographically random JWT signing key and supplies it to the backend service.",
|
||||
"reason": "Avoid deploying the documented placeholder or placing a signing secret in the public recipe.",
|
||||
"behavioral_impact": "The generated key is not an app login credential; KitchenOwl has no catalog-provided default account.",
|
||||
"validation": "pending-runtime"
|
||||
},
|
||||
{
|
||||
"id": "image-healthchecks-preserved",
|
||||
"upstream_behavior": "The web and backend OCI images declare Docker healthchecks in image metadata.",
|
||||
"native_lxc_behavior": "The current image healthchecks are recorded in the stack so ProxMenux can check backend readiness and frontend availability during first start.",
|
||||
"reason": "The Compose file inherits these checks from its images instead of declaring them itself.",
|
||||
"behavioral_impact": "Checks gate stack startup; Docker does not run as a separate health-monitoring daemon inside the LXCs.",
|
||||
"validation": "pending-runtime"
|
||||
}
|
||||
],
|
||||
"custom_compose_stack": true,
|
||||
"custom_compose_interpolated": false,
|
||||
"stack_image_entrypoints": {},
|
||||
"installer_profile": {
|
||||
"stack_driver": "generic-multi-lxc-stack",
|
||||
"volume_preparations": [
|
||||
{
|
||||
"container_path": "/data",
|
||||
"remove_lost_found": true,
|
||||
"owner_strategy": "mapped-application-user",
|
||||
"only_when_mount_type": "managed-volume"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stack_completion_notes": [
|
||||
"Open the web address and follow KitchenOwl’s onboarding to create the first account; no default account is configured."
|
||||
]
|
||||
},
|
||||
"compatibility": {
|
||||
"automatic_install_candidate": true,
|
||||
"validated": false,
|
||||
"supported_compose_keys": [
|
||||
"cap_add",
|
||||
"command",
|
||||
"container_name",
|
||||
"cpu_shares",
|
||||
"deploy",
|
||||
"devices",
|
||||
"entrypoint",
|
||||
"environment",
|
||||
"extra_hosts",
|
||||
"group_add",
|
||||
"healthcheck",
|
||||
"hostname",
|
||||
"image",
|
||||
"init",
|
||||
"ipc",
|
||||
"labels",
|
||||
"logging",
|
||||
"mac_address",
|
||||
"mem_limit",
|
||||
"network_mode",
|
||||
"networks",
|
||||
"ports",
|
||||
"privileged",
|
||||
"restart",
|
||||
"runtime",
|
||||
"security_opt",
|
||||
"shm_size",
|
||||
"stdin_open",
|
||||
"stop_grace_period",
|
||||
"stop_signal",
|
||||
"sysctls",
|
||||
"tty",
|
||||
"ulimits",
|
||||
"user",
|
||||
"volumes",
|
||||
"working_dir"
|
||||
],
|
||||
"untranslated_blockers": [],
|
||||
"policy": "The official two-image Compose stack is mapped to native Proxmox OCI LXCs with private service networking. Lab checks confirmed installation, web/API behavior, persistence, backup and recovery, modification, managed storage, and host-bind storage. Advanced custom settings have not been validated."
|
||||
},
|
||||
"validation": {
|
||||
"schema": "passed-at-generation",
|
||||
"clean_install": "passed-default-official-two-service-stack-and-first-start",
|
||||
"advanced_install": "pending-custom-install-summary-validation",
|
||||
"service_health": "passed-web-api-and-created-user-household-list-item",
|
||||
"restart_persistence": "passed-managed-and-host-bind-data",
|
||||
"backup_restore": "passed-manager-recover-new-vmids-stack-network-and-data",
|
||||
"update_preserves_data": "no-update-available-at-check",
|
||||
"modification": "passed-added-managed-volume-without-image-pull-or-container-recreate",
|
||||
"volumes": "passed-managed-storage-and-host-bind-with-owner-and-data-check",
|
||||
"removal": "passed-manager-removal-temporary-data-cleaned-after-retention-check"
|
||||
},
|
||||
"lifecycle": {
|
||||
"update_strategy": "resolve-latest-image-then-apply-reviewed-native-lxc-update",
|
||||
"registry_state": {
|
||||
"resolved_architecture": null,
|
||||
"resolved_digest": null,
|
||||
"image_version_label": null,
|
||||
"image_created": null
|
||||
},
|
||||
"change_detection": "compare-compose-sha256-and-resolved-latest-image-digest",
|
||||
"automatic_unattended_updates": false,
|
||||
"dependency_lifecycle": {
|
||||
"implementation": "proxmox-hookscript",
|
||||
"trigger": "main-lxc-pre-start",
|
||||
"waits_for_dependency_healthchecks": true,
|
||||
"stops_dependencies_with_main": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
{
|
||||
"schema_version": "0.5.0",
|
||||
"kind": "proxmenux.oci-template",
|
||||
"id": "image-krusader",
|
||||
"status": "generated-unvalidated",
|
||||
"catalog_ui": {
|
||||
"title": {
|
||||
"en_US": "Krusader",
|
||||
"es_ES": "Krusader"
|
||||
},
|
||||
"tagline": {
|
||||
"en_US": "KDE's twin-panel file manager in a browser-based desktop.",
|
||||
"es_ES": "El gestor de archivos de dos paneles de KDE, disponible desde el navegador."
|
||||
},
|
||||
"description": {
|
||||
"en_US": "Open Krusader in a browser and manage files from a directory you explicitly mount at /storage. The maintained jlesage image provides the graphical desktop; ProxMenux runs it as one native OCI LXC.",
|
||||
"es_ES": "Abre Krusader en el navegador y gestiona los archivos de una carpeta que montes expresamente en /storage. La imagen mantenida por jlesage incluye el escritorio gráfico; ProxMenux la ejecuta como un LXC OCI nativo."
|
||||
},
|
||||
"category": "desktop",
|
||||
"category_label": "Browsers & Web Desktops",
|
||||
"author": "jlesage",
|
||||
"developer": "jlesage",
|
||||
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/krusader.webp",
|
||||
"thumbnail": null,
|
||||
"screenshots": [],
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"launch": {
|
||||
"scheme": "https",
|
||||
"port": 5800,
|
||||
"path": "/"
|
||||
},
|
||||
"website": "https://krusader.org/",
|
||||
"documentation": "https://github.com/jlesage/docker-krusader",
|
||||
"repository": "https://github.com/jlesage/docker-krusader",
|
||||
"tips": [
|
||||
"The web login uses HTTPS with an automatically generated self-signed certificate and password. Accept the certificate warning on the first visit or configure a trusted certificate.",
|
||||
"Krusader can read, change and delete files in the directory mounted at /storage. Mount only the directory you intend to manage; the rest of the Proxmox host is not exposed.",
|
||||
"The VNC port is not published. Use the browser interface on port 5800."
|
||||
],
|
||||
"mini_changelog": [],
|
||||
"display_version": null,
|
||||
"updated_at": null
|
||||
},
|
||||
"source": {
|
||||
"provider": "jlesage",
|
||||
"repository": "https://github.com/jlesage/docker-krusader",
|
||||
"revision": "855c8140e1bb65b2a2526d82db44c808f23eb49f",
|
||||
"image_repository_url": "https://hub.docker.com/r/jlesage/krusader",
|
||||
"readme_pushed_at": "2026-09-22T15:18:17Z",
|
||||
"compose_sha256": "9dd16cc30aa19afe7e148598c9cfb0c65c31606d67f3f22254c33a4d05212c7f",
|
||||
"generated_at": "2026-11-02T20:05:40+00:00",
|
||||
"default_branch": "master",
|
||||
"readme_raw_url": "https://raw.githubusercontent.com/jlesage/docker-krusader/855c8140e1bb65b2a2526d82db44c808f23eb49f/README.md"
|
||||
},
|
||||
"container_contract": {
|
||||
"service_name": "krusader",
|
||||
"container_name": "krusader",
|
||||
"image": {
|
||||
"reference": "jlesage/krusader:latest",
|
||||
"registry": "docker.io",
|
||||
"repository": "jlesage/krusader",
|
||||
"tag": "latest",
|
||||
"digest": null,
|
||||
"pull_policy": "resolve-written-reference-to-architecture-digest-at-install"
|
||||
},
|
||||
"environment": [
|
||||
{
|
||||
"name": "USER_ID",
|
||||
"example": "1000",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "upstream-image-default"
|
||||
},
|
||||
{
|
||||
"name": "GROUP_ID",
|
||||
"example": "1000",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "upstream-image-default"
|
||||
},
|
||||
{
|
||||
"name": "TZ",
|
||||
"example": "Europe/Madrid",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "ProxMenux-local-timezone"
|
||||
},
|
||||
{
|
||||
"name": "VNC_LOCALHOST_ONLY",
|
||||
"example": "1",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "upstream-image-security-setting"
|
||||
},
|
||||
{
|
||||
"name": "WEB_AUTHENTICATION",
|
||||
"example": "1",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "upstream-image-security-setting"
|
||||
},
|
||||
{
|
||||
"name": "WEB_AUTHENTICATION_USERNAME",
|
||||
"example": "admin",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "upstream-image-security-setting"
|
||||
},
|
||||
{
|
||||
"name": "WEB_AUTHENTICATION_PASSWORD",
|
||||
"example": "${GENERATED_WEB_PASSWORD}",
|
||||
"required": true,
|
||||
"sensitive": true,
|
||||
"source": "ProxMenux-generated-credential"
|
||||
},
|
||||
{
|
||||
"name": "SECURE_CONNECTION",
|
||||
"example": "1",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "upstream-image-security-setting"
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"id": "krusader-config",
|
||||
"container_path": "/config",
|
||||
"compose_source_example": "krusader-config",
|
||||
"read_only": false,
|
||||
"required": true,
|
||||
"installation_choice": [
|
||||
"managed-volume"
|
||||
],
|
||||
"default": "managed-volume",
|
||||
"managed_volume": {
|
||||
"backup": true,
|
||||
"default_size_gb": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "krusader-storage",
|
||||
"container_path": "/storage",
|
||||
"compose_source_example": "/mnt/oci-shared/krusader/storage",
|
||||
"read_only": false,
|
||||
"required": true,
|
||||
"installation_choice": [
|
||||
"managed-volume",
|
||||
"host-bind"
|
||||
],
|
||||
"default": "host-bind",
|
||||
"managed_volume": {
|
||||
"backup": true,
|
||||
"default_size_gb": 8
|
||||
}
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
"container_port": 5800,
|
||||
"published_example": 5800,
|
||||
"protocol": "tcp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
}
|
||||
],
|
||||
"related_services": [],
|
||||
"restart": "unless-stopped",
|
||||
"stop_grace_period": null,
|
||||
"original_compose": "name: krusader\nservices:\n krusader:\n image: jlesage/krusader:latest\n container_name: krusader\n restart: unless-stopped\n environment:\n USER_ID: '1000'\n GROUP_ID: '1000'\n TZ: Europe/Madrid\n VNC_LOCALHOST_ONLY: '1'\n WEB_AUTHENTICATION: '1'\n WEB_AUTHENTICATION_USERNAME: admin\n WEB_AUTHENTICATION_PASSWORD: ${GENERATED_WEB_PASSWORD}\n SECURE_CONNECTION: '1'\n ports:\n - 5800:5800/tcp\n volumes:\n - krusader-config:/config\n - /mnt/oci-shared/krusader/storage:/storage\nvolumes:\n krusader-config: {}\n"
|
||||
},
|
||||
"compose_stack": {
|
||||
"project_name": "krusader",
|
||||
"deployment_model": "one-native-oci-lxc-per-compose-service",
|
||||
"user_experience": "single-application-install",
|
||||
"main_service": "krusader",
|
||||
"service_count": 1,
|
||||
"services": [
|
||||
{
|
||||
"name": "krusader",
|
||||
"image": "jlesage/krusader:latest",
|
||||
"is_main": true,
|
||||
"role": "frontend",
|
||||
"vmid_offset": 0,
|
||||
"depends_on": [],
|
||||
"frontend_network": true,
|
||||
"private_network": false,
|
||||
"compose": {
|
||||
"environment": {
|
||||
"GROUP_ID": "1000",
|
||||
"SECURE_CONNECTION": "1",
|
||||
"TZ": "Europe/Madrid",
|
||||
"USER_ID": "1000",
|
||||
"VNC_LOCALHOST_ONLY": "1",
|
||||
"WEB_AUTHENTICATION": "1",
|
||||
"WEB_AUTHENTICATION_PASSWORD": "${GENERATED_GENERATED_WEB_PASSWORD}",
|
||||
"WEB_AUTHENTICATION_USERNAME": "admin"
|
||||
},
|
||||
"image": "jlesage/krusader:latest",
|
||||
"ports": [
|
||||
"5800:5800"
|
||||
],
|
||||
"volumes": [
|
||||
"./config:/config",
|
||||
"./storage:/storage"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"top_level": {
|
||||
"name": "krusader"
|
||||
},
|
||||
"networking": {
|
||||
"frontend": "selected-proxmox-bridge",
|
||||
"private_required": false,
|
||||
"private_creation": "automatic-create-if-missing",
|
||||
"private_address_allocation": "automatic-static-address-per-service",
|
||||
"service_discovery": "private-addresses-with-compose-service-host-aliases",
|
||||
"dependency_external_access": "disabled-unless-service-publishes-ports",
|
||||
"prompt_user_for_private_network": false
|
||||
},
|
||||
"storage": [
|
||||
{
|
||||
"id": "krusader-volume-0",
|
||||
"service": "krusader",
|
||||
"container_path": "/config",
|
||||
"mode": "managed-volume",
|
||||
"user_selectable": false,
|
||||
"backup": true,
|
||||
"shared_with_other_lxc": false,
|
||||
"source_path": null,
|
||||
"source_path_prompt": null
|
||||
},
|
||||
{
|
||||
"id": "krusader-volume-1",
|
||||
"service": "krusader",
|
||||
"container_path": "/storage",
|
||||
"mode": "user-selectable",
|
||||
"user_selectable": true,
|
||||
"backup": true,
|
||||
"shared_with_other_lxc": false,
|
||||
"source_path": null,
|
||||
"source_path_prompt": "Host directory for Krusader:/storage",
|
||||
"default": "host-bind",
|
||||
"installation_choice": [
|
||||
"managed-volume",
|
||||
"host-bind"
|
||||
]
|
||||
}
|
||||
],
|
||||
"orchestration": {
|
||||
"reserve_vmids_atomically": 1,
|
||||
"start_order": [
|
||||
"krusader"
|
||||
],
|
||||
"stop_order": [
|
||||
"krusader"
|
||||
],
|
||||
"dependency_readiness": "compose-healthcheck-then-port-or-process-fallback",
|
||||
"rollback_on_failure": "remove-new-rootfs-preserve-created-persistent-volumes"
|
||||
},
|
||||
"installer_inputs": {
|
||||
"prompted": [
|
||||
"stack_name",
|
||||
"base_vmid",
|
||||
"rootfs_storage",
|
||||
"persistent_data_destinations",
|
||||
"frontend_bridge",
|
||||
"frontend_ipv4_mode"
|
||||
],
|
||||
"automatic": [
|
||||
"dependent_vmids",
|
||||
"private_bridge",
|
||||
"private_subnet",
|
||||
"private_service_addresses",
|
||||
"compose_service_aliases",
|
||||
"generated_secrets",
|
||||
"dependency_start_and_stop_order"
|
||||
],
|
||||
"generated_secrets": [
|
||||
{
|
||||
"id": "generated-web-password",
|
||||
"strategy": "generate-cryptographically-random-at-install",
|
||||
"bindings": [
|
||||
{
|
||||
"service": "krusader",
|
||||
"environment_variable": "WEB_AUTHENTICATION_PASSWORD"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"first_run": {
|
||||
"endpoints": [
|
||||
{
|
||||
"label": "Krusader Web UI",
|
||||
"scheme": "https",
|
||||
"port": 5800,
|
||||
"path": "/",
|
||||
"source": "upstream-jlesage-secure-connection-and-web-authentication"
|
||||
}
|
||||
],
|
||||
"credentials": [
|
||||
{
|
||||
"label": "Krusader Web UI",
|
||||
"type": "configured-or-installer-generated",
|
||||
"username": "admin",
|
||||
"password": null,
|
||||
"username_environment": "WEB_AUTHENTICATION_USERNAME",
|
||||
"password_environment": "WEB_AUTHENTICATION_PASSWORD",
|
||||
"change_required": false,
|
||||
"source": "ProxMenux generated password and upstream image environment",
|
||||
"retrieval": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"proxmox": {
|
||||
"runtime": "native-oci-lxc",
|
||||
"technology_status": "proxmox-technology-preview",
|
||||
"defaults": {
|
||||
"unprivileged": true,
|
||||
"ostype": "auto-from-image",
|
||||
"cores": 2,
|
||||
"memory_mb": 2048,
|
||||
"swap_mb": 512,
|
||||
"rootfs_size_gb": 8,
|
||||
"rootfs_storage": "local-lvm",
|
||||
"volume_storage": "local-lvm",
|
||||
"template_storage": "local",
|
||||
"bridge": "vmbr0",
|
||||
"ipv4": "dhcp",
|
||||
"firewall": true,
|
||||
"host_managed_network": true,
|
||||
"onboot": false,
|
||||
"features": [
|
||||
"nesting=1"
|
||||
],
|
||||
"shutdown_timeout_seconds": 30
|
||||
},
|
||||
"image_metadata_policy": {
|
||||
"entrypoint": "import-from-oci-image",
|
||||
"cmd": "import-from-oci-image",
|
||||
"environment": "import-image-env-then-apply-compose-overrides",
|
||||
"user": "import-from-oci-image",
|
||||
"working_dir": "import-from-oci-image",
|
||||
"stop_signal": "import-from-oci-image"
|
||||
},
|
||||
"adaptations": [
|
||||
{
|
||||
"id": "web-authentication-https",
|
||||
"upstream_behavior": "The jlesage image can protect its browser GUI with WEB_AUTHENTICATION and requires SECURE_CONNECTION for authenticated web access.",
|
||||
"native_lxc_behavior": "Enable authenticated HTTPS on port 5800, generate a unique password at install and keep VNC bound to localhost.",
|
||||
"reason": "Krusader can modify files exposed under /storage, so the browser endpoint must not be unauthenticated.",
|
||||
"behavioral_impact": "The image generates a self-signed TLS certificate; a browser may ask to trust it on first access.",
|
||||
"validation": "pending"
|
||||
},
|
||||
{
|
||||
"id": "scoped-storage-mount",
|
||||
"upstream_behavior": "Docker Compose mounts the selected host directory at /storage.",
|
||||
"native_lxc_behavior": "The same selected directory is mounted in the dedicated OCI LXC; no other host paths are exposed.",
|
||||
"reason": "The file manager must only see data explicitly selected by the user.",
|
||||
"behavioral_impact": "Write access permits changing or deleting files in that one directory.",
|
||||
"validation": "pending"
|
||||
},
|
||||
{
|
||||
"id": "dedicated-lxc-network",
|
||||
"upstream_behavior": "Docker publishes web port 5800 on the Docker host.",
|
||||
"native_lxc_behavior": "The native LXC listens on its own IP at port 5800 without Docker NAT.",
|
||||
"reason": "The LXC has a dedicated network namespace and address.",
|
||||
"behavioral_impact": "The port is reached at the LXC address rather than through a host-published port.",
|
||||
"validation": "pending"
|
||||
}
|
||||
],
|
||||
"installer_profile": {
|
||||
"volume_preparations": [
|
||||
{
|
||||
"container_path": "/config",
|
||||
"remove_lost_found": true,
|
||||
"owner_strategy": "mapped-application-user",
|
||||
"uid": 1000,
|
||||
"gid": 1000,
|
||||
"only_when_mount_type": "managed-volume"
|
||||
},
|
||||
{
|
||||
"container_path": "/storage",
|
||||
"remove_lost_found": true,
|
||||
"owner_strategy": "mapped-application-user",
|
||||
"uid": 1000,
|
||||
"gid": 1000,
|
||||
"only_when_mount_type": "managed-volume"
|
||||
}
|
||||
],
|
||||
"startup_healthcheck": {
|
||||
"scheme": "https",
|
||||
"port": 5800,
|
||||
"path": "/",
|
||||
"timeout_seconds": 240,
|
||||
"request_timeout_seconds": 10,
|
||||
"stability_seconds": 4,
|
||||
"verify_tls": false
|
||||
},
|
||||
"generated_sensitive_environment": {
|
||||
"WEB_AUTHENTICATION_PASSWORD": {
|
||||
"strategy": "token-hex",
|
||||
"bytes": 16,
|
||||
"prompt": false
|
||||
}
|
||||
},
|
||||
"volume_owner": {
|
||||
"uid": 1000,
|
||||
"gid": 1000
|
||||
}
|
||||
}
|
||||
},
|
||||
"compatibility": {
|
||||
"automatic_install_candidate": true,
|
||||
"validated": false,
|
||||
"supported_compose_keys": [
|
||||
"cap_add",
|
||||
"command",
|
||||
"container_name",
|
||||
"cpu_shares",
|
||||
"deploy",
|
||||
"devices",
|
||||
"entrypoint",
|
||||
"environment",
|
||||
"extra_hosts",
|
||||
"group_add",
|
||||
"healthcheck",
|
||||
"hostname",
|
||||
"image",
|
||||
"init",
|
||||
"ipc",
|
||||
"labels",
|
||||
"logging",
|
||||
"mac_address",
|
||||
"mem_limit",
|
||||
"network_mode",
|
||||
"networks",
|
||||
"ports",
|
||||
"privileged",
|
||||
"restart",
|
||||
"runtime",
|
||||
"security_opt",
|
||||
"shm_size",
|
||||
"stdin_open",
|
||||
"stop_grace_period",
|
||||
"stop_signal",
|
||||
"sysctls",
|
||||
"tty",
|
||||
"ulimits",
|
||||
"user",
|
||||
"volumes",
|
||||
"working_dir"
|
||||
],
|
||||
"untranslated_blockers": [],
|
||||
"policy": "Single-image definitions are installable when every declared Compose option has a native Proxmox translation. Multi-image and unsupported runtime features remain blocked until their orchestrator or mapping is available."
|
||||
},
|
||||
"validation": {
|
||||
"schema": "passed-at-generation",
|
||||
"clean_install": "passed-first-start",
|
||||
"service_health": "passed-login-page-and-authenticated-session-http-200",
|
||||
"restart_persistence": "passed-container-restart-and-host-bind-write-check",
|
||||
"backup_restore": "pending",
|
||||
"update_preserves_data": "pending",
|
||||
"image_resolution": "passed-skopeo-327mb-11-layers-integrity",
|
||||
"credentials": "passed-generated-password-login-and-session-cookie",
|
||||
"advanced_install": "pending-custom-install-summary-validation",
|
||||
"managed_volume": "passed-managed-config-volume",
|
||||
"host_bind": "passed-storage-bind-owner-and-app-user-write",
|
||||
"browser_visual": "pending-self-signed-certificate-review",
|
||||
"modification": "pending",
|
||||
"removal": "passed-manager-remove-ct-disks-record-and-empty-test-path-cleaned",
|
||||
"web_authentication": "passed-generated-credential-post-and-session-cookie"
|
||||
},
|
||||
"lifecycle": {
|
||||
"update_strategy": "resolve-latest-image-then-apply-reviewed-native-lxc-update",
|
||||
"registry_state": {
|
||||
"resolved_architecture": "amd64",
|
||||
"resolved_digest": "sha256:ecb77bb73b1d3c66720eefb07805f70289349b449d1ddaa15d120c399bd89cc4",
|
||||
"image_version_label": "26.09.1",
|
||||
"image_created": "2026-09-22T15:22:56.230858151Z"
|
||||
},
|
||||
"change_detection": "compare-compose-sha256-and-resolved-latest-image-digest",
|
||||
"automatic_unattended_updates": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
{
|
||||
"schema_version": "0.5.0",
|
||||
"kind": "proxmenux.oci-template",
|
||||
"id": "image-spoolman",
|
||||
"status": "generated-unvalidated",
|
||||
"catalog_ui": {
|
||||
"title": {
|
||||
"en_US": "Spoolman"
|
||||
},
|
||||
"tagline": {
|
||||
"en_US": "Track 3D printer filament spools and usage."
|
||||
},
|
||||
"description": {
|
||||
"en_US": "A self-hosted filament inventory for 3D printers. Manage spools, materials, manufacturers, locations and labels in the web UI, and integrate with Moonraker, OctoPrint and other printer tools. The official image uses SQLite by default; no separate database container is required."
|
||||
},
|
||||
"category": "tools",
|
||||
"category_label": "Tools",
|
||||
"author": "Donkie",
|
||||
"developer": "Donkie",
|
||||
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/spoolman.webp",
|
||||
"thumbnail": null,
|
||||
"screenshots": [],
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"launch": {
|
||||
"scheme": "http",
|
||||
"port": 8000,
|
||||
"path": "/"
|
||||
},
|
||||
"website": "https://github.com/Donkie/Spoolman",
|
||||
"documentation": "https://github.com/Donkie/Spoolman/wiki/Installation",
|
||||
"repository": "https://github.com/Donkie/Spoolman",
|
||||
"tips": [
|
||||
"Spoolman has no built-in authentication. Keep it on a trusted network; do not expose it directly to the internet.",
|
||||
"The official image uses SQLite by default and stores the database, automatic backups, logs and cache in the persistent data directory."
|
||||
],
|
||||
"mini_changelog": [],
|
||||
"display_version": null,
|
||||
"updated_at": null
|
||||
},
|
||||
"source": {
|
||||
"provider": "Donkie",
|
||||
"repository": "https://github.com/Donkie/Spoolman",
|
||||
"revision": "ff8efd517b76a4b2783f29c89e9b9fbcd8cd167b",
|
||||
"image_repository_url": "https://github.com/Donkie/Spoolman/pkgs/container/spoolman",
|
||||
"readme_pushed_at": "2026-10-31T09:39:19Z",
|
||||
"compose_sha256": "d9b115c4c37589f9813e54f926fdd22e7a10a44a6e56160f837b1247638fac83",
|
||||
"generated_at": "2026-11-02T20:02:46+00:00",
|
||||
"default_branch": "master",
|
||||
"readme_raw_url": "https://github.com/Donkie/Spoolman/wiki/Installation"
|
||||
},
|
||||
"container_contract": {
|
||||
"service_name": "spoolman",
|
||||
"container_name": "spoolman",
|
||||
"image": {
|
||||
"reference": "ghcr.io/donkie/spoolman:latest",
|
||||
"registry": "ghcr.io",
|
||||
"repository": "ghcr.io/donkie/spoolman",
|
||||
"tag": "latest",
|
||||
"digest": null,
|
||||
"pull_policy": "resolve-written-reference-to-architecture-digest-at-install"
|
||||
},
|
||||
"environment": [
|
||||
{
|
||||
"name": "PUID",
|
||||
"example": "1000",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "upstream-image-default",
|
||||
"prompt_user": false
|
||||
},
|
||||
{
|
||||
"name": "PGID",
|
||||
"example": "1000",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "upstream-image-default",
|
||||
"prompt_user": false
|
||||
},
|
||||
{
|
||||
"name": "TZ",
|
||||
"example": "Etc/UTC",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "upstream-compose",
|
||||
"prompt_user": false
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"id": "volume-0",
|
||||
"container_path": "/home/app/.local/share/spoolman",
|
||||
"compose_source_example": "./data",
|
||||
"read_only": false,
|
||||
"required": true,
|
||||
"installation_choice": [
|
||||
"managed-volume",
|
||||
"host-bind"
|
||||
],
|
||||
"default": "managed-volume",
|
||||
"managed_volume": {
|
||||
"backup": true,
|
||||
"default_size_gb": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
"container_port": 8000,
|
||||
"published_example": 7912,
|
||||
"protocol": "tcp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
}
|
||||
],
|
||||
"related_services": [],
|
||||
"restart": "unless-stopped",
|
||||
"stop_grace_period": null,
|
||||
"original_compose": "services:\n spoolman:\n image: ghcr.io/donkie/spoolman:latest\n container_name: spoolman\n restart: unless-stopped\n ports:\n - \"7912:8000/tcp\"\n volumes:\n - ./data:/home/app/.local/share/spoolman\n environment:\n PUID: \"1000\"\n PGID: \"1000\"\n TZ: Etc/UTC\n"
|
||||
},
|
||||
"compose_stack": {
|
||||
"project_name": "spoolman",
|
||||
"deployment_model": "one-native-oci-lxc-per-compose-service",
|
||||
"user_experience": "single-application-install",
|
||||
"main_service": "spoolman",
|
||||
"service_count": 1,
|
||||
"services": [
|
||||
{
|
||||
"name": "spoolman",
|
||||
"image": "ghcr.io/donkie/spoolman:latest",
|
||||
"is_main": true,
|
||||
"role": "frontend",
|
||||
"vmid_offset": 0,
|
||||
"depends_on": [],
|
||||
"frontend_network": true,
|
||||
"private_network": false,
|
||||
"compose": {
|
||||
"image": "ghcr.io/donkie/spoolman:latest",
|
||||
"container_name": "spoolman",
|
||||
"restart": "unless-stopped",
|
||||
"ports": [
|
||||
"7912:8000/tcp"
|
||||
],
|
||||
"volumes": [
|
||||
"./data:/home/app/.local/share/spoolman"
|
||||
],
|
||||
"environment": {
|
||||
"PUID": "1000",
|
||||
"PGID": "1000",
|
||||
"TZ": "Etc/UTC"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"top_level": {
|
||||
"name": "spoolman"
|
||||
},
|
||||
"networking": {
|
||||
"frontend": "selected-proxmox-bridge",
|
||||
"private_required": false,
|
||||
"private_creation": "automatic-create-if-missing",
|
||||
"private_address_allocation": "automatic-static-address-per-service",
|
||||
"service_discovery": "private-addresses-with-compose-service-host-aliases",
|
||||
"dependency_external_access": "disabled-unless-service-publishes-ports",
|
||||
"prompt_user_for_private_network": false
|
||||
},
|
||||
"storage": [
|
||||
{
|
||||
"id": "spoolman-volume-0",
|
||||
"service": "spoolman",
|
||||
"container_path": "/home/app/.local/share/spoolman",
|
||||
"mode": "managed-volume",
|
||||
"user_selectable": true,
|
||||
"backup": true,
|
||||
"shared_with_other_lxc": false,
|
||||
"source_path": null,
|
||||
"source_path_prompt": null
|
||||
}
|
||||
],
|
||||
"orchestration": {
|
||||
"reserve_vmids_atomically": 1,
|
||||
"start_order": [
|
||||
"spoolman"
|
||||
],
|
||||
"stop_order": [
|
||||
"spoolman"
|
||||
],
|
||||
"dependency_readiness": "compose-healthcheck-then-port-or-process-fallback",
|
||||
"rollback_on_failure": "remove-new-rootfs-preserve-created-persistent-volumes"
|
||||
},
|
||||
"installer_inputs": {
|
||||
"prompted": [
|
||||
"stack_name",
|
||||
"base_vmid",
|
||||
"rootfs_storage",
|
||||
"persistent_data_destinations",
|
||||
"frontend_bridge",
|
||||
"frontend_ipv4_mode"
|
||||
],
|
||||
"automatic": [
|
||||
"dependent_vmids",
|
||||
"private_bridge",
|
||||
"private_subnet",
|
||||
"private_service_addresses",
|
||||
"compose_service_aliases",
|
||||
"generated_secrets",
|
||||
"dependency_start_and_stop_order"
|
||||
],
|
||||
"generated_secrets": []
|
||||
}
|
||||
},
|
||||
"first_run": {
|
||||
"endpoints": [
|
||||
{
|
||||
"label": "Spoolman web interface",
|
||||
"scheme": "http",
|
||||
"port": 8000,
|
||||
"path": "/",
|
||||
"source": "upstream-image"
|
||||
}
|
||||
],
|
||||
"credentials": []
|
||||
},
|
||||
"proxmox": {
|
||||
"runtime": "native-oci-lxc",
|
||||
"technology_status": "proxmox-technology-preview",
|
||||
"defaults": {
|
||||
"unprivileged": true,
|
||||
"ostype": "auto-from-image",
|
||||
"cores": 2,
|
||||
"memory_mb": 1024,
|
||||
"swap_mb": 512,
|
||||
"rootfs_size_gb": 8,
|
||||
"rootfs_storage": "local-lvm",
|
||||
"volume_storage": "local-lvm",
|
||||
"template_storage": "local",
|
||||
"bridge": "vmbr0",
|
||||
"ipv4": "dhcp",
|
||||
"firewall": true,
|
||||
"host_managed_network": true,
|
||||
"onboot": false,
|
||||
"features": [
|
||||
"nesting=1"
|
||||
],
|
||||
"shutdown_timeout_seconds": 30
|
||||
},
|
||||
"image_metadata_policy": {
|
||||
"entrypoint": "import-from-oci-image",
|
||||
"cmd": "import-from-oci-image",
|
||||
"environment": "import-image-env-then-apply-compose-overrides",
|
||||
"user": "import-from-oci-image",
|
||||
"working_dir": "import-from-oci-image",
|
||||
"stop_signal": "import-from-oci-image"
|
||||
},
|
||||
"adaptations": [
|
||||
{
|
||||
"id": "imported-compose-source",
|
||||
"upstream_behavior": "The source definition deploys the complete Docker Compose application model.",
|
||||
"native_lxc_behavior": "The source model is preserved and remains blocked until every service option has a reviewed native Proxmox mapping.",
|
||||
"reason": "Catalog import must not imply runtime compatibility.",
|
||||
"behavioral_impact": "No automatic installation before review.",
|
||||
"validation": "pending-per-application"
|
||||
},
|
||||
{
|
||||
"id": "rolling-latest-image",
|
||||
"upstream_behavior": "A discovered Compose may pin a release tag or digest.",
|
||||
"native_lxc_behavior": "ProxMenux selects the same image repository with the latest tag for catalog installations.",
|
||||
"reason": "The automatic catalog intentionally offers rolling latest images; pinned versions belong to the future manual installer.",
|
||||
"behavioral_impact": "The installed release can be newer than the discovered Compose revision.",
|
||||
"validation": "pending-per-application"
|
||||
},
|
||||
{
|
||||
"id": "dedicated-lxc-network",
|
||||
"upstream_behavior": "Docker publishes selected container ports on the Docker host.",
|
||||
"native_lxc_behavior": "A reviewed native application will listen on its original container ports at a dedicated LXC address.",
|
||||
"reason": "A native LXC has its own address and does not require Docker port NAT.",
|
||||
"behavioral_impact": "Published ports are metadata; ProxMenux URLs use the matching container target port.",
|
||||
"validation": "pending-per-application"
|
||||
},
|
||||
{
|
||||
"id": "compose-shm-size",
|
||||
"upstream_behavior": "Compose sets the size of the container /dev/shm tmpfs.",
|
||||
"native_lxc_behavior": "ProxMenux mounts a native LXC tmpfs at /dev/shm with the same requested capacity.",
|
||||
"reason": "The OCI image runs directly as an LXC and therefore needs the equivalent Proxmox mount entry.",
|
||||
"behavioral_impact": "None expected.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-command",
|
||||
"upstream_behavior": "Compose replaces the image Cmd while retaining its Entrypoint.",
|
||||
"native_lxc_behavior": "ProxMenux reads the official OCI Entrypoint and combines it with the Compose command as the native LXC init command.",
|
||||
"reason": "Proxmox stores the effective OCI process as one entrypoint string.",
|
||||
"behavioral_impact": "None expected.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-privileged-mode",
|
||||
"upstream_behavior": "Compose selects whether the container runs in privileged mode.",
|
||||
"native_lxc_behavior": "ProxMenux keeps the LXC unprivileged by default and exposes the broad Compose privilege request only as an explicit compatibility option.",
|
||||
"reason": "A Compose privilege request is source metadata, not proof that the image technically requires a privileged LXC.",
|
||||
"behavioral_impact": "A privileged LXC has weaker isolation from the Proxmox host.",
|
||||
"validation": "native-equivalent"
|
||||
},
|
||||
{
|
||||
"id": "compose-process-runtime",
|
||||
"upstream_behavior": "Compose can replace Entrypoint, User and WorkingDir and request an init process or interactive terminal.",
|
||||
"native_lxc_behavior": "ProxMenux applies the process overrides through native LXC init directives; lxc-init provides PID 1 supervision and the CT console provides terminal access.",
|
||||
"reason": "The OCI process must start with the same identity, command and working directory without Docker.",
|
||||
"behavioral_impact": "Compose stdin_open and tty become access through the Proxmox LXC console.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-healthcheck",
|
||||
"upstream_behavior": "Docker periodically executes the declared container healthcheck.",
|
||||
"native_lxc_behavior": "For a single-service LXC, ProxMenux translates HTTP localhost checks into a mandatory first-start service check.",
|
||||
"reason": "Proxmox has no persistent Docker health state, while the installer still must detect a failed first boot.",
|
||||
"behavioral_impact": "The check runs during installation rather than continuously after installation.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-cpu-priority",
|
||||
"upstream_behavior": "Docker cpu_shares sets a relative scheduling weight with 1024 as its neutral value.",
|
||||
"native_lxc_behavior": "ProxMenux converts the relative weight to Proxmox cpuunits with 100 as its neutral value and lets the user review it.",
|
||||
"reason": "Both settings express relative CPU priority on different scales.",
|
||||
"behavioral_impact": "Rounding and Proxmox minimum limits can slightly change very low weights.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-resource-limits",
|
||||
"upstream_behavior": "mem_limit sets the memory ceiling; ulimits sets process soft/hard resource limits.",
|
||||
"native_lxc_behavior": "mem_limit supplies the editable Proxmox memory default, rounded up to MiB; ulimits maps to lxc.prlimit with -1 represented as unlimited.",
|
||||
"reason": "Use native Proxmox memory and LXC prlimits, without a wrapper or changing the host kernel configuration.",
|
||||
"behavioral_impact": "User-selected memory overrides Compose. Swap is a separate choice. Kernel restrictions still apply; nproc counts processes by real UID, not by container.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-network-identity",
|
||||
"upstream_behavior": "Compose can set hostname, MAC address, extra hosts and attach a service to Docker networks.",
|
||||
"native_lxc_behavior": "ProxMenux applies hostname and MAC to net0, writes additional host aliases into the LXC and uses its dedicated bridge connection for single-service networks.",
|
||||
"reason": "A dedicated LXC has its own network namespace and does not need a Docker bridge per service.",
|
||||
"behavioral_impact": "host-gateway resolves to the IPv4 address of the selected Proxmox bridge.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-network-mode",
|
||||
"upstream_behavior": "Docker host mode removes Docker network isolation; bridge and default use a Docker-managed network.",
|
||||
"native_lxc_behavior": "The OCI process uses the dedicated LXC network namespace directly, so host, bridge and default all listen on the LXC address without Docker NAT.",
|
||||
"reason": "The LXC is the application host and already has its own address and port namespace.",
|
||||
"behavioral_impact": "host means the LXC host, never the Proxmox host; this preserves Proxmox network isolation.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-capabilities-and-sysctls",
|
||||
"upstream_behavior": "Compose can add Linux capabilities and set kernel parameters in the container network namespace.",
|
||||
"native_lxc_behavior": "ProxMenux validates requested capabilities against the native LXC capability set and writes namespaced network settings as lxc.sysctl directives.",
|
||||
"reason": "A native OCI-LXC already starts with the namespaced capability set; lxc.cap.keep would incorrectly discard unrelated required capabilities.",
|
||||
"behavioral_impact": "Host-global capabilities such as SYS_MODULE remain blocked until their host prerequisite is explicitly adapted.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "docker-engine-metadata",
|
||||
"upstream_behavior": "Compose labels annotate Docker objects and the json-file logging driver rotates Docker-managed logs.",
|
||||
"native_lxc_behavior": "Labels remain source metadata; Docker json-file settings are not applied because the OCI process runs directly under LXC.",
|
||||
"reason": "There is no Docker object or Docker json-file log behind a native OCI-LXC application.",
|
||||
"behavioral_impact": "Docker-only label consumers and Docker log-driver rotation do not exist in the native deployment.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-device-passthrough",
|
||||
"upstream_behavior": "Compose passes host character devices or requests an NVIDIA runtime GPU.",
|
||||
"native_lxc_behavior": "ProxMenux converts recognized device declarations to native Proxmox dev resources; NVIDIA profiles also inject compatible host driver libraries read-only.",
|
||||
"reason": "Native OCI-LXC does not execute Docker device or NVIDIA runtime hooks.",
|
||||
"behavioral_impact": "Hardware is exposed only after explicit user confirmation and host-path validation.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-host-ipc",
|
||||
"upstream_behavior": "ipc: host shares the Docker host IPC namespace, commonly to avoid Docker's small default shared-memory allocation.",
|
||||
"native_lxc_behavior": "The application keeps the LXC IPC namespace and receives a configurable 1 GiB /dev/shm instead of sharing Proxmox host IPC.",
|
||||
"reason": "Processes in a single native LXC already share one IPC namespace; retaining isolation is safer than exposing host IPC.",
|
||||
"behavioral_impact": "The application cannot exchange IPC objects with processes on the Proxmox host.",
|
||||
"validation": "not-requested-by-compose"
|
||||
}
|
||||
],
|
||||
"installer_profile": {
|
||||
"startup_healthcheck": {
|
||||
"scheme": "http",
|
||||
"port": 8000,
|
||||
"path": "/",
|
||||
"timeout_seconds": 180,
|
||||
"request_timeout_seconds": 10,
|
||||
"stability_seconds": 4,
|
||||
"verify_tls": true
|
||||
},
|
||||
"volume_preparations": [
|
||||
{
|
||||
"container_path": "/home/app/.local/share/spoolman",
|
||||
"remove_lost_found": true,
|
||||
"owner_strategy": "mapped-application-user",
|
||||
"only_when_mount_type": "managed-volume"
|
||||
}
|
||||
],
|
||||
"volume_owner": {
|
||||
"uid": 1000,
|
||||
"gid": 1000
|
||||
}
|
||||
}
|
||||
},
|
||||
"compatibility": {
|
||||
"automatic_install_candidate": true,
|
||||
"validated": false,
|
||||
"supported_compose_keys": [
|
||||
"cap_add",
|
||||
"command",
|
||||
"container_name",
|
||||
"cpu_shares",
|
||||
"deploy",
|
||||
"devices",
|
||||
"entrypoint",
|
||||
"environment",
|
||||
"extra_hosts",
|
||||
"group_add",
|
||||
"healthcheck",
|
||||
"hostname",
|
||||
"image",
|
||||
"init",
|
||||
"ipc",
|
||||
"labels",
|
||||
"logging",
|
||||
"mac_address",
|
||||
"mem_limit",
|
||||
"network_mode",
|
||||
"networks",
|
||||
"ports",
|
||||
"privileged",
|
||||
"restart",
|
||||
"runtime",
|
||||
"security_opt",
|
||||
"shm_size",
|
||||
"stdin_open",
|
||||
"stop_grace_period",
|
||||
"sysctls",
|
||||
"tty",
|
||||
"ulimits",
|
||||
"user",
|
||||
"volumes",
|
||||
"working_dir"
|
||||
],
|
||||
"untranslated_blockers": [],
|
||||
"policy": "Single-image definitions are installable when every declared Compose option has a native Proxmox translation. Multi-image and unsupported runtime features remain blocked until their orchestrator or mapping is available."
|
||||
},
|
||||
"validation": {
|
||||
"schema": "passed-at-generation",
|
||||
"clean_install": "passed-default-official-image-skopeo-and-first-start-http",
|
||||
"advanced_install": "pending-custom-install-summary-validation",
|
||||
"service_health": "passed-web-api-crud",
|
||||
"restart_persistence": "passed-managed-and-host-bind-database-and-records",
|
||||
"backup_restore": "passed-pbs-backup-and-manager-recover-record-preserved",
|
||||
"update_preserves_data": "no-update-available-at-check",
|
||||
"modification": "passed-cpu-ram-up-and-down-without-image-pull-or-recreate",
|
||||
"volumes": "passed-managed-storage-and-host-bind-crud-owner-and-data-check",
|
||||
"removal": "passed-manager-removal-host-path-retained-then-temporary-data-cleaned"
|
||||
},
|
||||
"lifecycle": {
|
||||
"update_strategy": "resolve-latest-image-then-apply-reviewed-native-lxc-update",
|
||||
"registry_state": {
|
||||
"resolved_architecture": null,
|
||||
"resolved_digest": null,
|
||||
"image_version_label": null,
|
||||
"image_created": null
|
||||
},
|
||||
"change_detection": "compare-compose-sha256-and-resolved-latest-image-digest",
|
||||
"automatic_unattended_updates": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
{
|
||||
"schema_version": "0.5.0",
|
||||
"kind": "proxmenux.oci-template",
|
||||
"id": "image-unbound",
|
||||
"status": "generated-unvalidated",
|
||||
"catalog_ui": {
|
||||
"title": {
|
||||
"en_US": "Unbound"
|
||||
},
|
||||
"tagline": {
|
||||
"en_US": "A validating, recursive DNS resolver for your private network."
|
||||
},
|
||||
"description": {
|
||||
"en_US": "Run the community-maintained klutchell image of Unbound as a DNS resolver on its own Proxmox LXC. It listens on TCP and UDP port 53 and can load optional custom configuration files."
|
||||
},
|
||||
"category": "adblock",
|
||||
"category_label": "Adblock & DNS",
|
||||
"author": "Kyle Harding",
|
||||
"developer": "Kyle Harding (klutchell)",
|
||||
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/unbound.webp",
|
||||
"thumbnail": null,
|
||||
"screenshots": [],
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64",
|
||||
"armhf"
|
||||
],
|
||||
"launch": {
|
||||
"scheme": "http",
|
||||
"port": null,
|
||||
"path": "/"
|
||||
},
|
||||
"website": "https://unbound.net/",
|
||||
"documentation": "https://github.com/klutchell/unbound-docker#readme",
|
||||
"repository": "https://github.com/klutchell/unbound-docker",
|
||||
"tips": [
|
||||
"Unbound has no web interface. Use the LXC IP as a DNS server on TCP and UDP port 53.",
|
||||
"The default image config listens on all interfaces and accepts clients from private IPv4 ranges. Keep it behind your trusted LAN firewall; do not expose recursive DNS publicly.",
|
||||
"The original GitHub repository has moved to klutchell/unbound-docker; the published Docker Hub image remains klutchell/unbound:main. The image is an unofficial community image, not published by NLnet Labs.",
|
||||
"Optional custom .conf files go under /etc/unbound/custom.conf.d and must be readable by UID/GID 101:102 or world-readable."
|
||||
],
|
||||
"mini_changelog": [],
|
||||
"display_version": null,
|
||||
"updated_at": null
|
||||
},
|
||||
"source": {
|
||||
"provider": "klutchell",
|
||||
"repository": "https://github.com/klutchell/unbound-docker",
|
||||
"revision": "1efbea960bf9462b6fb6dd71aa702ad32d6820d7",
|
||||
"default_branch": "main",
|
||||
"image_repository_url": "https://hub.docker.com/r/klutchell/unbound",
|
||||
"readme_pushed_at": "2026-11-02T14:31:22Z",
|
||||
"compose_sha256": "faf73a93c1f7d4d43f1eb61ba2e62a4418bd501f0c827b080ea9897964e75848",
|
||||
"generated_at": "2026-11-02T21:21:54+00:00",
|
||||
"readme_raw_url": "https://raw.githubusercontent.com/klutchell/unbound-docker/main/README.md"
|
||||
},
|
||||
"container_contract": {
|
||||
"service_name": "unbound",
|
||||
"container_name": "unbound",
|
||||
"image": {
|
||||
"reference": "klutchell/unbound:main",
|
||||
"registry": "docker.io",
|
||||
"repository": "klutchell/unbound",
|
||||
"tag": "main",
|
||||
"digest": null,
|
||||
"pull_policy": "resolve-selected-tag-to-architecture-digest-at-install"
|
||||
},
|
||||
"environment": [],
|
||||
"volumes": [
|
||||
{
|
||||
"id": "volume-0",
|
||||
"container_path": "/etc/unbound/custom.conf.d",
|
||||
"compose_source_example": "./config",
|
||||
"read_only": false,
|
||||
"required": false,
|
||||
"installation_choice": [
|
||||
"managed-volume",
|
||||
"host-bind"
|
||||
],
|
||||
"default": "host-bind",
|
||||
"managed_volume": {
|
||||
"backup": true,
|
||||
"default_size_gb": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
"container_port": 53,
|
||||
"published_example": 53,
|
||||
"protocol": "udp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
},
|
||||
{
|
||||
"container_port": 53,
|
||||
"published_example": 53,
|
||||
"protocol": "tcp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
}
|
||||
],
|
||||
"related_services": [],
|
||||
"restart": "unless-stopped",
|
||||
"stop_grace_period": null,
|
||||
"original_compose": "name: unbound\nservices:\n unbound:\n image: klutchell/unbound:main\n container_name: unbound\n restart: unless-stopped\n ports:\n - 53:53/udp\n - 53:53/tcp\n volumes:\n - ./config:/etc/unbound/custom.conf.d\n"
|
||||
},
|
||||
"compose_stack": {
|
||||
"project_name": "unbound",
|
||||
"deployment_model": "one-native-oci-lxc-per-compose-service",
|
||||
"user_experience": "single-application-install",
|
||||
"main_service": "unbound",
|
||||
"service_count": 1,
|
||||
"services": [
|
||||
{
|
||||
"name": "unbound",
|
||||
"image": "klutchell/unbound:main",
|
||||
"is_main": true,
|
||||
"role": "frontend",
|
||||
"vmid_offset": 0,
|
||||
"depends_on": [],
|
||||
"frontend_network": true,
|
||||
"private_network": false,
|
||||
"compose": {
|
||||
"image": "klutchell/unbound:main",
|
||||
"container_name": "unbound",
|
||||
"restart": "unless-stopped",
|
||||
"ports": [
|
||||
"53:53/udp",
|
||||
"53:53/tcp"
|
||||
],
|
||||
"volumes": [
|
||||
"./config:/etc/unbound/custom.conf.d"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"top_level": {
|
||||
"name": "unbound"
|
||||
},
|
||||
"networking": {
|
||||
"frontend": "selected-proxmox-bridge",
|
||||
"private_required": false,
|
||||
"private_creation": "automatic-create-if-missing",
|
||||
"private_address_allocation": "automatic-static-address-per-service",
|
||||
"service_discovery": "private-addresses-with-compose-service-host-aliases",
|
||||
"dependency_external_access": "disabled-unless-service-publishes-ports",
|
||||
"prompt_user_for_private_network": false
|
||||
},
|
||||
"storage": [
|
||||
{
|
||||
"id": "unbound-volume-0",
|
||||
"service": "unbound",
|
||||
"container_path": "/etc/unbound/custom.conf.d",
|
||||
"mode": "user-selectable",
|
||||
"user_selectable": true,
|
||||
"backup": true,
|
||||
"shared_with_other_lxc": false,
|
||||
"source_path": null,
|
||||
"source_path_prompt": "Host directory containing optional custom Unbound .conf files",
|
||||
"default": "host-bind",
|
||||
"installation_choice": [
|
||||
"managed-volume",
|
||||
"host-bind"
|
||||
]
|
||||
}
|
||||
],
|
||||
"orchestration": {
|
||||
"reserve_vmids_atomically": 1,
|
||||
"start_order": [
|
||||
"unbound"
|
||||
],
|
||||
"stop_order": [
|
||||
"unbound"
|
||||
],
|
||||
"dependency_readiness": "compose-healthcheck-then-port-or-process-fallback",
|
||||
"rollback_on_failure": "remove-new-rootfs-preserve-created-persistent-volumes"
|
||||
},
|
||||
"installer_inputs": {
|
||||
"prompted": [
|
||||
"stack_name",
|
||||
"base_vmid",
|
||||
"rootfs_storage",
|
||||
"persistent_data_destinations",
|
||||
"frontend_bridge",
|
||||
"frontend_ipv4_mode"
|
||||
],
|
||||
"automatic": [
|
||||
"dependent_vmids",
|
||||
"private_bridge",
|
||||
"private_subnet",
|
||||
"private_service_addresses",
|
||||
"compose_service_aliases",
|
||||
"generated_secrets",
|
||||
"dependency_start_and_stop_order"
|
||||
]
|
||||
}
|
||||
},
|
||||
"first_run": {
|
||||
"endpoints": [],
|
||||
"credentials": []
|
||||
},
|
||||
"proxmox": {
|
||||
"runtime": "native-oci-lxc",
|
||||
"technology_status": "proxmox-technology-preview",
|
||||
"defaults": {
|
||||
"unprivileged": true,
|
||||
"ostype": "auto-from-image",
|
||||
"cores": 2,
|
||||
"memory_mb": 512,
|
||||
"swap_mb": 512,
|
||||
"rootfs_size_gb": 8,
|
||||
"rootfs_storage": "local-lvm",
|
||||
"volume_storage": "local-lvm",
|
||||
"template_storage": "local",
|
||||
"bridge": "vmbr0",
|
||||
"ipv4": "dhcp",
|
||||
"firewall": true,
|
||||
"host_managed_network": true,
|
||||
"onboot": false,
|
||||
"features": [
|
||||
"nesting=1"
|
||||
],
|
||||
"shutdown_timeout_seconds": 120
|
||||
},
|
||||
"image_metadata_policy": {
|
||||
"entrypoint": "import-from-oci-image",
|
||||
"cmd": "import-from-oci-image",
|
||||
"environment": "import-image-env-then-apply-compose-overrides",
|
||||
"user": "import-from-oci-image",
|
||||
"working_dir": "import-from-oci-image",
|
||||
"stop_signal": "import-from-oci-image"
|
||||
},
|
||||
"adaptations": [],
|
||||
"installer_profile": {
|
||||
"volume_preparations": [
|
||||
{
|
||||
"container_path": "/etc/unbound/custom.conf.d",
|
||||
"owner_strategy": "mapped-root",
|
||||
"remove_lost_found": true,
|
||||
"only_when_mount_type": "managed-volume"
|
||||
}
|
||||
],
|
||||
"startup_healthcheck": {
|
||||
"type": "running",
|
||||
"timeout_seconds": 90,
|
||||
"stability_seconds": 20
|
||||
}
|
||||
},
|
||||
"catalog": {
|
||||
"replaces_discovered_ids": []
|
||||
}
|
||||
},
|
||||
"compatibility": {
|
||||
"automatic_install_candidate": true,
|
||||
"validated": false,
|
||||
"supported_compose_keys": [
|
||||
"container_name",
|
||||
"image",
|
||||
"ports",
|
||||
"restart",
|
||||
"volumes"
|
||||
],
|
||||
"untranslated_blockers": [],
|
||||
"policy": "This single-image Compose contract maps to one unprivileged Proxmox LXC. The optional custom-config directory can use a Proxmox-managed volume or a host directory; the image defaults work with an empty directory."
|
||||
},
|
||||
"validation": {
|
||||
"schema": "passed-at-generation",
|
||||
"image_resolution": "passed-skopeo-16-layers",
|
||||
"clean_install": "passed-default-install-and-first-start-healthcheck",
|
||||
"advanced_install": "pending",
|
||||
"service_health": "passed-recursion-and-custom-local-data-over-udp-and-tcp",
|
||||
"restart_persistence": "passed-custom-zone-after-pct-reboot",
|
||||
"backup_restore": "pending",
|
||||
"update_preserves_data": "pending-update-check-not-run",
|
||||
"modification": "pending-manager-ui-unresponsive-no-changes-applied",
|
||||
"managed_volume": "passed-config-volume-1g",
|
||||
"host_bind": "pending",
|
||||
"removal": "passed-manager-remove-ct-disks-and-record-cleaned"
|
||||
},
|
||||
"lifecycle": {
|
||||
"update_strategy": "resolve-latest-image-then-apply-reviewed-native-lxc-update",
|
||||
"registry_state": {
|
||||
"resolved_architecture": null,
|
||||
"resolved_digest": null,
|
||||
"image_version_label": null,
|
||||
"image_created": null
|
||||
},
|
||||
"change_detection": "compare-compose-sha256-and-resolved-latest-image-digest",
|
||||
"automatic_unattended_updates": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
{
|
||||
"schema_version": "0.3.0",
|
||||
"kind": "proxmenux.oci-template",
|
||||
"id": "image-zwave-js-ui",
|
||||
"status": "generated-unvalidated",
|
||||
"catalog_ui": {
|
||||
"title": {
|
||||
"en_US": "Z-Wave JS UI"
|
||||
},
|
||||
"tagline": {
|
||||
"en_US": "Manage a Z-Wave network from a web dashboard and WebSocket gateway."
|
||||
},
|
||||
"description": {
|
||||
"en_US": "A self-hosted Z-Wave control panel and MQTT gateway. Its web interface can start without a Z-Wave controller; device control requires a compatible USB or serial controller and network security keys."
|
||||
},
|
||||
"category": "smarthome",
|
||||
"author": "Z-Wave JS",
|
||||
"developer": "Z-Wave JS",
|
||||
"icon": "https://raw.githubusercontent.com/zwave-js/zwave-js-ui/c38e0d2cbbb826bf2bab2db596a3452e59ce0e8e/public/apple-touch-icon-180x180.png",
|
||||
"screenshots": [],
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"launch": {
|
||||
"scheme": "http",
|
||||
"port": 8091,
|
||||
"path": "/"
|
||||
},
|
||||
"website": "https://zwave-js.github.io/zwave-js-ui/",
|
||||
"documentation": "https://zwave-js.github.io/zwave-js-ui/#/getting-started/docker",
|
||||
"repository": "https://github.com/zwave-js/zwave-js-ui",
|
||||
"tips": [
|
||||
"The web UI can start without a Z-Wave controller. To control devices, pass through a compatible USB/serial controller and select its stable /dev/serial/by-id/... path in Settings → Z-Wave → Serial Port.",
|
||||
"Configure and securely back up the S0/S2 network keys in Settings → Z-Wave before pairing security-enabled devices.",
|
||||
"Port 3000 is the Z-Wave JS WebSocket server for integrations such as Home Assistant; it listens only after a Z-Wave client is configured. Upstream documents it as unauthenticated, so expose it only to trusted integrations and networks.",
|
||||
"ProxMenux enables the web login and generates the initial admin password. Configure trusted HTTPS before remote access over an untrusted network."
|
||||
],
|
||||
"mini_changelog": [],
|
||||
"display_version": "11.25.2",
|
||||
"updated_at": "2026-11-02",
|
||||
"category_label": "IoT & Smart Home"
|
||||
},
|
||||
"source": {
|
||||
"provider": "Z-Wave JS",
|
||||
"repository": "https://github.com/zwave-js/zwave-js-ui",
|
||||
"default_branch": "master",
|
||||
"revision": "c38e0d2cbbb826bf2bab2db596a3452e59ce0e8e",
|
||||
"readme_raw_url": "https://raw.githubusercontent.com/zwave-js/zwave-js-ui/c38e0d2cbbb826bf2bab2db596a3452e59ce0e8e/README.md",
|
||||
"image_repository_url": "https://hub.docker.com/r/zwavejs/zwave-js-ui",
|
||||
"compose_sha256": "ed371d9e9805db5d2abd356b54e3ff5fdb677d637857313f6aac3f498f480f87",
|
||||
"generated_at": "2026-11-02T20:10:33+00:00"
|
||||
},
|
||||
"container_contract": {
|
||||
"service_name": "zwave-js-ui",
|
||||
"container_name": "zwave-js-ui",
|
||||
"image": {
|
||||
"reference": "docker.io/zwavejs/zwave-js-ui:latest",
|
||||
"registry": "docker.io",
|
||||
"repository": "zwavejs/zwave-js-ui",
|
||||
"tag": "latest",
|
||||
"digest": null,
|
||||
"pull_policy": "resolve-selected-tag-to-architecture-digest-at-install"
|
||||
},
|
||||
"environment": [
|
||||
{
|
||||
"name": "SESSION_SECRET",
|
||||
"example": "${GENERATED_SESSION_SECRET}",
|
||||
"required": true,
|
||||
"sensitive": true,
|
||||
"source": "ProxMenux-generated; supported by upstream image"
|
||||
},
|
||||
{
|
||||
"name": "DEFAULT_PASSWORD",
|
||||
"example": "${GENERATED_ADMIN_PASSWORD}",
|
||||
"prompt": "Initial Z-Wave JS UI admin password",
|
||||
"required": true,
|
||||
"sensitive": true,
|
||||
"source": "Upstream image; generated by ProxMenux"
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"id": "zwave-config",
|
||||
"container_path": "/usr/src/app/store",
|
||||
"compose_source_example": "zwave-config",
|
||||
"read_only": false,
|
||||
"required": true,
|
||||
"installation_choice": [
|
||||
"managed-volume"
|
||||
],
|
||||
"default": "managed-volume",
|
||||
"managed_volume": {
|
||||
"backup": true,
|
||||
"default_size_gb": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
"container_port": 8091,
|
||||
"published_example": 8091,
|
||||
"protocol": "tcp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
},
|
||||
{
|
||||
"container_port": 3000,
|
||||
"published_example": 3000,
|
||||
"protocol": "tcp",
|
||||
"required": false,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
}
|
||||
],
|
||||
"related_services": [],
|
||||
"restart": "always",
|
||||
"stop_grace_period": null,
|
||||
"original_compose": "version: '3.7'\nservices:\n zwave-js-ui:\n container_name: zwave-js-ui\n image: zwavejs/zwave-js-ui:latest\n restart: always\n tty: true\n stop_signal: SIGINT\n environment:\n SESSION_SECRET: ${GENERATED_SESSION_SECRET}\n DEFAULT_PASSWORD: ${GENERATED_ADMIN_PASSWORD}\n networks:\n - zwave\n devices:\n - /dev/serial/by-id/insert_stick_reference_here:/dev/zwave\n volumes:\n - zwave-config:/usr/src/app/store\n ports:\n - '8091:8091'\n - '3000:3000'\nnetworks:\n zwave:\nvolumes:\n zwave-config:\n name: zwave-config\n"
|
||||
},
|
||||
"first_run": {
|
||||
"endpoints": [
|
||||
{
|
||||
"label": "Z-Wave JS UI",
|
||||
"scheme": "http",
|
||||
"port": 8091,
|
||||
"path": "/",
|
||||
"source": "official-docker-documentation"
|
||||
}
|
||||
],
|
||||
"credentials": [
|
||||
{
|
||||
"label": "Z-Wave JS UI admin login",
|
||||
"type": "configured-or-installer-generated",
|
||||
"username": "admin",
|
||||
"password": null,
|
||||
"password_environment": "DEFAULT_PASSWORD",
|
||||
"change_required": false,
|
||||
"source": "Upstream DEFAULT_PASSWORD, generated by ProxMenux",
|
||||
"retrieval": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"proxmox": {
|
||||
"runtime": "native-oci-lxc",
|
||||
"technology_status": "proxmox-technology-preview",
|
||||
"defaults": {
|
||||
"unprivileged": true,
|
||||
"ostype": "auto-from-image",
|
||||
"cores": 2,
|
||||
"memory_mb": 1024,
|
||||
"swap_mb": 512,
|
||||
"rootfs_size_gb": 8,
|
||||
"rootfs_storage": "local-lvm",
|
||||
"volume_storage": "local-lvm",
|
||||
"template_storage": "local",
|
||||
"bridge": "vmbr0",
|
||||
"ipv4": "dhcp",
|
||||
"firewall": true,
|
||||
"host_managed_network": true,
|
||||
"onboot": true,
|
||||
"features": [
|
||||
"nesting=1"
|
||||
],
|
||||
"shutdown_timeout_seconds": 60
|
||||
},
|
||||
"image_metadata_policy": {
|
||||
"entrypoint": "import-from-oci-image",
|
||||
"cmd": "import-from-oci-image",
|
||||
"environment": "import-image-env-then-apply-compose-overrides",
|
||||
"user": "import-from-oci-image",
|
||||
"working_dir": "import-from-oci-image",
|
||||
"stop_signal": "apply-compose-override"
|
||||
},
|
||||
"adaptations": [
|
||||
{
|
||||
"id": "dedicated-lxc-network",
|
||||
"upstream_behavior": "Docker publishes ports on a Docker host address and attaches the service to its Compose network.",
|
||||
"native_lxc_behavior": "The OCI process listens on the dedicated LXC address; ProxMenux maps the single-service network to the LXC network namespace.",
|
||||
"reason": "A one-service native OCI LXC has its own network namespace and address.",
|
||||
"behavioral_impact": "Use the LXC address directly; no Docker port NAT or separate bridge is created.",
|
||||
"validation": "pending-per-application"
|
||||
},
|
||||
{
|
||||
"id": "generated-login-and-session-secret",
|
||||
"upstream_behavior": "The image defaults to user admin and password zwave; web authentication is disabled by default, and the Compose example uses a sample session secret.",
|
||||
"native_lxc_behavior": "ProxMenux generates a unique initial admin password and session secret, and seeds gateway.authEnabled=true before first boot.",
|
||||
"reason": "Avoid shipping a shared password, sample session key or unauthenticated web UI.",
|
||||
"behavioral_impact": "The installer must display the generated admin password; change it after first login.",
|
||||
"validation": "pending-per-application"
|
||||
},
|
||||
{
|
||||
"id": "stop-signal",
|
||||
"upstream_behavior": "Compose requests SIGINT when stopping the container.",
|
||||
"native_lxc_behavior": "ProxMenux uses the same signal for the OCI process shutdown lifecycle.",
|
||||
"reason": "Preserve the upstream graceful-shutdown behavior.",
|
||||
"behavioral_impact": "None expected.",
|
||||
"validation": "pending-per-application"
|
||||
}
|
||||
],
|
||||
"installer_profile": {
|
||||
"generated_sensitive_environment": {
|
||||
"SESSION_SECRET": {
|
||||
"strategy": "token-hex",
|
||||
"bytes": 32,
|
||||
"prompt": false
|
||||
},
|
||||
"DEFAULT_PASSWORD": {
|
||||
"strategy": "token-hex",
|
||||
"bytes": 20,
|
||||
"prompt": true
|
||||
}
|
||||
},
|
||||
"generated_files": [
|
||||
{
|
||||
"container_path": "/usr/src/app/store/settings.json",
|
||||
"mode": "0600",
|
||||
"owner": "mapped-root",
|
||||
"content": "{\"gateway\":{\"authEnabled\":true}}\n",
|
||||
"only_if_missing": true
|
||||
}
|
||||
],
|
||||
"volume_preparations": [
|
||||
{
|
||||
"container_path": "/usr/src/app/store",
|
||||
"remove_lost_found": true,
|
||||
"owner_strategy": "mapped-root",
|
||||
"only_when_mount_type": "managed-volume"
|
||||
}
|
||||
],
|
||||
"optional_devices": [
|
||||
{
|
||||
"id": "zwave-serial-controller",
|
||||
"kind": "character-device",
|
||||
"purpose": "serial",
|
||||
"enable_prompt": "Pass through a Z-Wave USB or serial controller",
|
||||
"enabled_default": false,
|
||||
"required_by_compose": false,
|
||||
"path_prompt": "Controller path on the Proxmox host; prefer /dev/serial/by-id/...",
|
||||
"host_path_default": "/dev/serial/by-id/insert_stick_reference_here",
|
||||
"container_path_strategy": "same-as-host",
|
||||
"mode": "preserve-host",
|
||||
"deny_write": false,
|
||||
"gid_strategy": "host-device-gid"
|
||||
}
|
||||
],
|
||||
"startup_healthcheck": {
|
||||
"scheme": "http",
|
||||
"port": 8091,
|
||||
"path": "/",
|
||||
"timeout_seconds": 180,
|
||||
"request_timeout_seconds": 10,
|
||||
"stability_seconds": 4,
|
||||
"verify_tls": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"compatibility": {
|
||||
"automatic_install_candidate": true,
|
||||
"validated": false,
|
||||
"supported_compose_keys": [
|
||||
"container_name",
|
||||
"devices",
|
||||
"environment",
|
||||
"image",
|
||||
"networks",
|
||||
"ports",
|
||||
"restart",
|
||||
"stop_signal",
|
||||
"tty",
|
||||
"volumes"
|
||||
],
|
||||
"untranslated_blockers": [],
|
||||
"policy": "The single-image recipe has passed installation, UI/login, restart, backup and recovery, modification, and engine checks. Host-bind storage has not been checked. Radio control remains untested because no compatible Z-Wave coordinator was attached during the lab test."
|
||||
},
|
||||
"validation": {
|
||||
"schema": "passed-static-validation",
|
||||
"image_resolution": "passed-skopeo-download-and-layer-verification",
|
||||
"clean_install": "passed-default-install-and-health-check",
|
||||
"advanced_install": "pending-custom-install-summary-validation",
|
||||
"web_response": "passed-ui-and-generated-login",
|
||||
"credentials": "passed-generated-login-before-and-after-restore",
|
||||
"restart_persistence": "passed-restart-with-store-preserved",
|
||||
"backup_restore": "passed-proxmox-backup-and-manager-recover-new-vmid",
|
||||
"update_preserves_data": "no-update-available-at-check",
|
||||
"modification": "passed-cpu-ram-up-down-without-recreation",
|
||||
"managed_volume": "passed-store-volume-and-recovery",
|
||||
"host_bind": "pending",
|
||||
"hardware_radio": "not-tested-no-z-wave-coordinator-on-host",
|
||||
"removal": "passed-remove-original-and-restored-ct-and-local-backup-cleanup"
|
||||
},
|
||||
"lifecycle": {
|
||||
"update_strategy": "replace-rootfs-from-new-oci-image-preserve-managed-volumes",
|
||||
"registry_state": {
|
||||
"resolved_architecture": null,
|
||||
"resolved_digest": null,
|
||||
"image_version_label": "11.25.2",
|
||||
"image_created": null
|
||||
},
|
||||
"change_detection": "compare-resolved-architecture-digest",
|
||||
"automatic_unattended_updates": false
|
||||
}
|
||||
}
|
||||
@@ -432,6 +432,10 @@
|
||||
"metabase": "monitoring",
|
||||
"kopia": "backup",
|
||||
"restic-rest-server": "backup",
|
||||
"netalertx": "monitoring"
|
||||
"netalertx": "monitoring",
|
||||
"kitchenowl": "productivity",
|
||||
"unbound": "adblock",
|
||||
"spoolman": "tools",
|
||||
"zwave-js-ui": "smarthome"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
{
|
||||
"schema_version": "0.5.0",
|
||||
"kind": "proxmenux.oci-template",
|
||||
"id": "image-kitchenowl",
|
||||
"status": "generated-unvalidated",
|
||||
"catalog_ui": {
|
||||
"title": {
|
||||
"en_US": "KitchenOwl"
|
||||
},
|
||||
"tagline": {
|
||||
"en_US": "Self-hosted grocery list and recipe manager"
|
||||
},
|
||||
"description": {
|
||||
"en_US": "Manage shared shopping lists, recipes and meal plans in one self-hosted app."
|
||||
},
|
||||
"category": "productivity",
|
||||
"category_label": "Productivity & Workflows",
|
||||
"author": "Tom Bursch",
|
||||
"developer": "Tom Bursch",
|
||||
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/kitchenowl.webp",
|
||||
"thumbnail": null,
|
||||
"screenshots": [],
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"launch": {
|
||||
"scheme": "http",
|
||||
"port": 80,
|
||||
"path": "/"
|
||||
},
|
||||
"website": "https://kitchenowl.org/",
|
||||
"documentation": "https://docs.kitchenowl.org/self-hosting/",
|
||||
"repository": "https://github.com/TomBursch/kitchenowl",
|
||||
"tips": [
|
||||
"This recipe follows KitchenOwl’s standard official Compose deployment: a web frontend and backend in separate OCI LXCs.",
|
||||
"The upstream sample uses PLEASE_CHANGE_ME for JWT_SECRET_KEY; this recipe generates a unique signing key at install time."
|
||||
],
|
||||
"mini_changelog": [],
|
||||
"display_version": null,
|
||||
"updated_at": "2026-11-01"
|
||||
},
|
||||
"source": {
|
||||
"provider": "tombursch",
|
||||
"repository": "https://github.com/TomBursch/kitchenowl",
|
||||
"revision": "60a012ea5cae829878f78a46a585a7ee530b069a",
|
||||
"image_repository_url": "https://hub.docker.com/r/tombursch/kitchenowl-web",
|
||||
"readme_pushed_at": "2026-11-01T23:22:22Z",
|
||||
"compose_sha256": "f620124c2a4943d4f2465ee304677bc9ed172a088f0fe43f5db7cbc8f6424263",
|
||||
"generated_at": "2026-11-02T20:13:05+00:00",
|
||||
"default_branch": "main",
|
||||
"readme_raw_url": "https://raw.githubusercontent.com/TomBursch/kitchenowl/main/docker-compose.yml"
|
||||
},
|
||||
"container_contract": {
|
||||
"service_name": "front",
|
||||
"container_name": "front",
|
||||
"image": {
|
||||
"reference": "tombursch/kitchenowl-web:latest",
|
||||
"registry": "docker.io",
|
||||
"repository": "tombursch/kitchenowl-web",
|
||||
"tag": "latest",
|
||||
"digest": null,
|
||||
"pull_policy": "resolve-written-reference-to-architecture-digest-at-install"
|
||||
},
|
||||
"environment": [],
|
||||
"volumes": [],
|
||||
"ports": [
|
||||
{
|
||||
"container_port": 80,
|
||||
"published_example": 80,
|
||||
"protocol": "tcp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
}
|
||||
],
|
||||
"related_services": [
|
||||
{
|
||||
"name": "back",
|
||||
"image": "tombursch/kitchenowl-backend:latest"
|
||||
}
|
||||
],
|
||||
"restart": "unless-stopped",
|
||||
"stop_grace_period": null,
|
||||
"original_compose": "services:\n back:\n environment:\n JWT_SECRET_KEY: ${GENERATED_JWT_SECRET_KEY}\n image: tombursch/kitchenowl-backend:latest\n restart: unless-stopped\n volumes:\n - read_only: false\n source: kitchenowl_data\n target: /data\n front:\n depends_on:\n - back\n image: tombursch/kitchenowl-web:latest\n ports:\n - 80:80\n restart: unless-stopped\n volumes: []\nvolumes:\n kitchenowl_data: null\n"
|
||||
},
|
||||
"compose_stack": {
|
||||
"project_name": "kitchenowl",
|
||||
"deployment_model": "one-native-oci-lxc-per-compose-service",
|
||||
"user_experience": "single-application-install",
|
||||
"main_service": "front",
|
||||
"service_count": 2,
|
||||
"services": [
|
||||
{
|
||||
"name": "back",
|
||||
"image": "tombursch/kitchenowl-backend:latest",
|
||||
"is_main": false,
|
||||
"role": "dependency",
|
||||
"vmid_offset": 1,
|
||||
"depends_on": [],
|
||||
"frontend_network": false,
|
||||
"private_network": true,
|
||||
"compose": {
|
||||
"environment": {
|
||||
"JWT_SECRET_KEY": "${GENERATED_JWT_SECRET_KEY}"
|
||||
},
|
||||
"image": "tombursch/kitchenowl-backend:latest",
|
||||
"restart": "unless-stopped",
|
||||
"volumes": [
|
||||
{
|
||||
"source": "kitchenowl_data",
|
||||
"target": "/data",
|
||||
"read_only": false
|
||||
}
|
||||
],
|
||||
"healthcheck": {
|
||||
"test": [
|
||||
"CMD-SHELL",
|
||||
"uwsgi_curl localhost:5000 /api/health/8M4F88S8ooi4sMbLBfkkV7ctWwgibW6V || exit 1"
|
||||
],
|
||||
"interval": "60s",
|
||||
"timeout": "3s",
|
||||
"retries": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "front",
|
||||
"image": "tombursch/kitchenowl-web:latest",
|
||||
"is_main": true,
|
||||
"role": "frontend",
|
||||
"vmid_offset": 0,
|
||||
"depends_on": [
|
||||
"back"
|
||||
],
|
||||
"frontend_network": true,
|
||||
"private_network": true,
|
||||
"compose": {
|
||||
"depends_on": [
|
||||
"back"
|
||||
],
|
||||
"image": "tombursch/kitchenowl-web:latest",
|
||||
"ports": [
|
||||
"80:80"
|
||||
],
|
||||
"restart": "unless-stopped",
|
||||
"volumes": [],
|
||||
"healthcheck": {
|
||||
"test": [
|
||||
"CMD-SHELL",
|
||||
"curl -f http://localhost/ || exit 1"
|
||||
],
|
||||
"interval": "30s",
|
||||
"timeout": "3s",
|
||||
"retries": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"top_level": {
|
||||
"volumes": {
|
||||
"kitchenowl_data": null
|
||||
}
|
||||
},
|
||||
"networking": {
|
||||
"frontend": "selected-proxmox-bridge",
|
||||
"private_required": true,
|
||||
"private_creation": "automatic-create-if-missing",
|
||||
"private_address_allocation": "automatic-static-address-per-service",
|
||||
"service_discovery": "private-addresses-with-compose-service-host-aliases",
|
||||
"dependency_external_access": "disabled-unless-service-publishes-ports",
|
||||
"prompt_user_for_private_network": false
|
||||
},
|
||||
"storage": [
|
||||
{
|
||||
"id": "back-volume-0",
|
||||
"service": "back",
|
||||
"container_path": "/data",
|
||||
"mode": "managed-volume",
|
||||
"user_selectable": true,
|
||||
"backup": true,
|
||||
"shared_with_other_lxc": false,
|
||||
"source_path": null,
|
||||
"source_path_prompt": null
|
||||
}
|
||||
],
|
||||
"orchestration": {
|
||||
"reserve_vmids_atomically": 2,
|
||||
"start_order": [
|
||||
"back",
|
||||
"front"
|
||||
],
|
||||
"stop_order": [
|
||||
"front",
|
||||
"back"
|
||||
],
|
||||
"dependency_readiness": "compose-healthcheck-then-port-or-process-fallback",
|
||||
"rollback_on_failure": "remove-new-rootfs-preserve-created-persistent-volumes"
|
||||
},
|
||||
"installer_inputs": {
|
||||
"prompted": [
|
||||
"stack_name",
|
||||
"base_vmid",
|
||||
"rootfs_storage",
|
||||
"persistent_data_destinations",
|
||||
"frontend_bridge",
|
||||
"frontend_ipv4_mode"
|
||||
],
|
||||
"automatic": [
|
||||
"dependent_vmids",
|
||||
"private_bridge",
|
||||
"private_subnet",
|
||||
"private_service_addresses",
|
||||
"compose_service_aliases",
|
||||
"generated_secrets",
|
||||
"dependency_start_and_stop_order"
|
||||
],
|
||||
"generated_secrets": [
|
||||
{
|
||||
"id": "generated-jwt-secret-key",
|
||||
"strategy": "generate-cryptographically-random-at-install",
|
||||
"bindings": [
|
||||
{
|
||||
"service": "back",
|
||||
"environment_variable": "JWT_SECRET_KEY"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"first_run": {
|
||||
"endpoints": [
|
||||
{
|
||||
"label": "KitchenOwl",
|
||||
"scheme": "http",
|
||||
"port": 80,
|
||||
"path": "/",
|
||||
"source": "upstream-documentation"
|
||||
}
|
||||
],
|
||||
"credentials": []
|
||||
},
|
||||
"proxmox": {
|
||||
"runtime": "native-oci-lxc",
|
||||
"technology_status": "proxmox-technology-preview",
|
||||
"defaults": {
|
||||
"unprivileged": true,
|
||||
"ostype": "auto-from-image",
|
||||
"cores": 2,
|
||||
"memory_mb": 1024,
|
||||
"swap_mb": 512,
|
||||
"rootfs_size_gb": 8,
|
||||
"rootfs_storage": "local-lvm",
|
||||
"volume_storage": "local-lvm",
|
||||
"template_storage": "local",
|
||||
"bridge": "vmbr0",
|
||||
"ipv4": "dhcp",
|
||||
"firewall": true,
|
||||
"host_managed_network": true,
|
||||
"onboot": false,
|
||||
"features": [
|
||||
"nesting=1"
|
||||
],
|
||||
"shutdown_timeout_seconds": 30
|
||||
},
|
||||
"image_metadata_policy": {
|
||||
"entrypoint": "import-from-oci-image",
|
||||
"cmd": "import-from-oci-image",
|
||||
"environment": "import-image-env-then-apply-compose-overrides",
|
||||
"user": "import-from-oci-image",
|
||||
"working_dir": "import-from-oci-image",
|
||||
"stop_signal": "import-from-oci-image"
|
||||
},
|
||||
"adaptations": [
|
||||
{
|
||||
"id": "generated-jwt-signing-key",
|
||||
"upstream_behavior": "The official Compose example uses the placeholder PLEASE_CHANGE_ME for JWT_SECRET_KEY and instructs operators to replace it.",
|
||||
"native_lxc_behavior": "ProxMenux generates a unique cryptographically random JWT signing key and supplies it to the backend service.",
|
||||
"reason": "Avoid deploying the documented placeholder or placing a signing secret in the public recipe.",
|
||||
"behavioral_impact": "The generated key is not an app login credential; KitchenOwl has no catalog-provided default account.",
|
||||
"validation": "pending-runtime"
|
||||
},
|
||||
{
|
||||
"id": "image-healthchecks-preserved",
|
||||
"upstream_behavior": "The web and backend OCI images declare Docker healthchecks in image metadata.",
|
||||
"native_lxc_behavior": "The current image healthchecks are recorded in the stack so ProxMenux can check backend readiness and frontend availability during first start.",
|
||||
"reason": "The Compose file inherits these checks from its images instead of declaring them itself.",
|
||||
"behavioral_impact": "Checks gate stack startup; Docker does not run as a separate health-monitoring daemon inside the LXCs.",
|
||||
"validation": "pending-runtime"
|
||||
}
|
||||
],
|
||||
"custom_compose_stack": true,
|
||||
"custom_compose_interpolated": false,
|
||||
"stack_image_entrypoints": {},
|
||||
"installer_profile": {
|
||||
"stack_driver": "generic-multi-lxc-stack",
|
||||
"volume_preparations": [
|
||||
{
|
||||
"container_path": "/data",
|
||||
"remove_lost_found": true,
|
||||
"owner_strategy": "mapped-application-user",
|
||||
"only_when_mount_type": "managed-volume"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stack_completion_notes": [
|
||||
"Open the web address and follow KitchenOwl’s onboarding to create the first account; no default account is configured."
|
||||
]
|
||||
},
|
||||
"compatibility": {
|
||||
"automatic_install_candidate": true,
|
||||
"validated": false,
|
||||
"supported_compose_keys": [
|
||||
"cap_add",
|
||||
"command",
|
||||
"container_name",
|
||||
"cpu_shares",
|
||||
"deploy",
|
||||
"devices",
|
||||
"entrypoint",
|
||||
"environment",
|
||||
"extra_hosts",
|
||||
"group_add",
|
||||
"healthcheck",
|
||||
"hostname",
|
||||
"image",
|
||||
"init",
|
||||
"ipc",
|
||||
"labels",
|
||||
"logging",
|
||||
"mac_address",
|
||||
"mem_limit",
|
||||
"network_mode",
|
||||
"networks",
|
||||
"ports",
|
||||
"privileged",
|
||||
"restart",
|
||||
"runtime",
|
||||
"security_opt",
|
||||
"shm_size",
|
||||
"stdin_open",
|
||||
"stop_grace_period",
|
||||
"stop_signal",
|
||||
"sysctls",
|
||||
"tty",
|
||||
"ulimits",
|
||||
"user",
|
||||
"volumes",
|
||||
"working_dir"
|
||||
],
|
||||
"untranslated_blockers": [],
|
||||
"policy": "The official two-image Compose stack is mapped to native Proxmox OCI LXCs with private service networking. Lab checks confirmed installation, web/API behavior, persistence, backup and recovery, modification, managed storage, and host-bind storage. Advanced custom settings have not been validated."
|
||||
},
|
||||
"validation": {
|
||||
"schema": "passed-at-generation",
|
||||
"clean_install": "passed-default-official-two-service-stack-and-first-start",
|
||||
"advanced_install": "pending-custom-install-summary-validation",
|
||||
"service_health": "passed-web-api-and-created-user-household-list-item",
|
||||
"restart_persistence": "passed-managed-and-host-bind-data",
|
||||
"backup_restore": "passed-manager-recover-new-vmids-stack-network-and-data",
|
||||
"update_preserves_data": "no-update-available-at-check",
|
||||
"modification": "passed-added-managed-volume-without-image-pull-or-container-recreate",
|
||||
"volumes": "passed-managed-storage-and-host-bind-with-owner-and-data-check",
|
||||
"removal": "passed-manager-removal-temporary-data-cleaned-after-retention-check"
|
||||
},
|
||||
"lifecycle": {
|
||||
"update_strategy": "resolve-latest-image-then-apply-reviewed-native-lxc-update",
|
||||
"registry_state": {
|
||||
"resolved_architecture": null,
|
||||
"resolved_digest": null,
|
||||
"image_version_label": null,
|
||||
"image_created": null
|
||||
},
|
||||
"change_detection": "compare-compose-sha256-and-resolved-latest-image-digest",
|
||||
"automatic_unattended_updates": false,
|
||||
"dependency_lifecycle": {
|
||||
"implementation": "proxmox-hookscript",
|
||||
"trigger": "main-lxc-pre-start",
|
||||
"waits_for_dependency_healthchecks": true,
|
||||
"stops_dependencies_with_main": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
{
|
||||
"schema_version": "0.5.0",
|
||||
"kind": "proxmenux.oci-template",
|
||||
"id": "image-krusader",
|
||||
"status": "generated-unvalidated",
|
||||
"catalog_ui": {
|
||||
"title": {
|
||||
"en_US": "Krusader",
|
||||
"es_ES": "Krusader"
|
||||
},
|
||||
"tagline": {
|
||||
"en_US": "KDE's twin-panel file manager in a browser-based desktop.",
|
||||
"es_ES": "El gestor de archivos de dos paneles de KDE, disponible desde el navegador."
|
||||
},
|
||||
"description": {
|
||||
"en_US": "Open Krusader in a browser and manage files from a directory you explicitly mount at /storage. The maintained jlesage image provides the graphical desktop; ProxMenux runs it as one native OCI LXC.",
|
||||
"es_ES": "Abre Krusader en el navegador y gestiona los archivos de una carpeta que montes expresamente en /storage. La imagen mantenida por jlesage incluye el escritorio gráfico; ProxMenux la ejecuta como un LXC OCI nativo."
|
||||
},
|
||||
"category": "desktop",
|
||||
"category_label": "Browsers & Web Desktops",
|
||||
"author": "jlesage",
|
||||
"developer": "jlesage",
|
||||
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/krusader.webp",
|
||||
"thumbnail": null,
|
||||
"screenshots": [],
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"launch": {
|
||||
"scheme": "https",
|
||||
"port": 5800,
|
||||
"path": "/"
|
||||
},
|
||||
"website": "https://krusader.org/",
|
||||
"documentation": "https://github.com/jlesage/docker-krusader",
|
||||
"repository": "https://github.com/jlesage/docker-krusader",
|
||||
"tips": [
|
||||
"The web login uses HTTPS with an automatically generated self-signed certificate and password. Accept the certificate warning on the first visit or configure a trusted certificate.",
|
||||
"Krusader can read, change and delete files in the directory mounted at /storage. Mount only the directory you intend to manage; the rest of the Proxmox host is not exposed.",
|
||||
"The VNC port is not published. Use the browser interface on port 5800."
|
||||
],
|
||||
"mini_changelog": [],
|
||||
"display_version": null,
|
||||
"updated_at": null
|
||||
},
|
||||
"source": {
|
||||
"provider": "jlesage",
|
||||
"repository": "https://github.com/jlesage/docker-krusader",
|
||||
"revision": "855c8140e1bb65b2a2526d82db44c808f23eb49f",
|
||||
"image_repository_url": "https://hub.docker.com/r/jlesage/krusader",
|
||||
"readme_pushed_at": "2026-09-22T15:18:17Z",
|
||||
"compose_sha256": "9dd16cc30aa19afe7e148598c9cfb0c65c31606d67f3f22254c33a4d05212c7f",
|
||||
"generated_at": "2026-11-02T20:05:40+00:00",
|
||||
"default_branch": "master",
|
||||
"readme_raw_url": "https://raw.githubusercontent.com/jlesage/docker-krusader/855c8140e1bb65b2a2526d82db44c808f23eb49f/README.md"
|
||||
},
|
||||
"container_contract": {
|
||||
"service_name": "krusader",
|
||||
"container_name": "krusader",
|
||||
"image": {
|
||||
"reference": "jlesage/krusader:latest",
|
||||
"registry": "docker.io",
|
||||
"repository": "jlesage/krusader",
|
||||
"tag": "latest",
|
||||
"digest": null,
|
||||
"pull_policy": "resolve-written-reference-to-architecture-digest-at-install"
|
||||
},
|
||||
"environment": [
|
||||
{
|
||||
"name": "USER_ID",
|
||||
"example": "1000",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "upstream-image-default"
|
||||
},
|
||||
{
|
||||
"name": "GROUP_ID",
|
||||
"example": "1000",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "upstream-image-default"
|
||||
},
|
||||
{
|
||||
"name": "TZ",
|
||||
"example": "Europe/Madrid",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "ProxMenux-local-timezone"
|
||||
},
|
||||
{
|
||||
"name": "VNC_LOCALHOST_ONLY",
|
||||
"example": "1",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "upstream-image-security-setting"
|
||||
},
|
||||
{
|
||||
"name": "WEB_AUTHENTICATION",
|
||||
"example": "1",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "upstream-image-security-setting"
|
||||
},
|
||||
{
|
||||
"name": "WEB_AUTHENTICATION_USERNAME",
|
||||
"example": "admin",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "upstream-image-security-setting"
|
||||
},
|
||||
{
|
||||
"name": "WEB_AUTHENTICATION_PASSWORD",
|
||||
"example": "${GENERATED_WEB_PASSWORD}",
|
||||
"required": true,
|
||||
"sensitive": true,
|
||||
"source": "ProxMenux-generated-credential"
|
||||
},
|
||||
{
|
||||
"name": "SECURE_CONNECTION",
|
||||
"example": "1",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "upstream-image-security-setting"
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"id": "krusader-config",
|
||||
"container_path": "/config",
|
||||
"compose_source_example": "krusader-config",
|
||||
"read_only": false,
|
||||
"required": true,
|
||||
"installation_choice": [
|
||||
"managed-volume"
|
||||
],
|
||||
"default": "managed-volume",
|
||||
"managed_volume": {
|
||||
"backup": true,
|
||||
"default_size_gb": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "krusader-storage",
|
||||
"container_path": "/storage",
|
||||
"compose_source_example": "/mnt/oci-shared/krusader/storage",
|
||||
"read_only": false,
|
||||
"required": true,
|
||||
"installation_choice": [
|
||||
"managed-volume",
|
||||
"host-bind"
|
||||
],
|
||||
"default": "host-bind",
|
||||
"managed_volume": {
|
||||
"backup": true,
|
||||
"default_size_gb": 8
|
||||
}
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
"container_port": 5800,
|
||||
"published_example": 5800,
|
||||
"protocol": "tcp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
}
|
||||
],
|
||||
"related_services": [],
|
||||
"restart": "unless-stopped",
|
||||
"stop_grace_period": null,
|
||||
"original_compose": "name: krusader\nservices:\n krusader:\n image: jlesage/krusader:latest\n container_name: krusader\n restart: unless-stopped\n environment:\n USER_ID: '1000'\n GROUP_ID: '1000'\n TZ: Europe/Madrid\n VNC_LOCALHOST_ONLY: '1'\n WEB_AUTHENTICATION: '1'\n WEB_AUTHENTICATION_USERNAME: admin\n WEB_AUTHENTICATION_PASSWORD: ${GENERATED_WEB_PASSWORD}\n SECURE_CONNECTION: '1'\n ports:\n - 5800:5800/tcp\n volumes:\n - krusader-config:/config\n - /mnt/oci-shared/krusader/storage:/storage\nvolumes:\n krusader-config: {}\n"
|
||||
},
|
||||
"compose_stack": {
|
||||
"project_name": "krusader",
|
||||
"deployment_model": "one-native-oci-lxc-per-compose-service",
|
||||
"user_experience": "single-application-install",
|
||||
"main_service": "krusader",
|
||||
"service_count": 1,
|
||||
"services": [
|
||||
{
|
||||
"name": "krusader",
|
||||
"image": "jlesage/krusader:latest",
|
||||
"is_main": true,
|
||||
"role": "frontend",
|
||||
"vmid_offset": 0,
|
||||
"depends_on": [],
|
||||
"frontend_network": true,
|
||||
"private_network": false,
|
||||
"compose": {
|
||||
"environment": {
|
||||
"GROUP_ID": "1000",
|
||||
"SECURE_CONNECTION": "1",
|
||||
"TZ": "Europe/Madrid",
|
||||
"USER_ID": "1000",
|
||||
"VNC_LOCALHOST_ONLY": "1",
|
||||
"WEB_AUTHENTICATION": "1",
|
||||
"WEB_AUTHENTICATION_PASSWORD": "${GENERATED_GENERATED_WEB_PASSWORD}",
|
||||
"WEB_AUTHENTICATION_USERNAME": "admin"
|
||||
},
|
||||
"image": "jlesage/krusader:latest",
|
||||
"ports": [
|
||||
"5800:5800"
|
||||
],
|
||||
"volumes": [
|
||||
"./config:/config",
|
||||
"./storage:/storage"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"top_level": {
|
||||
"name": "krusader"
|
||||
},
|
||||
"networking": {
|
||||
"frontend": "selected-proxmox-bridge",
|
||||
"private_required": false,
|
||||
"private_creation": "automatic-create-if-missing",
|
||||
"private_address_allocation": "automatic-static-address-per-service",
|
||||
"service_discovery": "private-addresses-with-compose-service-host-aliases",
|
||||
"dependency_external_access": "disabled-unless-service-publishes-ports",
|
||||
"prompt_user_for_private_network": false
|
||||
},
|
||||
"storage": [
|
||||
{
|
||||
"id": "krusader-volume-0",
|
||||
"service": "krusader",
|
||||
"container_path": "/config",
|
||||
"mode": "managed-volume",
|
||||
"user_selectable": false,
|
||||
"backup": true,
|
||||
"shared_with_other_lxc": false,
|
||||
"source_path": null,
|
||||
"source_path_prompt": null
|
||||
},
|
||||
{
|
||||
"id": "krusader-volume-1",
|
||||
"service": "krusader",
|
||||
"container_path": "/storage",
|
||||
"mode": "user-selectable",
|
||||
"user_selectable": true,
|
||||
"backup": true,
|
||||
"shared_with_other_lxc": false,
|
||||
"source_path": null,
|
||||
"source_path_prompt": "Host directory for Krusader:/storage",
|
||||
"default": "host-bind",
|
||||
"installation_choice": [
|
||||
"managed-volume",
|
||||
"host-bind"
|
||||
]
|
||||
}
|
||||
],
|
||||
"orchestration": {
|
||||
"reserve_vmids_atomically": 1,
|
||||
"start_order": [
|
||||
"krusader"
|
||||
],
|
||||
"stop_order": [
|
||||
"krusader"
|
||||
],
|
||||
"dependency_readiness": "compose-healthcheck-then-port-or-process-fallback",
|
||||
"rollback_on_failure": "remove-new-rootfs-preserve-created-persistent-volumes"
|
||||
},
|
||||
"installer_inputs": {
|
||||
"prompted": [
|
||||
"stack_name",
|
||||
"base_vmid",
|
||||
"rootfs_storage",
|
||||
"persistent_data_destinations",
|
||||
"frontend_bridge",
|
||||
"frontend_ipv4_mode"
|
||||
],
|
||||
"automatic": [
|
||||
"dependent_vmids",
|
||||
"private_bridge",
|
||||
"private_subnet",
|
||||
"private_service_addresses",
|
||||
"compose_service_aliases",
|
||||
"generated_secrets",
|
||||
"dependency_start_and_stop_order"
|
||||
],
|
||||
"generated_secrets": [
|
||||
{
|
||||
"id": "generated-web-password",
|
||||
"strategy": "generate-cryptographically-random-at-install",
|
||||
"bindings": [
|
||||
{
|
||||
"service": "krusader",
|
||||
"environment_variable": "WEB_AUTHENTICATION_PASSWORD"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"first_run": {
|
||||
"endpoints": [
|
||||
{
|
||||
"label": "Krusader Web UI",
|
||||
"scheme": "https",
|
||||
"port": 5800,
|
||||
"path": "/",
|
||||
"source": "upstream-jlesage-secure-connection-and-web-authentication"
|
||||
}
|
||||
],
|
||||
"credentials": [
|
||||
{
|
||||
"label": "Krusader Web UI",
|
||||
"type": "configured-or-installer-generated",
|
||||
"username": "admin",
|
||||
"password": null,
|
||||
"username_environment": "WEB_AUTHENTICATION_USERNAME",
|
||||
"password_environment": "WEB_AUTHENTICATION_PASSWORD",
|
||||
"change_required": false,
|
||||
"source": "ProxMenux generated password and upstream image environment",
|
||||
"retrieval": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"proxmox": {
|
||||
"runtime": "native-oci-lxc",
|
||||
"technology_status": "proxmox-technology-preview",
|
||||
"defaults": {
|
||||
"unprivileged": true,
|
||||
"ostype": "auto-from-image",
|
||||
"cores": 2,
|
||||
"memory_mb": 2048,
|
||||
"swap_mb": 512,
|
||||
"rootfs_size_gb": 8,
|
||||
"rootfs_storage": "local-lvm",
|
||||
"volume_storage": "local-lvm",
|
||||
"template_storage": "local",
|
||||
"bridge": "vmbr0",
|
||||
"ipv4": "dhcp",
|
||||
"firewall": true,
|
||||
"host_managed_network": true,
|
||||
"onboot": false,
|
||||
"features": [
|
||||
"nesting=1"
|
||||
],
|
||||
"shutdown_timeout_seconds": 30
|
||||
},
|
||||
"image_metadata_policy": {
|
||||
"entrypoint": "import-from-oci-image",
|
||||
"cmd": "import-from-oci-image",
|
||||
"environment": "import-image-env-then-apply-compose-overrides",
|
||||
"user": "import-from-oci-image",
|
||||
"working_dir": "import-from-oci-image",
|
||||
"stop_signal": "import-from-oci-image"
|
||||
},
|
||||
"adaptations": [
|
||||
{
|
||||
"id": "web-authentication-https",
|
||||
"upstream_behavior": "The jlesage image can protect its browser GUI with WEB_AUTHENTICATION and requires SECURE_CONNECTION for authenticated web access.",
|
||||
"native_lxc_behavior": "Enable authenticated HTTPS on port 5800, generate a unique password at install and keep VNC bound to localhost.",
|
||||
"reason": "Krusader can modify files exposed under /storage, so the browser endpoint must not be unauthenticated.",
|
||||
"behavioral_impact": "The image generates a self-signed TLS certificate; a browser may ask to trust it on first access.",
|
||||
"validation": "pending"
|
||||
},
|
||||
{
|
||||
"id": "scoped-storage-mount",
|
||||
"upstream_behavior": "Docker Compose mounts the selected host directory at /storage.",
|
||||
"native_lxc_behavior": "The same selected directory is mounted in the dedicated OCI LXC; no other host paths are exposed.",
|
||||
"reason": "The file manager must only see data explicitly selected by the user.",
|
||||
"behavioral_impact": "Write access permits changing or deleting files in that one directory.",
|
||||
"validation": "pending"
|
||||
},
|
||||
{
|
||||
"id": "dedicated-lxc-network",
|
||||
"upstream_behavior": "Docker publishes web port 5800 on the Docker host.",
|
||||
"native_lxc_behavior": "The native LXC listens on its own IP at port 5800 without Docker NAT.",
|
||||
"reason": "The LXC has a dedicated network namespace and address.",
|
||||
"behavioral_impact": "The port is reached at the LXC address rather than through a host-published port.",
|
||||
"validation": "pending"
|
||||
}
|
||||
],
|
||||
"installer_profile": {
|
||||
"volume_preparations": [
|
||||
{
|
||||
"container_path": "/config",
|
||||
"remove_lost_found": true,
|
||||
"owner_strategy": "mapped-application-user",
|
||||
"uid": 1000,
|
||||
"gid": 1000,
|
||||
"only_when_mount_type": "managed-volume"
|
||||
},
|
||||
{
|
||||
"container_path": "/storage",
|
||||
"remove_lost_found": true,
|
||||
"owner_strategy": "mapped-application-user",
|
||||
"uid": 1000,
|
||||
"gid": 1000,
|
||||
"only_when_mount_type": "managed-volume"
|
||||
}
|
||||
],
|
||||
"startup_healthcheck": {
|
||||
"scheme": "https",
|
||||
"port": 5800,
|
||||
"path": "/",
|
||||
"timeout_seconds": 240,
|
||||
"request_timeout_seconds": 10,
|
||||
"stability_seconds": 4,
|
||||
"verify_tls": false
|
||||
},
|
||||
"generated_sensitive_environment": {
|
||||
"WEB_AUTHENTICATION_PASSWORD": {
|
||||
"strategy": "token-hex",
|
||||
"bytes": 16,
|
||||
"prompt": false
|
||||
}
|
||||
},
|
||||
"volume_owner": {
|
||||
"uid": 1000,
|
||||
"gid": 1000
|
||||
}
|
||||
}
|
||||
},
|
||||
"compatibility": {
|
||||
"automatic_install_candidate": true,
|
||||
"validated": false,
|
||||
"supported_compose_keys": [
|
||||
"cap_add",
|
||||
"command",
|
||||
"container_name",
|
||||
"cpu_shares",
|
||||
"deploy",
|
||||
"devices",
|
||||
"entrypoint",
|
||||
"environment",
|
||||
"extra_hosts",
|
||||
"group_add",
|
||||
"healthcheck",
|
||||
"hostname",
|
||||
"image",
|
||||
"init",
|
||||
"ipc",
|
||||
"labels",
|
||||
"logging",
|
||||
"mac_address",
|
||||
"mem_limit",
|
||||
"network_mode",
|
||||
"networks",
|
||||
"ports",
|
||||
"privileged",
|
||||
"restart",
|
||||
"runtime",
|
||||
"security_opt",
|
||||
"shm_size",
|
||||
"stdin_open",
|
||||
"stop_grace_period",
|
||||
"stop_signal",
|
||||
"sysctls",
|
||||
"tty",
|
||||
"ulimits",
|
||||
"user",
|
||||
"volumes",
|
||||
"working_dir"
|
||||
],
|
||||
"untranslated_blockers": [],
|
||||
"policy": "Single-image definitions are installable when every declared Compose option has a native Proxmox translation. Multi-image and unsupported runtime features remain blocked until their orchestrator or mapping is available."
|
||||
},
|
||||
"validation": {
|
||||
"schema": "passed-at-generation",
|
||||
"clean_install": "passed-first-start",
|
||||
"service_health": "passed-login-page-and-authenticated-session-http-200",
|
||||
"restart_persistence": "passed-container-restart-and-host-bind-write-check",
|
||||
"backup_restore": "pending",
|
||||
"update_preserves_data": "pending",
|
||||
"image_resolution": "passed-skopeo-327mb-11-layers-integrity",
|
||||
"credentials": "passed-generated-password-login-and-session-cookie",
|
||||
"advanced_install": "pending-custom-install-summary-validation",
|
||||
"managed_volume": "passed-managed-config-volume",
|
||||
"host_bind": "passed-storage-bind-owner-and-app-user-write",
|
||||
"browser_visual": "pending-self-signed-certificate-review",
|
||||
"modification": "pending",
|
||||
"removal": "passed-manager-remove-ct-disks-record-and-empty-test-path-cleaned",
|
||||
"web_authentication": "passed-generated-credential-post-and-session-cookie"
|
||||
},
|
||||
"lifecycle": {
|
||||
"update_strategy": "resolve-latest-image-then-apply-reviewed-native-lxc-update",
|
||||
"registry_state": {
|
||||
"resolved_architecture": "amd64",
|
||||
"resolved_digest": "sha256:ecb77bb73b1d3c66720eefb07805f70289349b449d1ddaa15d120c399bd89cc4",
|
||||
"image_version_label": "26.09.1",
|
||||
"image_created": "2026-09-22T15:22:56.230858151Z"
|
||||
},
|
||||
"change_detection": "compare-compose-sha256-and-resolved-latest-image-digest",
|
||||
"automatic_unattended_updates": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
{
|
||||
"schema_version": "0.5.0",
|
||||
"kind": "proxmenux.oci-template",
|
||||
"id": "image-spoolman",
|
||||
"status": "generated-unvalidated",
|
||||
"catalog_ui": {
|
||||
"title": {
|
||||
"en_US": "Spoolman"
|
||||
},
|
||||
"tagline": {
|
||||
"en_US": "Track 3D printer filament spools and usage."
|
||||
},
|
||||
"description": {
|
||||
"en_US": "A self-hosted filament inventory for 3D printers. Manage spools, materials, manufacturers, locations and labels in the web UI, and integrate with Moonraker, OctoPrint and other printer tools. The official image uses SQLite by default; no separate database container is required."
|
||||
},
|
||||
"category": "tools",
|
||||
"category_label": "Tools",
|
||||
"author": "Donkie",
|
||||
"developer": "Donkie",
|
||||
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/spoolman.webp",
|
||||
"thumbnail": null,
|
||||
"screenshots": [],
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"launch": {
|
||||
"scheme": "http",
|
||||
"port": 8000,
|
||||
"path": "/"
|
||||
},
|
||||
"website": "https://github.com/Donkie/Spoolman",
|
||||
"documentation": "https://github.com/Donkie/Spoolman/wiki/Installation",
|
||||
"repository": "https://github.com/Donkie/Spoolman",
|
||||
"tips": [
|
||||
"Spoolman has no built-in authentication. Keep it on a trusted network; do not expose it directly to the internet.",
|
||||
"The official image uses SQLite by default and stores the database, automatic backups, logs and cache in the persistent data directory."
|
||||
],
|
||||
"mini_changelog": [],
|
||||
"display_version": null,
|
||||
"updated_at": null
|
||||
},
|
||||
"source": {
|
||||
"provider": "Donkie",
|
||||
"repository": "https://github.com/Donkie/Spoolman",
|
||||
"revision": "ff8efd517b76a4b2783f29c89e9b9fbcd8cd167b",
|
||||
"image_repository_url": "https://github.com/Donkie/Spoolman/pkgs/container/spoolman",
|
||||
"readme_pushed_at": "2026-10-31T09:39:19Z",
|
||||
"compose_sha256": "d9b115c4c37589f9813e54f926fdd22e7a10a44a6e56160f837b1247638fac83",
|
||||
"generated_at": "2026-11-02T20:02:46+00:00",
|
||||
"default_branch": "master",
|
||||
"readme_raw_url": "https://github.com/Donkie/Spoolman/wiki/Installation"
|
||||
},
|
||||
"container_contract": {
|
||||
"service_name": "spoolman",
|
||||
"container_name": "spoolman",
|
||||
"image": {
|
||||
"reference": "ghcr.io/donkie/spoolman:latest",
|
||||
"registry": "ghcr.io",
|
||||
"repository": "ghcr.io/donkie/spoolman",
|
||||
"tag": "latest",
|
||||
"digest": null,
|
||||
"pull_policy": "resolve-written-reference-to-architecture-digest-at-install"
|
||||
},
|
||||
"environment": [
|
||||
{
|
||||
"name": "PUID",
|
||||
"example": "1000",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "upstream-image-default",
|
||||
"prompt_user": false
|
||||
},
|
||||
{
|
||||
"name": "PGID",
|
||||
"example": "1000",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "upstream-image-default",
|
||||
"prompt_user": false
|
||||
},
|
||||
{
|
||||
"name": "TZ",
|
||||
"example": "Etc/UTC",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "upstream-compose",
|
||||
"prompt_user": false
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"id": "volume-0",
|
||||
"container_path": "/home/app/.local/share/spoolman",
|
||||
"compose_source_example": "./data",
|
||||
"read_only": false,
|
||||
"required": true,
|
||||
"installation_choice": [
|
||||
"managed-volume",
|
||||
"host-bind"
|
||||
],
|
||||
"default": "managed-volume",
|
||||
"managed_volume": {
|
||||
"backup": true,
|
||||
"default_size_gb": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
"container_port": 8000,
|
||||
"published_example": 7912,
|
||||
"protocol": "tcp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
}
|
||||
],
|
||||
"related_services": [],
|
||||
"restart": "unless-stopped",
|
||||
"stop_grace_period": null,
|
||||
"original_compose": "services:\n spoolman:\n image: ghcr.io/donkie/spoolman:latest\n container_name: spoolman\n restart: unless-stopped\n ports:\n - \"7912:8000/tcp\"\n volumes:\n - ./data:/home/app/.local/share/spoolman\n environment:\n PUID: \"1000\"\n PGID: \"1000\"\n TZ: Etc/UTC\n"
|
||||
},
|
||||
"compose_stack": {
|
||||
"project_name": "spoolman",
|
||||
"deployment_model": "one-native-oci-lxc-per-compose-service",
|
||||
"user_experience": "single-application-install",
|
||||
"main_service": "spoolman",
|
||||
"service_count": 1,
|
||||
"services": [
|
||||
{
|
||||
"name": "spoolman",
|
||||
"image": "ghcr.io/donkie/spoolman:latest",
|
||||
"is_main": true,
|
||||
"role": "frontend",
|
||||
"vmid_offset": 0,
|
||||
"depends_on": [],
|
||||
"frontend_network": true,
|
||||
"private_network": false,
|
||||
"compose": {
|
||||
"image": "ghcr.io/donkie/spoolman:latest",
|
||||
"container_name": "spoolman",
|
||||
"restart": "unless-stopped",
|
||||
"ports": [
|
||||
"7912:8000/tcp"
|
||||
],
|
||||
"volumes": [
|
||||
"./data:/home/app/.local/share/spoolman"
|
||||
],
|
||||
"environment": {
|
||||
"PUID": "1000",
|
||||
"PGID": "1000",
|
||||
"TZ": "Etc/UTC"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"top_level": {
|
||||
"name": "spoolman"
|
||||
},
|
||||
"networking": {
|
||||
"frontend": "selected-proxmox-bridge",
|
||||
"private_required": false,
|
||||
"private_creation": "automatic-create-if-missing",
|
||||
"private_address_allocation": "automatic-static-address-per-service",
|
||||
"service_discovery": "private-addresses-with-compose-service-host-aliases",
|
||||
"dependency_external_access": "disabled-unless-service-publishes-ports",
|
||||
"prompt_user_for_private_network": false
|
||||
},
|
||||
"storage": [
|
||||
{
|
||||
"id": "spoolman-volume-0",
|
||||
"service": "spoolman",
|
||||
"container_path": "/home/app/.local/share/spoolman",
|
||||
"mode": "managed-volume",
|
||||
"user_selectable": true,
|
||||
"backup": true,
|
||||
"shared_with_other_lxc": false,
|
||||
"source_path": null,
|
||||
"source_path_prompt": null
|
||||
}
|
||||
],
|
||||
"orchestration": {
|
||||
"reserve_vmids_atomically": 1,
|
||||
"start_order": [
|
||||
"spoolman"
|
||||
],
|
||||
"stop_order": [
|
||||
"spoolman"
|
||||
],
|
||||
"dependency_readiness": "compose-healthcheck-then-port-or-process-fallback",
|
||||
"rollback_on_failure": "remove-new-rootfs-preserve-created-persistent-volumes"
|
||||
},
|
||||
"installer_inputs": {
|
||||
"prompted": [
|
||||
"stack_name",
|
||||
"base_vmid",
|
||||
"rootfs_storage",
|
||||
"persistent_data_destinations",
|
||||
"frontend_bridge",
|
||||
"frontend_ipv4_mode"
|
||||
],
|
||||
"automatic": [
|
||||
"dependent_vmids",
|
||||
"private_bridge",
|
||||
"private_subnet",
|
||||
"private_service_addresses",
|
||||
"compose_service_aliases",
|
||||
"generated_secrets",
|
||||
"dependency_start_and_stop_order"
|
||||
],
|
||||
"generated_secrets": []
|
||||
}
|
||||
},
|
||||
"first_run": {
|
||||
"endpoints": [
|
||||
{
|
||||
"label": "Spoolman web interface",
|
||||
"scheme": "http",
|
||||
"port": 8000,
|
||||
"path": "/",
|
||||
"source": "upstream-image"
|
||||
}
|
||||
],
|
||||
"credentials": []
|
||||
},
|
||||
"proxmox": {
|
||||
"runtime": "native-oci-lxc",
|
||||
"technology_status": "proxmox-technology-preview",
|
||||
"defaults": {
|
||||
"unprivileged": true,
|
||||
"ostype": "auto-from-image",
|
||||
"cores": 2,
|
||||
"memory_mb": 1024,
|
||||
"swap_mb": 512,
|
||||
"rootfs_size_gb": 8,
|
||||
"rootfs_storage": "local-lvm",
|
||||
"volume_storage": "local-lvm",
|
||||
"template_storage": "local",
|
||||
"bridge": "vmbr0",
|
||||
"ipv4": "dhcp",
|
||||
"firewall": true,
|
||||
"host_managed_network": true,
|
||||
"onboot": false,
|
||||
"features": [
|
||||
"nesting=1"
|
||||
],
|
||||
"shutdown_timeout_seconds": 30
|
||||
},
|
||||
"image_metadata_policy": {
|
||||
"entrypoint": "import-from-oci-image",
|
||||
"cmd": "import-from-oci-image",
|
||||
"environment": "import-image-env-then-apply-compose-overrides",
|
||||
"user": "import-from-oci-image",
|
||||
"working_dir": "import-from-oci-image",
|
||||
"stop_signal": "import-from-oci-image"
|
||||
},
|
||||
"adaptations": [
|
||||
{
|
||||
"id": "imported-compose-source",
|
||||
"upstream_behavior": "The source definition deploys the complete Docker Compose application model.",
|
||||
"native_lxc_behavior": "The source model is preserved and remains blocked until every service option has a reviewed native Proxmox mapping.",
|
||||
"reason": "Catalog import must not imply runtime compatibility.",
|
||||
"behavioral_impact": "No automatic installation before review.",
|
||||
"validation": "pending-per-application"
|
||||
},
|
||||
{
|
||||
"id": "rolling-latest-image",
|
||||
"upstream_behavior": "A discovered Compose may pin a release tag or digest.",
|
||||
"native_lxc_behavior": "ProxMenux selects the same image repository with the latest tag for catalog installations.",
|
||||
"reason": "The automatic catalog intentionally offers rolling latest images; pinned versions belong to the future manual installer.",
|
||||
"behavioral_impact": "The installed release can be newer than the discovered Compose revision.",
|
||||
"validation": "pending-per-application"
|
||||
},
|
||||
{
|
||||
"id": "dedicated-lxc-network",
|
||||
"upstream_behavior": "Docker publishes selected container ports on the Docker host.",
|
||||
"native_lxc_behavior": "A reviewed native application will listen on its original container ports at a dedicated LXC address.",
|
||||
"reason": "A native LXC has its own address and does not require Docker port NAT.",
|
||||
"behavioral_impact": "Published ports are metadata; ProxMenux URLs use the matching container target port.",
|
||||
"validation": "pending-per-application"
|
||||
},
|
||||
{
|
||||
"id": "compose-shm-size",
|
||||
"upstream_behavior": "Compose sets the size of the container /dev/shm tmpfs.",
|
||||
"native_lxc_behavior": "ProxMenux mounts a native LXC tmpfs at /dev/shm with the same requested capacity.",
|
||||
"reason": "The OCI image runs directly as an LXC and therefore needs the equivalent Proxmox mount entry.",
|
||||
"behavioral_impact": "None expected.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-command",
|
||||
"upstream_behavior": "Compose replaces the image Cmd while retaining its Entrypoint.",
|
||||
"native_lxc_behavior": "ProxMenux reads the official OCI Entrypoint and combines it with the Compose command as the native LXC init command.",
|
||||
"reason": "Proxmox stores the effective OCI process as one entrypoint string.",
|
||||
"behavioral_impact": "None expected.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-privileged-mode",
|
||||
"upstream_behavior": "Compose selects whether the container runs in privileged mode.",
|
||||
"native_lxc_behavior": "ProxMenux keeps the LXC unprivileged by default and exposes the broad Compose privilege request only as an explicit compatibility option.",
|
||||
"reason": "A Compose privilege request is source metadata, not proof that the image technically requires a privileged LXC.",
|
||||
"behavioral_impact": "A privileged LXC has weaker isolation from the Proxmox host.",
|
||||
"validation": "native-equivalent"
|
||||
},
|
||||
{
|
||||
"id": "compose-process-runtime",
|
||||
"upstream_behavior": "Compose can replace Entrypoint, User and WorkingDir and request an init process or interactive terminal.",
|
||||
"native_lxc_behavior": "ProxMenux applies the process overrides through native LXC init directives; lxc-init provides PID 1 supervision and the CT console provides terminal access.",
|
||||
"reason": "The OCI process must start with the same identity, command and working directory without Docker.",
|
||||
"behavioral_impact": "Compose stdin_open and tty become access through the Proxmox LXC console.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-healthcheck",
|
||||
"upstream_behavior": "Docker periodically executes the declared container healthcheck.",
|
||||
"native_lxc_behavior": "For a single-service LXC, ProxMenux translates HTTP localhost checks into a mandatory first-start service check.",
|
||||
"reason": "Proxmox has no persistent Docker health state, while the installer still must detect a failed first boot.",
|
||||
"behavioral_impact": "The check runs during installation rather than continuously after installation.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-cpu-priority",
|
||||
"upstream_behavior": "Docker cpu_shares sets a relative scheduling weight with 1024 as its neutral value.",
|
||||
"native_lxc_behavior": "ProxMenux converts the relative weight to Proxmox cpuunits with 100 as its neutral value and lets the user review it.",
|
||||
"reason": "Both settings express relative CPU priority on different scales.",
|
||||
"behavioral_impact": "Rounding and Proxmox minimum limits can slightly change very low weights.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-resource-limits",
|
||||
"upstream_behavior": "mem_limit sets the memory ceiling; ulimits sets process soft/hard resource limits.",
|
||||
"native_lxc_behavior": "mem_limit supplies the editable Proxmox memory default, rounded up to MiB; ulimits maps to lxc.prlimit with -1 represented as unlimited.",
|
||||
"reason": "Use native Proxmox memory and LXC prlimits, without a wrapper or changing the host kernel configuration.",
|
||||
"behavioral_impact": "User-selected memory overrides Compose. Swap is a separate choice. Kernel restrictions still apply; nproc counts processes by real UID, not by container.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-network-identity",
|
||||
"upstream_behavior": "Compose can set hostname, MAC address, extra hosts and attach a service to Docker networks.",
|
||||
"native_lxc_behavior": "ProxMenux applies hostname and MAC to net0, writes additional host aliases into the LXC and uses its dedicated bridge connection for single-service networks.",
|
||||
"reason": "A dedicated LXC has its own network namespace and does not need a Docker bridge per service.",
|
||||
"behavioral_impact": "host-gateway resolves to the IPv4 address of the selected Proxmox bridge.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-network-mode",
|
||||
"upstream_behavior": "Docker host mode removes Docker network isolation; bridge and default use a Docker-managed network.",
|
||||
"native_lxc_behavior": "The OCI process uses the dedicated LXC network namespace directly, so host, bridge and default all listen on the LXC address without Docker NAT.",
|
||||
"reason": "The LXC is the application host and already has its own address and port namespace.",
|
||||
"behavioral_impact": "host means the LXC host, never the Proxmox host; this preserves Proxmox network isolation.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-capabilities-and-sysctls",
|
||||
"upstream_behavior": "Compose can add Linux capabilities and set kernel parameters in the container network namespace.",
|
||||
"native_lxc_behavior": "ProxMenux validates requested capabilities against the native LXC capability set and writes namespaced network settings as lxc.sysctl directives.",
|
||||
"reason": "A native OCI-LXC already starts with the namespaced capability set; lxc.cap.keep would incorrectly discard unrelated required capabilities.",
|
||||
"behavioral_impact": "Host-global capabilities such as SYS_MODULE remain blocked until their host prerequisite is explicitly adapted.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "docker-engine-metadata",
|
||||
"upstream_behavior": "Compose labels annotate Docker objects and the json-file logging driver rotates Docker-managed logs.",
|
||||
"native_lxc_behavior": "Labels remain source metadata; Docker json-file settings are not applied because the OCI process runs directly under LXC.",
|
||||
"reason": "There is no Docker object or Docker json-file log behind a native OCI-LXC application.",
|
||||
"behavioral_impact": "Docker-only label consumers and Docker log-driver rotation do not exist in the native deployment.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-device-passthrough",
|
||||
"upstream_behavior": "Compose passes host character devices or requests an NVIDIA runtime GPU.",
|
||||
"native_lxc_behavior": "ProxMenux converts recognized device declarations to native Proxmox dev resources; NVIDIA profiles also inject compatible host driver libraries read-only.",
|
||||
"reason": "Native OCI-LXC does not execute Docker device or NVIDIA runtime hooks.",
|
||||
"behavioral_impact": "Hardware is exposed only after explicit user confirmation and host-path validation.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-host-ipc",
|
||||
"upstream_behavior": "ipc: host shares the Docker host IPC namespace, commonly to avoid Docker's small default shared-memory allocation.",
|
||||
"native_lxc_behavior": "The application keeps the LXC IPC namespace and receives a configurable 1 GiB /dev/shm instead of sharing Proxmox host IPC.",
|
||||
"reason": "Processes in a single native LXC already share one IPC namespace; retaining isolation is safer than exposing host IPC.",
|
||||
"behavioral_impact": "The application cannot exchange IPC objects with processes on the Proxmox host.",
|
||||
"validation": "not-requested-by-compose"
|
||||
}
|
||||
],
|
||||
"installer_profile": {
|
||||
"startup_healthcheck": {
|
||||
"scheme": "http",
|
||||
"port": 8000,
|
||||
"path": "/",
|
||||
"timeout_seconds": 180,
|
||||
"request_timeout_seconds": 10,
|
||||
"stability_seconds": 4,
|
||||
"verify_tls": true
|
||||
},
|
||||
"volume_preparations": [
|
||||
{
|
||||
"container_path": "/home/app/.local/share/spoolman",
|
||||
"remove_lost_found": true,
|
||||
"owner_strategy": "mapped-application-user",
|
||||
"only_when_mount_type": "managed-volume"
|
||||
}
|
||||
],
|
||||
"volume_owner": {
|
||||
"uid": 1000,
|
||||
"gid": 1000
|
||||
}
|
||||
}
|
||||
},
|
||||
"compatibility": {
|
||||
"automatic_install_candidate": true,
|
||||
"validated": false,
|
||||
"supported_compose_keys": [
|
||||
"cap_add",
|
||||
"command",
|
||||
"container_name",
|
||||
"cpu_shares",
|
||||
"deploy",
|
||||
"devices",
|
||||
"entrypoint",
|
||||
"environment",
|
||||
"extra_hosts",
|
||||
"group_add",
|
||||
"healthcheck",
|
||||
"hostname",
|
||||
"image",
|
||||
"init",
|
||||
"ipc",
|
||||
"labels",
|
||||
"logging",
|
||||
"mac_address",
|
||||
"mem_limit",
|
||||
"network_mode",
|
||||
"networks",
|
||||
"ports",
|
||||
"privileged",
|
||||
"restart",
|
||||
"runtime",
|
||||
"security_opt",
|
||||
"shm_size",
|
||||
"stdin_open",
|
||||
"stop_grace_period",
|
||||
"sysctls",
|
||||
"tty",
|
||||
"ulimits",
|
||||
"user",
|
||||
"volumes",
|
||||
"working_dir"
|
||||
],
|
||||
"untranslated_blockers": [],
|
||||
"policy": "Single-image definitions are installable when every declared Compose option has a native Proxmox translation. Multi-image and unsupported runtime features remain blocked until their orchestrator or mapping is available."
|
||||
},
|
||||
"validation": {
|
||||
"schema": "passed-at-generation",
|
||||
"clean_install": "passed-default-official-image-skopeo-and-first-start-http",
|
||||
"advanced_install": "pending-custom-install-summary-validation",
|
||||
"service_health": "passed-web-api-crud",
|
||||
"restart_persistence": "passed-managed-and-host-bind-database-and-records",
|
||||
"backup_restore": "passed-pbs-backup-and-manager-recover-record-preserved",
|
||||
"update_preserves_data": "no-update-available-at-check",
|
||||
"modification": "passed-cpu-ram-up-and-down-without-image-pull-or-recreate",
|
||||
"volumes": "passed-managed-storage-and-host-bind-crud-owner-and-data-check",
|
||||
"removal": "passed-manager-removal-host-path-retained-then-temporary-data-cleaned"
|
||||
},
|
||||
"lifecycle": {
|
||||
"update_strategy": "resolve-latest-image-then-apply-reviewed-native-lxc-update",
|
||||
"registry_state": {
|
||||
"resolved_architecture": null,
|
||||
"resolved_digest": null,
|
||||
"image_version_label": null,
|
||||
"image_created": null
|
||||
},
|
||||
"change_detection": "compare-compose-sha256-and-resolved-latest-image-digest",
|
||||
"automatic_unattended_updates": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
{
|
||||
"schema_version": "0.5.0",
|
||||
"kind": "proxmenux.oci-template",
|
||||
"id": "image-unbound",
|
||||
"status": "generated-unvalidated",
|
||||
"catalog_ui": {
|
||||
"title": {
|
||||
"en_US": "Unbound"
|
||||
},
|
||||
"tagline": {
|
||||
"en_US": "A validating, recursive DNS resolver for your private network."
|
||||
},
|
||||
"description": {
|
||||
"en_US": "Run the community-maintained klutchell image of Unbound as a DNS resolver on its own Proxmox LXC. It listens on TCP and UDP port 53 and can load optional custom configuration files."
|
||||
},
|
||||
"category": "adblock",
|
||||
"category_label": "Adblock & DNS",
|
||||
"author": "Kyle Harding",
|
||||
"developer": "Kyle Harding (klutchell)",
|
||||
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/unbound.webp",
|
||||
"thumbnail": null,
|
||||
"screenshots": [],
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64",
|
||||
"armhf"
|
||||
],
|
||||
"launch": {
|
||||
"scheme": "http",
|
||||
"port": null,
|
||||
"path": "/"
|
||||
},
|
||||
"website": "https://unbound.net/",
|
||||
"documentation": "https://github.com/klutchell/unbound-docker#readme",
|
||||
"repository": "https://github.com/klutchell/unbound-docker",
|
||||
"tips": [
|
||||
"Unbound has no web interface. Use the LXC IP as a DNS server on TCP and UDP port 53.",
|
||||
"The default image config listens on all interfaces and accepts clients from private IPv4 ranges. Keep it behind your trusted LAN firewall; do not expose recursive DNS publicly.",
|
||||
"The original GitHub repository has moved to klutchell/unbound-docker; the published Docker Hub image remains klutchell/unbound:main. The image is an unofficial community image, not published by NLnet Labs.",
|
||||
"Optional custom .conf files go under /etc/unbound/custom.conf.d and must be readable by UID/GID 101:102 or world-readable."
|
||||
],
|
||||
"mini_changelog": [],
|
||||
"display_version": null,
|
||||
"updated_at": null
|
||||
},
|
||||
"source": {
|
||||
"provider": "klutchell",
|
||||
"repository": "https://github.com/klutchell/unbound-docker",
|
||||
"revision": "1efbea960bf9462b6fb6dd71aa702ad32d6820d7",
|
||||
"default_branch": "main",
|
||||
"image_repository_url": "https://hub.docker.com/r/klutchell/unbound",
|
||||
"readme_pushed_at": "2026-11-02T14:31:22Z",
|
||||
"compose_sha256": "faf73a93c1f7d4d43f1eb61ba2e62a4418bd501f0c827b080ea9897964e75848",
|
||||
"generated_at": "2026-11-02T21:21:54+00:00",
|
||||
"readme_raw_url": "https://raw.githubusercontent.com/klutchell/unbound-docker/main/README.md"
|
||||
},
|
||||
"container_contract": {
|
||||
"service_name": "unbound",
|
||||
"container_name": "unbound",
|
||||
"image": {
|
||||
"reference": "klutchell/unbound:main",
|
||||
"registry": "docker.io",
|
||||
"repository": "klutchell/unbound",
|
||||
"tag": "main",
|
||||
"digest": null,
|
||||
"pull_policy": "resolve-selected-tag-to-architecture-digest-at-install"
|
||||
},
|
||||
"environment": [],
|
||||
"volumes": [
|
||||
{
|
||||
"id": "volume-0",
|
||||
"container_path": "/etc/unbound/custom.conf.d",
|
||||
"compose_source_example": "./config",
|
||||
"read_only": false,
|
||||
"required": false,
|
||||
"installation_choice": [
|
||||
"managed-volume",
|
||||
"host-bind"
|
||||
],
|
||||
"default": "host-bind",
|
||||
"managed_volume": {
|
||||
"backup": true,
|
||||
"default_size_gb": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
"container_port": 53,
|
||||
"published_example": 53,
|
||||
"protocol": "udp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
},
|
||||
{
|
||||
"container_port": 53,
|
||||
"published_example": 53,
|
||||
"protocol": "tcp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
}
|
||||
],
|
||||
"related_services": [],
|
||||
"restart": "unless-stopped",
|
||||
"stop_grace_period": null,
|
||||
"original_compose": "name: unbound\nservices:\n unbound:\n image: klutchell/unbound:main\n container_name: unbound\n restart: unless-stopped\n ports:\n - 53:53/udp\n - 53:53/tcp\n volumes:\n - ./config:/etc/unbound/custom.conf.d\n"
|
||||
},
|
||||
"compose_stack": {
|
||||
"project_name": "unbound",
|
||||
"deployment_model": "one-native-oci-lxc-per-compose-service",
|
||||
"user_experience": "single-application-install",
|
||||
"main_service": "unbound",
|
||||
"service_count": 1,
|
||||
"services": [
|
||||
{
|
||||
"name": "unbound",
|
||||
"image": "klutchell/unbound:main",
|
||||
"is_main": true,
|
||||
"role": "frontend",
|
||||
"vmid_offset": 0,
|
||||
"depends_on": [],
|
||||
"frontend_network": true,
|
||||
"private_network": false,
|
||||
"compose": {
|
||||
"image": "klutchell/unbound:main",
|
||||
"container_name": "unbound",
|
||||
"restart": "unless-stopped",
|
||||
"ports": [
|
||||
"53:53/udp",
|
||||
"53:53/tcp"
|
||||
],
|
||||
"volumes": [
|
||||
"./config:/etc/unbound/custom.conf.d"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"top_level": {
|
||||
"name": "unbound"
|
||||
},
|
||||
"networking": {
|
||||
"frontend": "selected-proxmox-bridge",
|
||||
"private_required": false,
|
||||
"private_creation": "automatic-create-if-missing",
|
||||
"private_address_allocation": "automatic-static-address-per-service",
|
||||
"service_discovery": "private-addresses-with-compose-service-host-aliases",
|
||||
"dependency_external_access": "disabled-unless-service-publishes-ports",
|
||||
"prompt_user_for_private_network": false
|
||||
},
|
||||
"storage": [
|
||||
{
|
||||
"id": "unbound-volume-0",
|
||||
"service": "unbound",
|
||||
"container_path": "/etc/unbound/custom.conf.d",
|
||||
"mode": "user-selectable",
|
||||
"user_selectable": true,
|
||||
"backup": true,
|
||||
"shared_with_other_lxc": false,
|
||||
"source_path": null,
|
||||
"source_path_prompt": "Host directory containing optional custom Unbound .conf files",
|
||||
"default": "host-bind",
|
||||
"installation_choice": [
|
||||
"managed-volume",
|
||||
"host-bind"
|
||||
]
|
||||
}
|
||||
],
|
||||
"orchestration": {
|
||||
"reserve_vmids_atomically": 1,
|
||||
"start_order": [
|
||||
"unbound"
|
||||
],
|
||||
"stop_order": [
|
||||
"unbound"
|
||||
],
|
||||
"dependency_readiness": "compose-healthcheck-then-port-or-process-fallback",
|
||||
"rollback_on_failure": "remove-new-rootfs-preserve-created-persistent-volumes"
|
||||
},
|
||||
"installer_inputs": {
|
||||
"prompted": [
|
||||
"stack_name",
|
||||
"base_vmid",
|
||||
"rootfs_storage",
|
||||
"persistent_data_destinations",
|
||||
"frontend_bridge",
|
||||
"frontend_ipv4_mode"
|
||||
],
|
||||
"automatic": [
|
||||
"dependent_vmids",
|
||||
"private_bridge",
|
||||
"private_subnet",
|
||||
"private_service_addresses",
|
||||
"compose_service_aliases",
|
||||
"generated_secrets",
|
||||
"dependency_start_and_stop_order"
|
||||
]
|
||||
}
|
||||
},
|
||||
"first_run": {
|
||||
"endpoints": [],
|
||||
"credentials": []
|
||||
},
|
||||
"proxmox": {
|
||||
"runtime": "native-oci-lxc",
|
||||
"technology_status": "proxmox-technology-preview",
|
||||
"defaults": {
|
||||
"unprivileged": true,
|
||||
"ostype": "auto-from-image",
|
||||
"cores": 2,
|
||||
"memory_mb": 512,
|
||||
"swap_mb": 512,
|
||||
"rootfs_size_gb": 8,
|
||||
"rootfs_storage": "local-lvm",
|
||||
"volume_storage": "local-lvm",
|
||||
"template_storage": "local",
|
||||
"bridge": "vmbr0",
|
||||
"ipv4": "dhcp",
|
||||
"firewall": true,
|
||||
"host_managed_network": true,
|
||||
"onboot": false,
|
||||
"features": [
|
||||
"nesting=1"
|
||||
],
|
||||
"shutdown_timeout_seconds": 120
|
||||
},
|
||||
"image_metadata_policy": {
|
||||
"entrypoint": "import-from-oci-image",
|
||||
"cmd": "import-from-oci-image",
|
||||
"environment": "import-image-env-then-apply-compose-overrides",
|
||||
"user": "import-from-oci-image",
|
||||
"working_dir": "import-from-oci-image",
|
||||
"stop_signal": "import-from-oci-image"
|
||||
},
|
||||
"adaptations": [],
|
||||
"installer_profile": {
|
||||
"volume_preparations": [
|
||||
{
|
||||
"container_path": "/etc/unbound/custom.conf.d",
|
||||
"owner_strategy": "mapped-root",
|
||||
"remove_lost_found": true,
|
||||
"only_when_mount_type": "managed-volume"
|
||||
}
|
||||
],
|
||||
"startup_healthcheck": {
|
||||
"type": "running",
|
||||
"timeout_seconds": 90,
|
||||
"stability_seconds": 20
|
||||
}
|
||||
},
|
||||
"catalog": {
|
||||
"replaces_discovered_ids": []
|
||||
}
|
||||
},
|
||||
"compatibility": {
|
||||
"automatic_install_candidate": true,
|
||||
"validated": false,
|
||||
"supported_compose_keys": [
|
||||
"container_name",
|
||||
"image",
|
||||
"ports",
|
||||
"restart",
|
||||
"volumes"
|
||||
],
|
||||
"untranslated_blockers": [],
|
||||
"policy": "This single-image Compose contract maps to one unprivileged Proxmox LXC. The optional custom-config directory can use a Proxmox-managed volume or a host directory; the image defaults work with an empty directory."
|
||||
},
|
||||
"validation": {
|
||||
"schema": "passed-at-generation",
|
||||
"image_resolution": "passed-skopeo-16-layers",
|
||||
"clean_install": "passed-default-install-and-first-start-healthcheck",
|
||||
"advanced_install": "pending",
|
||||
"service_health": "passed-recursion-and-custom-local-data-over-udp-and-tcp",
|
||||
"restart_persistence": "passed-custom-zone-after-pct-reboot",
|
||||
"backup_restore": "pending",
|
||||
"update_preserves_data": "pending-update-check-not-run",
|
||||
"modification": "pending-manager-ui-unresponsive-no-changes-applied",
|
||||
"managed_volume": "passed-config-volume-1g",
|
||||
"host_bind": "pending",
|
||||
"removal": "passed-manager-remove-ct-disks-and-record-cleaned"
|
||||
},
|
||||
"lifecycle": {
|
||||
"update_strategy": "resolve-latest-image-then-apply-reviewed-native-lxc-update",
|
||||
"registry_state": {
|
||||
"resolved_architecture": null,
|
||||
"resolved_digest": null,
|
||||
"image_version_label": null,
|
||||
"image_created": null
|
||||
},
|
||||
"change_detection": "compare-compose-sha256-and-resolved-latest-image-digest",
|
||||
"automatic_unattended_updates": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
{
|
||||
"schema_version": "0.3.0",
|
||||
"kind": "proxmenux.oci-template",
|
||||
"id": "image-zwave-js-ui",
|
||||
"status": "generated-unvalidated",
|
||||
"catalog_ui": {
|
||||
"title": {
|
||||
"en_US": "Z-Wave JS UI"
|
||||
},
|
||||
"tagline": {
|
||||
"en_US": "Manage a Z-Wave network from a web dashboard and WebSocket gateway."
|
||||
},
|
||||
"description": {
|
||||
"en_US": "A self-hosted Z-Wave control panel and MQTT gateway. Its web interface can start without a Z-Wave controller; device control requires a compatible USB or serial controller and network security keys."
|
||||
},
|
||||
"category": "smarthome",
|
||||
"author": "Z-Wave JS",
|
||||
"developer": "Z-Wave JS",
|
||||
"icon": "https://raw.githubusercontent.com/zwave-js/zwave-js-ui/c38e0d2cbbb826bf2bab2db596a3452e59ce0e8e/public/apple-touch-icon-180x180.png",
|
||||
"screenshots": [],
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"launch": {
|
||||
"scheme": "http",
|
||||
"port": 8091,
|
||||
"path": "/"
|
||||
},
|
||||
"website": "https://zwave-js.github.io/zwave-js-ui/",
|
||||
"documentation": "https://zwave-js.github.io/zwave-js-ui/#/getting-started/docker",
|
||||
"repository": "https://github.com/zwave-js/zwave-js-ui",
|
||||
"tips": [
|
||||
"The web UI can start without a Z-Wave controller. To control devices, pass through a compatible USB/serial controller and select its stable /dev/serial/by-id/... path in Settings → Z-Wave → Serial Port.",
|
||||
"Configure and securely back up the S0/S2 network keys in Settings → Z-Wave before pairing security-enabled devices.",
|
||||
"Port 3000 is the Z-Wave JS WebSocket server for integrations such as Home Assistant; it listens only after a Z-Wave client is configured. Upstream documents it as unauthenticated, so expose it only to trusted integrations and networks.",
|
||||
"ProxMenux enables the web login and generates the initial admin password. Configure trusted HTTPS before remote access over an untrusted network."
|
||||
],
|
||||
"mini_changelog": [],
|
||||
"display_version": "11.25.2",
|
||||
"updated_at": "2026-11-02",
|
||||
"category_label": "IoT & Smart Home"
|
||||
},
|
||||
"source": {
|
||||
"provider": "Z-Wave JS",
|
||||
"repository": "https://github.com/zwave-js/zwave-js-ui",
|
||||
"default_branch": "master",
|
||||
"revision": "c38e0d2cbbb826bf2bab2db596a3452e59ce0e8e",
|
||||
"readme_raw_url": "https://raw.githubusercontent.com/zwave-js/zwave-js-ui/c38e0d2cbbb826bf2bab2db596a3452e59ce0e8e/README.md",
|
||||
"image_repository_url": "https://hub.docker.com/r/zwavejs/zwave-js-ui",
|
||||
"compose_sha256": "ed371d9e9805db5d2abd356b54e3ff5fdb677d637857313f6aac3f498f480f87",
|
||||
"generated_at": "2026-11-02T20:10:33+00:00"
|
||||
},
|
||||
"container_contract": {
|
||||
"service_name": "zwave-js-ui",
|
||||
"container_name": "zwave-js-ui",
|
||||
"image": {
|
||||
"reference": "docker.io/zwavejs/zwave-js-ui:latest",
|
||||
"registry": "docker.io",
|
||||
"repository": "zwavejs/zwave-js-ui",
|
||||
"tag": "latest",
|
||||
"digest": null,
|
||||
"pull_policy": "resolve-selected-tag-to-architecture-digest-at-install"
|
||||
},
|
||||
"environment": [
|
||||
{
|
||||
"name": "SESSION_SECRET",
|
||||
"example": "${GENERATED_SESSION_SECRET}",
|
||||
"required": true,
|
||||
"sensitive": true,
|
||||
"source": "ProxMenux-generated; supported by upstream image"
|
||||
},
|
||||
{
|
||||
"name": "DEFAULT_PASSWORD",
|
||||
"example": "${GENERATED_ADMIN_PASSWORD}",
|
||||
"prompt": "Initial Z-Wave JS UI admin password",
|
||||
"required": true,
|
||||
"sensitive": true,
|
||||
"source": "Upstream image; generated by ProxMenux"
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"id": "zwave-config",
|
||||
"container_path": "/usr/src/app/store",
|
||||
"compose_source_example": "zwave-config",
|
||||
"read_only": false,
|
||||
"required": true,
|
||||
"installation_choice": [
|
||||
"managed-volume"
|
||||
],
|
||||
"default": "managed-volume",
|
||||
"managed_volume": {
|
||||
"backup": true,
|
||||
"default_size_gb": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
"container_port": 8091,
|
||||
"published_example": 8091,
|
||||
"protocol": "tcp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
},
|
||||
{
|
||||
"container_port": 3000,
|
||||
"published_example": 3000,
|
||||
"protocol": "tcp",
|
||||
"required": false,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
}
|
||||
],
|
||||
"related_services": [],
|
||||
"restart": "always",
|
||||
"stop_grace_period": null,
|
||||
"original_compose": "version: '3.7'\nservices:\n zwave-js-ui:\n container_name: zwave-js-ui\n image: zwavejs/zwave-js-ui:latest\n restart: always\n tty: true\n stop_signal: SIGINT\n environment:\n SESSION_SECRET: ${GENERATED_SESSION_SECRET}\n DEFAULT_PASSWORD: ${GENERATED_ADMIN_PASSWORD}\n networks:\n - zwave\n devices:\n - /dev/serial/by-id/insert_stick_reference_here:/dev/zwave\n volumes:\n - zwave-config:/usr/src/app/store\n ports:\n - '8091:8091'\n - '3000:3000'\nnetworks:\n zwave:\nvolumes:\n zwave-config:\n name: zwave-config\n"
|
||||
},
|
||||
"first_run": {
|
||||
"endpoints": [
|
||||
{
|
||||
"label": "Z-Wave JS UI",
|
||||
"scheme": "http",
|
||||
"port": 8091,
|
||||
"path": "/",
|
||||
"source": "official-docker-documentation"
|
||||
}
|
||||
],
|
||||
"credentials": [
|
||||
{
|
||||
"label": "Z-Wave JS UI admin login",
|
||||
"type": "configured-or-installer-generated",
|
||||
"username": "admin",
|
||||
"password": null,
|
||||
"password_environment": "DEFAULT_PASSWORD",
|
||||
"change_required": false,
|
||||
"source": "Upstream DEFAULT_PASSWORD, generated by ProxMenux",
|
||||
"retrieval": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"proxmox": {
|
||||
"runtime": "native-oci-lxc",
|
||||
"technology_status": "proxmox-technology-preview",
|
||||
"defaults": {
|
||||
"unprivileged": true,
|
||||
"ostype": "auto-from-image",
|
||||
"cores": 2,
|
||||
"memory_mb": 1024,
|
||||
"swap_mb": 512,
|
||||
"rootfs_size_gb": 8,
|
||||
"rootfs_storage": "local-lvm",
|
||||
"volume_storage": "local-lvm",
|
||||
"template_storage": "local",
|
||||
"bridge": "vmbr0",
|
||||
"ipv4": "dhcp",
|
||||
"firewall": true,
|
||||
"host_managed_network": true,
|
||||
"onboot": true,
|
||||
"features": [
|
||||
"nesting=1"
|
||||
],
|
||||
"shutdown_timeout_seconds": 60
|
||||
},
|
||||
"image_metadata_policy": {
|
||||
"entrypoint": "import-from-oci-image",
|
||||
"cmd": "import-from-oci-image",
|
||||
"environment": "import-image-env-then-apply-compose-overrides",
|
||||
"user": "import-from-oci-image",
|
||||
"working_dir": "import-from-oci-image",
|
||||
"stop_signal": "apply-compose-override"
|
||||
},
|
||||
"adaptations": [
|
||||
{
|
||||
"id": "dedicated-lxc-network",
|
||||
"upstream_behavior": "Docker publishes ports on a Docker host address and attaches the service to its Compose network.",
|
||||
"native_lxc_behavior": "The OCI process listens on the dedicated LXC address; ProxMenux maps the single-service network to the LXC network namespace.",
|
||||
"reason": "A one-service native OCI LXC has its own network namespace and address.",
|
||||
"behavioral_impact": "Use the LXC address directly; no Docker port NAT or separate bridge is created.",
|
||||
"validation": "pending-per-application"
|
||||
},
|
||||
{
|
||||
"id": "generated-login-and-session-secret",
|
||||
"upstream_behavior": "The image defaults to user admin and password zwave; web authentication is disabled by default, and the Compose example uses a sample session secret.",
|
||||
"native_lxc_behavior": "ProxMenux generates a unique initial admin password and session secret, and seeds gateway.authEnabled=true before first boot.",
|
||||
"reason": "Avoid shipping a shared password, sample session key or unauthenticated web UI.",
|
||||
"behavioral_impact": "The installer must display the generated admin password; change it after first login.",
|
||||
"validation": "pending-per-application"
|
||||
},
|
||||
{
|
||||
"id": "stop-signal",
|
||||
"upstream_behavior": "Compose requests SIGINT when stopping the container.",
|
||||
"native_lxc_behavior": "ProxMenux uses the same signal for the OCI process shutdown lifecycle.",
|
||||
"reason": "Preserve the upstream graceful-shutdown behavior.",
|
||||
"behavioral_impact": "None expected.",
|
||||
"validation": "pending-per-application"
|
||||
}
|
||||
],
|
||||
"installer_profile": {
|
||||
"generated_sensitive_environment": {
|
||||
"SESSION_SECRET": {
|
||||
"strategy": "token-hex",
|
||||
"bytes": 32,
|
||||
"prompt": false
|
||||
},
|
||||
"DEFAULT_PASSWORD": {
|
||||
"strategy": "token-hex",
|
||||
"bytes": 20,
|
||||
"prompt": true
|
||||
}
|
||||
},
|
||||
"generated_files": [
|
||||
{
|
||||
"container_path": "/usr/src/app/store/settings.json",
|
||||
"mode": "0600",
|
||||
"owner": "mapped-root",
|
||||
"content": "{\"gateway\":{\"authEnabled\":true}}\n",
|
||||
"only_if_missing": true
|
||||
}
|
||||
],
|
||||
"volume_preparations": [
|
||||
{
|
||||
"container_path": "/usr/src/app/store",
|
||||
"remove_lost_found": true,
|
||||
"owner_strategy": "mapped-root",
|
||||
"only_when_mount_type": "managed-volume"
|
||||
}
|
||||
],
|
||||
"optional_devices": [
|
||||
{
|
||||
"id": "zwave-serial-controller",
|
||||
"kind": "character-device",
|
||||
"purpose": "serial",
|
||||
"enable_prompt": "Pass through a Z-Wave USB or serial controller",
|
||||
"enabled_default": false,
|
||||
"required_by_compose": false,
|
||||
"path_prompt": "Controller path on the Proxmox host; prefer /dev/serial/by-id/...",
|
||||
"host_path_default": "/dev/serial/by-id/insert_stick_reference_here",
|
||||
"container_path_strategy": "same-as-host",
|
||||
"mode": "preserve-host",
|
||||
"deny_write": false,
|
||||
"gid_strategy": "host-device-gid"
|
||||
}
|
||||
],
|
||||
"startup_healthcheck": {
|
||||
"scheme": "http",
|
||||
"port": 8091,
|
||||
"path": "/",
|
||||
"timeout_seconds": 180,
|
||||
"request_timeout_seconds": 10,
|
||||
"stability_seconds": 4,
|
||||
"verify_tls": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"compatibility": {
|
||||
"automatic_install_candidate": true,
|
||||
"validated": false,
|
||||
"supported_compose_keys": [
|
||||
"container_name",
|
||||
"devices",
|
||||
"environment",
|
||||
"image",
|
||||
"networks",
|
||||
"ports",
|
||||
"restart",
|
||||
"stop_signal",
|
||||
"tty",
|
||||
"volumes"
|
||||
],
|
||||
"untranslated_blockers": [],
|
||||
"policy": "The single-image recipe has passed installation, UI/login, restart, backup and recovery, modification, and engine checks. Host-bind storage has not been checked. Radio control remains untested because no compatible Z-Wave coordinator was attached during the lab test."
|
||||
},
|
||||
"validation": {
|
||||
"schema": "passed-static-validation",
|
||||
"image_resolution": "passed-skopeo-download-and-layer-verification",
|
||||
"clean_install": "passed-default-install-and-health-check",
|
||||
"advanced_install": "pending-custom-install-summary-validation",
|
||||
"web_response": "passed-ui-and-generated-login",
|
||||
"credentials": "passed-generated-login-before-and-after-restore",
|
||||
"restart_persistence": "passed-restart-with-store-preserved",
|
||||
"backup_restore": "passed-proxmox-backup-and-manager-recover-new-vmid",
|
||||
"update_preserves_data": "no-update-available-at-check",
|
||||
"modification": "passed-cpu-ram-up-down-without-recreation",
|
||||
"managed_volume": "passed-store-volume-and-recovery",
|
||||
"host_bind": "pending",
|
||||
"hardware_radio": "not-tested-no-z-wave-coordinator-on-host",
|
||||
"removal": "passed-remove-original-and-restored-ct-and-local-backup-cleanup"
|
||||
},
|
||||
"lifecycle": {
|
||||
"update_strategy": "replace-rootfs-from-new-oci-image-preserve-managed-volumes",
|
||||
"registry_state": {
|
||||
"resolved_architecture": null,
|
||||
"resolved_digest": null,
|
||||
"image_version_label": "11.25.2",
|
||||
"image_created": null
|
||||
},
|
||||
"change_detection": "compare-resolved-architecture-digest",
|
||||
"automatic_unattended_updates": false
|
||||
}
|
||||
}
|
||||
+170
-1
@@ -2,7 +2,7 @@
|
||||
"schema_version": "0.2.0",
|
||||
"kind": "proxmenux.oci-catalog-index",
|
||||
"provider": "multiple-container-images",
|
||||
"generated_at": "2026-10-30T18:13:36+00:00",
|
||||
"generated_at": "2026-11-02T21:25:05+00:00",
|
||||
"applications": [
|
||||
{
|
||||
"id": "2fauth",
|
||||
@@ -18829,6 +18829,175 @@
|
||||
"optional_relaxed_confinement": false,
|
||||
"requires_security_confirmation": false,
|
||||
"hidden": false
|
||||
},
|
||||
{
|
||||
"id": "kitchenowl",
|
||||
"provider": "tombursch",
|
||||
"template_family": "curated-profile",
|
||||
"title": "KitchenOwl",
|
||||
"repository": "https://github.com/TomBursch/kitchenowl",
|
||||
"description": "Manage shared shopping lists, recipes and meal plans in one self-hosted app.",
|
||||
"website": "https://kitchenowl.org/",
|
||||
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/kitchenowl.webp",
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"updated_at": "2026-11-01",
|
||||
"main_image": "tombursch/kitchenowl-web:latest",
|
||||
"category": "productivity",
|
||||
"category_label": "Productivity & Workflows",
|
||||
"replaces_discovered_ids": [],
|
||||
"curated_path": "catalog/curated/kitchenowl.json",
|
||||
"template_id": "image-kitchenowl",
|
||||
"template": "apps/kitchenowl.json",
|
||||
"template_status": "generated-unvalidated",
|
||||
"hidden": false,
|
||||
"automatic_install_candidate": true,
|
||||
"untranslated_blockers": [],
|
||||
"requires_privileged_lxc": false,
|
||||
"optional_privileged_lxc": false,
|
||||
"requires_host_pid_namespace": false,
|
||||
"requires_relaxed_confinement": false,
|
||||
"optional_relaxed_confinement": false,
|
||||
"requires_security_confirmation": false,
|
||||
"multi_container": true
|
||||
},
|
||||
{
|
||||
"id": "krusader",
|
||||
"provider": "jlesage",
|
||||
"template_family": "curated-profile",
|
||||
"title": "Krusader",
|
||||
"repository": "https://github.com/jlesage/docker-krusader",
|
||||
"description": "Open Krusader in a browser and manage files from a directory you explicitly mount at /storage. The maintained jlesage image provides the graphical desktop; ProxMenux runs it as one native OCI LXC.",
|
||||
"website": "https://krusader.org/",
|
||||
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/krusader.webp",
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"updated_at": null,
|
||||
"main_image": "jlesage/krusader:latest",
|
||||
"category": "desktop",
|
||||
"category_label": "Browsers & Web Desktops",
|
||||
"replaces_discovered_ids": [],
|
||||
"curated_path": "catalog/curated/krusader.json",
|
||||
"template_id": "image-krusader",
|
||||
"template": "apps/krusader.json",
|
||||
"template_status": "generated-unvalidated",
|
||||
"hidden": false,
|
||||
"automatic_install_candidate": true,
|
||||
"untranslated_blockers": [],
|
||||
"requires_privileged_lxc": false,
|
||||
"optional_privileged_lxc": false,
|
||||
"requires_host_pid_namespace": false,
|
||||
"requires_relaxed_confinement": false,
|
||||
"optional_relaxed_confinement": false,
|
||||
"requires_security_confirmation": false,
|
||||
"multi_container": false
|
||||
},
|
||||
{
|
||||
"id": "spoolman",
|
||||
"provider": "Donkie",
|
||||
"template_family": "curated-profile",
|
||||
"title": "Spoolman",
|
||||
"repository": "https://github.com/Donkie/Spoolman",
|
||||
"description": "A self-hosted filament inventory for 3D printers. Manage spools, materials, manufacturers, locations and labels in the web UI, and integrate with Moonraker, OctoPrint and other printer tools. The official image uses SQLite by default; no separate database container is required.",
|
||||
"website": "https://github.com/Donkie/Spoolman",
|
||||
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/spoolman.webp",
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"updated_at": null,
|
||||
"main_image": "ghcr.io/donkie/spoolman:latest",
|
||||
"category": "tools",
|
||||
"category_label": "Tools",
|
||||
"replaces_discovered_ids": [],
|
||||
"curated_path": "catalog/curated/spoolman.json",
|
||||
"template": "apps/spoolman.json",
|
||||
"template_status": "generated-unvalidated",
|
||||
"hidden": false,
|
||||
"automatic_install_candidate": true,
|
||||
"untranslated_blockers": [],
|
||||
"requires_privileged_lxc": false,
|
||||
"optional_privileged_lxc": false,
|
||||
"requires_host_pid_namespace": false,
|
||||
"requires_relaxed_confinement": false,
|
||||
"optional_relaxed_confinement": false,
|
||||
"requires_security_confirmation": false,
|
||||
"multi_container": false,
|
||||
"template_id": "image-spoolman",
|
||||
"source_app_id": "spoolman",
|
||||
"default_branch": "master",
|
||||
"source_path": "OCI recipe from upstream installation guide",
|
||||
"source_revision": "ff8efd517b76a4b2783f29c89e9b9fbcd8cd167b",
|
||||
"pushed_at": "2026-10-31T09:39:19Z"
|
||||
},
|
||||
{
|
||||
"id": "unbound",
|
||||
"provider": "klutchell",
|
||||
"template_family": "curated-profile",
|
||||
"title": "Unbound",
|
||||
"repository": "https://github.com/klutchell/unbound-docker",
|
||||
"description": "Run the community-maintained klutchell image of Unbound as a DNS resolver on its own Proxmox LXC. It listens on TCP and UDP port 53 and can load optional custom configuration files.",
|
||||
"website": "https://unbound.net/",
|
||||
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/unbound.webp",
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"updated_at": null,
|
||||
"main_image": "klutchell/unbound:main",
|
||||
"category": "adblock",
|
||||
"category_label": "Adblock & DNS",
|
||||
"replaces_discovered_ids": [],
|
||||
"curated_path": "catalog/curated/unbound.json",
|
||||
"template_id": "image-unbound",
|
||||
"template": "apps/unbound.json",
|
||||
"template_status": "generated-unvalidated",
|
||||
"hidden": false,
|
||||
"automatic_install_candidate": true,
|
||||
"untranslated_blockers": [],
|
||||
"requires_privileged_lxc": false,
|
||||
"optional_privileged_lxc": false,
|
||||
"requires_host_pid_namespace": false,
|
||||
"requires_relaxed_confinement": false,
|
||||
"optional_relaxed_confinement": false,
|
||||
"requires_security_confirmation": false,
|
||||
"multi_container": false
|
||||
},
|
||||
{
|
||||
"id": "zwave-js-ui",
|
||||
"provider": "Z-Wave JS",
|
||||
"template_family": "curated-profile",
|
||||
"title": "Z-Wave JS UI",
|
||||
"repository": "https://github.com/zwave-js/zwave-js-ui",
|
||||
"description": "A self-hosted Z-Wave control panel and MQTT gateway. Its web interface can start without a Z-Wave controller; device control requires a compatible USB or serial controller and network security keys.",
|
||||
"website": "https://zwave-js.github.io/zwave-js-ui/",
|
||||
"icon": "https://raw.githubusercontent.com/zwave-js/zwave-js-ui/c38e0d2cbbb826bf2bab2db596a3452e59ce0e8e/public/apple-touch-icon-180x180.png",
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"updated_at": "2026-11-02",
|
||||
"main_image": "docker.io/zwavejs/zwave-js-ui:latest",
|
||||
"category": "smarthome",
|
||||
"category_label": "IoT & Smart Home",
|
||||
"replaces_discovered_ids": [],
|
||||
"curated_path": "catalog/curated/zwave-js-ui.json",
|
||||
"template_id": "image-zwave-js-ui",
|
||||
"template": "apps/zwave-js-ui.json",
|
||||
"template_status": "generated-unvalidated",
|
||||
"automatic_install_candidate": true,
|
||||
"untranslated_blockers": [],
|
||||
"requires_privileged_lxc": false,
|
||||
"optional_privileged_lxc": false,
|
||||
"requires_host_pid_namespace": false,
|
||||
"requires_relaxed_confinement": false,
|
||||
"optional_relaxed_confinement": false,
|
||||
"requires_security_confirmation": false,
|
||||
"multi_container": false
|
||||
}
|
||||
],
|
||||
"discovery": {
|
||||
|
||||
@@ -119,9 +119,15 @@
|
||||
"airdcpp-webclient": [
|
||||
"/Downloads",
|
||||
"/Share"
|
||||
],
|
||||
"spoolman": [
|
||||
"/home/app/.local/share/spoolman"
|
||||
]
|
||||
},
|
||||
"sizes": {
|
||||
"krusader": {
|
||||
"/config": 4
|
||||
},
|
||||
"sharecovex": {
|
||||
"/config": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
"""The KitchenOwl entry follows the upstream two-service Compose deployment."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from jsonschema import validate
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
from proxmenux_oci.catalog import Catalog
|
||||
from proxmenux_oci.custom import template_from_oci_compose
|
||||
from proxmenux_oci.stack import assess, build_stack, DefaultsUI
|
||||
from proxmenux_oci.installer import DEFAULT_MODE
|
||||
|
||||
CURATED_PATH = ROOT / "catalog/curated/kitchenowl.json"
|
||||
APPS_PATH = ROOT / "catalog/apps/kitchenowl.json"
|
||||
|
||||
|
||||
def compile_stack(template):
|
||||
"""Compile the recipe with deterministic answers, without touching Proxmox."""
|
||||
with patch("proxmenux_oci.installer.ask_storage", return_value="local-zfs"), \
|
||||
patch("proxmenux_oci.installer.ask_bridge", return_value="vmbr0"), \
|
||||
patch("proxmenux_oci.host.timezone", return_value="UTC"), \
|
||||
patch("proxmenux_oci.network.ask_addresses",
|
||||
side_effect=lambda ui, bridge, labels: ({name: "dhcp" for name in labels}, None)):
|
||||
return build_stack(template, DefaultsUI(), DEFAULT_MODE)
|
||||
|
||||
|
||||
class KitchenOwlRecipeTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.curated = json.loads(CURATED_PATH.read_text(encoding="utf-8"))
|
||||
self.generated = json.loads(APPS_PATH.read_text(encoding="utf-8"))
|
||||
self.index = json.loads((ROOT / "catalog/index.json").read_text(encoding="utf-8"))
|
||||
self.services = {
|
||||
service["name"]: service
|
||||
for service in self.curated["compose_stack"]["services"]
|
||||
}
|
||||
|
||||
def test_curated_and_catalog_templates_match_and_validate(self):
|
||||
self.assertEqual(self.curated, self.generated)
|
||||
schema = json.loads((ROOT / "schemas/oci-template.schema.json").read_text(encoding="utf-8"))
|
||||
validate(self.curated, schema)
|
||||
|
||||
def test_official_compose_is_the_two_image_frontend_backend_stack(self):
|
||||
self.assertEqual(set(self.services), {"front", "back"})
|
||||
self.assertEqual(self.curated["compose_stack"]["service_count"], 2)
|
||||
self.assertEqual(self.curated["compose_stack"]["main_service"], "front")
|
||||
self.assertEqual(self.services["front"]["image"], "tombursch/kitchenowl-web:latest")
|
||||
self.assertEqual(self.services["back"]["image"], "tombursch/kitchenowl-backend:latest")
|
||||
self.assertEqual(self.services["front"]["depends_on"], ["back"])
|
||||
self.assertEqual(self.curated["compose_stack"]["orchestration"]["start_order"], ["back", "front"])
|
||||
self.assertTrue(self.services["front"]["frontend_network"])
|
||||
self.assertTrue(self.services["front"]["private_network"])
|
||||
self.assertFalse(self.services["back"]["frontend_network"])
|
||||
self.assertTrue(self.services["back"]["private_network"])
|
||||
self.assertNotIn("postgres", self.services)
|
||||
self.assertNotIn("redis", self.services)
|
||||
|
||||
def test_frontend_uses_the_official_backend_alias_and_port(self):
|
||||
# BACK_URL is inherited from the official web image (back:5000); leaving
|
||||
# the override absent preserves the image default and Compose service alias.
|
||||
self.assertNotIn("BACK_URL", self.services["front"]["compose"].get("environment", {}))
|
||||
image_policy = self.curated["proxmox"]["image_metadata_policy"]
|
||||
self.assertEqual(image_policy["environment"], "import-image-env-then-apply-compose-overrides")
|
||||
self.assertEqual(self.curated["container_contract"]["ports"][0]["container_port"], 80)
|
||||
|
||||
def test_backend_data_and_generated_jwt_key_are_preserved_safely(self):
|
||||
back = self.services["back"]["compose"]
|
||||
self.assertEqual(back["volumes"][0]["target"], "/data")
|
||||
self.assertEqual(back["environment"]["JWT_SECRET_KEY"], "${GENERATED_JWT_SECRET_KEY}")
|
||||
self.assertNotIn("PLEASE_CHANGE_ME", json.dumps(back))
|
||||
secret = self.curated["compose_stack"]["installer_inputs"]["generated_secrets"]
|
||||
self.assertEqual(len(secret), 1)
|
||||
self.assertEqual(secret[0]["bindings"], [{"service": "back", "environment_variable": "JWT_SECRET_KEY"}])
|
||||
volume = next(v for v in self.curated["compose_stack"]["storage"] if v["service"] == "back")
|
||||
self.assertEqual(volume["container_path"], "/data")
|
||||
self.assertEqual(volume["mode"], "managed-volume")
|
||||
self.assertTrue(volume["user_selectable"])
|
||||
self.assertTrue(volume["backup"])
|
||||
|
||||
def test_image_healthchecks_and_first_run_setup_are_recorded(self):
|
||||
self.assertIn("uwsgi_curl localhost:5000 /api/health/", str(self.services["back"]["compose"]["healthcheck"]))
|
||||
self.assertIn("curl -f http://localhost/", str(self.services["front"]["compose"]["healthcheck"]))
|
||||
self.assertEqual(self.curated["first_run"]["endpoints"][0]["port"], 80)
|
||||
self.assertEqual(self.curated["first_run"]["credentials"], [])
|
||||
self.assertTrue(self.curated["proxmox"]["stack_completion_notes"])
|
||||
|
||||
def test_candidate_passes_stack_assessment_and_round_trips_through_catalog_reader(self):
|
||||
catalog = Catalog(ROOT)
|
||||
template = catalog.compose("kitchenowl")
|
||||
self.assertEqual(assess(template), [])
|
||||
self.assertEqual(template["compose_stack"]["project_name"], "kitchenowl")
|
||||
reloaded = template_from_oci_compose(json.dumps(template))
|
||||
expected = dict(template)
|
||||
# Reading a pasted/shared recipe deliberately resets trust status; the
|
||||
# catalog reapplies laboratory verification only for its own entry.
|
||||
expected["status"] = "generated-unvalidated"
|
||||
self.assertEqual(reloaded, expected)
|
||||
|
||||
def test_compiled_plan_keeps_private_backend_health_and_persistent_data(self):
|
||||
template = Catalog(ROOT).compose("kitchenowl")
|
||||
plan = compile_stack(template)
|
||||
self.assertEqual(plan["stack_name"], "kitchenowl")
|
||||
self.assertEqual(plan["network"]["private_allocation"], "automatic")
|
||||
self.assertEqual(plan["network"]["private_bridge"], "vmbr10")
|
||||
|
||||
services = {service["name"]: service for service in plan["services"]}
|
||||
self.assertEqual(list(services), ["back", "front"])
|
||||
back = services["back"]
|
||||
front = services["front"]
|
||||
self.assertIn("back", back["aliases"])
|
||||
self.assertIn("front", front["aliases"])
|
||||
self.assertNotIn("frontend", back)
|
||||
self.assertTrue(front["frontend"])
|
||||
|
||||
backend_health = back["healthcheck"]
|
||||
self.assertEqual(backend_health["type"], "exec")
|
||||
self.assertEqual(backend_health["timeout_seconds"], 189)
|
||||
self.assertIn("uwsgi_curl", backend_health["argv"][-1])
|
||||
frontend_health = front["healthcheck"]
|
||||
self.assertEqual(frontend_health["type"], "exec")
|
||||
self.assertEqual(frontend_health["timeout_seconds"], 99)
|
||||
self.assertIn("curl -f http://localhost/", frontend_health["argv"][-1])
|
||||
self.assertEqual(frontend_health["endpoint"], {"scheme": "http", "port": 80, "path": "/"})
|
||||
|
||||
self.assertEqual([mount["container_path"] for mount in back["deployment"]["mounts"]], ["/data"])
|
||||
self.assertEqual(back["deployment"]["mounts"][0]["type"], "managed-volume")
|
||||
self.assertTrue(back["deployment"]["mounts"][0]["backup"])
|
||||
self.assertEqual(front["deployment"]["mounts"], [])
|
||||
preparation = back["template"]["proxmox"]["installer_profile"]["volume_preparations"]
|
||||
self.assertEqual(preparation[0]["container_path"], "/data")
|
||||
self.assertEqual(preparation[0]["only_when_mount_type"], "managed-volume")
|
||||
self.assertEqual(template["compose_stack"]["storage"][0]["service"], "back")
|
||||
self.assertFalse(template["compose_stack"]["storage"][0]["shared_with_other_lxc"])
|
||||
|
||||
def test_icon_architectures_and_unvalidated_status_are_accurate(self):
|
||||
ui = self.curated["catalog_ui"]
|
||||
self.assertEqual(ui["icon"], "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/kitchenowl.webp")
|
||||
self.assertEqual(ui["architectures"], ["amd64", "arm64"])
|
||||
self.assertEqual(self.curated["status"], "generated-unvalidated")
|
||||
self.assertFalse(self.curated["compatibility"]["validated"])
|
||||
entry = next(app for app in self.index["applications"] if app["id"] == "kitchenowl")
|
||||
self.assertEqual(entry["template"], "apps/kitchenowl.json")
|
||||
self.assertTrue(entry["multi_container"])
|
||||
self.assertEqual(entry["category"], "productivity")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,135 @@
|
||||
"""Keep the native Krusader OCI profile aligned with its upstream image."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from proxmenux_oci.catalog import Catalog
|
||||
|
||||
|
||||
class KrusaderRecipeTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.curated = json.loads(
|
||||
(ROOT / "catalog/curated/krusader.json").read_text(encoding="utf-8")
|
||||
)
|
||||
cls.app = json.loads(
|
||||
(ROOT / "catalog/apps/krusader.json").read_text(encoding="utf-8")
|
||||
)
|
||||
cls.template = Catalog(ROOT).compose("krusader")
|
||||
|
||||
def test_catalog_copy_matches_curated_profile(self):
|
||||
self.assertEqual(self.curated, self.app)
|
||||
|
||||
def test_upstream_image_and_manifest_architectures_are_recorded(self):
|
||||
contract = self.curated["container_contract"]
|
||||
self.assertEqual(contract["image"]["reference"], "jlesage/krusader:latest")
|
||||
self.assertEqual(self.curated["source"]["provider"], "jlesage")
|
||||
self.assertEqual(self.curated["catalog_ui"]["architectures"], ["amd64", "arm64"])
|
||||
|
||||
def test_web_gui_uses_authenticated_https_and_does_not_expose_vnc(self):
|
||||
env = {
|
||||
entry["name"]: entry.get("example")
|
||||
for entry in self.template["container_contract"]["environment"]
|
||||
}
|
||||
self.assertEqual(env["WEB_AUTHENTICATION"], "1")
|
||||
self.assertEqual(env["SECURE_CONNECTION"], "1")
|
||||
self.assertEqual(env["VNC_LOCALHOST_ONLY"], "1")
|
||||
self.assertEqual(
|
||||
self.template["catalog_ui"]["launch"],
|
||||
{"scheme": "https", "port": 5800, "path": "/"},
|
||||
)
|
||||
self.assertEqual(
|
||||
[port["container_port"] for port in self.template["container_contract"]["ports"]],
|
||||
[5800],
|
||||
)
|
||||
credential = self.template["first_run"]["credentials"][0]
|
||||
self.assertEqual(credential["username"], "admin")
|
||||
self.assertEqual(credential["password_environment"], "WEB_AUTHENTICATION_PASSWORD")
|
||||
self.assertIn(
|
||||
"WEB_AUTHENTICATION_PASSWORD",
|
||||
self.template["proxmox"]["installer_profile"]["generated_sensitive_environment"],
|
||||
)
|
||||
|
||||
def test_only_config_and_selected_storage_are_persistent(self):
|
||||
volumes = {
|
||||
volume["container_path"]: volume
|
||||
for volume in self.template["container_contract"]["volumes"]
|
||||
}
|
||||
self.assertEqual(set(volumes), {"/config", "/storage"})
|
||||
self.assertEqual(volumes["/config"]["installation_choice"], ["managed-volume"])
|
||||
self.assertEqual(volumes["/storage"]["installation_choice"], ["managed-volume", "host-bind"])
|
||||
self.assertEqual(volumes["/storage"]["default"], "host-bind")
|
||||
self.assertTrue(all(volume["managed_volume"]["backup"] for volume in volumes.values()))
|
||||
self.assertEqual(volumes["/config"]["managed_volume"]["default_size_gb"], 4)
|
||||
compose = self.template["compose_stack"]["services"][0]["compose"]
|
||||
self.assertEqual(compose["image"], "jlesage/krusader:latest")
|
||||
|
||||
def test_storage_directory_offers_host_bind_with_the_image_user_identity(self):
|
||||
profile = self.template["proxmox"]["installer_profile"]
|
||||
storage = next(
|
||||
item for item in self.template["container_contract"]["volumes"]
|
||||
if item["container_path"] == "/storage"
|
||||
)
|
||||
compose_storage = next(
|
||||
item for item in self.template["compose_stack"]["storage"]
|
||||
if item["container_path"] == "/storage"
|
||||
)
|
||||
|
||||
self.assertEqual(storage["installation_choice"], ["managed-volume", "host-bind"])
|
||||
self.assertEqual(storage["default"], "host-bind")
|
||||
self.assertTrue(compose_storage["user_selectable"])
|
||||
self.assertEqual(compose_storage["mode"], "user-selectable")
|
||||
self.assertEqual(
|
||||
compose_storage["installation_choice"], ["managed-volume", "host-bind"]
|
||||
)
|
||||
self.assertEqual(
|
||||
profile["volume_owner"], {"uid": 1000, "gid": 1000}
|
||||
)
|
||||
self.assertEqual(
|
||||
{entry["name"]: entry["example"] for entry in self.template["container_contract"]["environment"]}
|
||||
["USER_ID"],
|
||||
"1000",
|
||||
)
|
||||
self.assertEqual(
|
||||
{entry["name"]: entry["example"] for entry in self.template["container_contract"]["environment"]}
|
||||
["GROUP_ID"],
|
||||
"1000",
|
||||
)
|
||||
|
||||
def test_partial_live_evidence_does_not_mark_app_as_fully_verified(self):
|
||||
self.assertEqual(
|
||||
self.curated["validation"]["clean_install"], "passed-first-start"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.curated["validation"]["service_health"],
|
||||
"passed-login-page-and-authenticated-session-http-200",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.curated["validation"]["credentials"],
|
||||
"passed-generated-password-login-and-session-cookie",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.curated["validation"]["web_authentication"],
|
||||
"passed-generated-credential-post-and-session-cookie",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.curated["validation"]["restart_persistence"],
|
||||
"passed-container-restart-and-host-bind-write-check",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.curated["validation"]["browser_visual"],
|
||||
"pending-self-signed-certificate-review",
|
||||
)
|
||||
self.assertEqual(self.curated["validation"]["backup_restore"], "pending")
|
||||
self.assertEqual(self.curated["validation"]["update_preserves_data"], "pending")
|
||||
self.assertEqual(self.curated["status"], "generated-unvalidated")
|
||||
self.assertFalse(self.curated["compatibility"]["validated"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,140 @@
|
||||
"""Spoolman uses its upstream single-image SQLite deployment in OCI Manager."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from proxmenux_oci.catalog import Catalog
|
||||
from proxmenux_oci.installer import DEFAULT_MODE, build_deployment
|
||||
|
||||
|
||||
class StorageChoiceUI:
|
||||
def __init__(self, volume_mode):
|
||||
self.volume_mode = volume_mode
|
||||
|
||||
def choose(self, _prompt, options, default=None, **_kwargs):
|
||||
available = {key for key, _label in options}
|
||||
if self.volume_mode not in available:
|
||||
raise AssertionError(f"{self.volume_mode} was not offered; got {available}")
|
||||
return self.volume_mode
|
||||
|
||||
def ask(self, _prompt, default="", **_kwargs):
|
||||
return default
|
||||
|
||||
def confirm(self, _prompt, default=False):
|
||||
return default
|
||||
|
||||
def message(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class SpoolmanRecipeTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.catalog = Catalog(ROOT)
|
||||
self.recipe = self.catalog.compose("spoolman")
|
||||
self.generated = json.loads(
|
||||
(ROOT / "catalog/apps/spoolman.json").read_text(encoding="utf-8")
|
||||
)
|
||||
self.curated = json.loads(
|
||||
(ROOT / "catalog/curated/spoolman.json").read_text(encoding="utf-8")
|
||||
)
|
||||
|
||||
def test_generated_and_curated_recipes_match(self):
|
||||
self.assertEqual(self.generated, self.curated)
|
||||
|
||||
def test_official_image_and_catalog_architectures_match_engine_support(self):
|
||||
image = self.recipe["container_contract"]["image"]
|
||||
self.assertEqual(image["reference"], "ghcr.io/donkie/spoolman:latest")
|
||||
self.assertEqual(image["registry"], "ghcr.io")
|
||||
self.assertEqual(image["repository"], "ghcr.io/donkie/spoolman")
|
||||
# The OCI catalog currently supports amd64 and arm64. The upstream
|
||||
# manifest also has arm/v7, which the current catalog engine does not
|
||||
# offer as an installable architecture.
|
||||
self.assertEqual(self.recipe["catalog_ui"]["architectures"], ["amd64", "arm64"])
|
||||
self.assertEqual(
|
||||
self.recipe["source"]["image_repository_url"],
|
||||
"https://github.com/Donkie/Spoolman/pkgs/container/spoolman",
|
||||
)
|
||||
|
||||
def test_sqlite_is_the_default_and_no_database_service_is_required(self):
|
||||
contract = self.recipe["container_contract"]
|
||||
self.assertEqual(self.recipe["compose_stack"]["service_count"], 1)
|
||||
self.assertEqual(self.recipe["compose_stack"]["services"][0]["image"],
|
||||
"ghcr.io/donkie/spoolman:latest")
|
||||
self.assertEqual(contract["related_services"], [])
|
||||
env_names = {item["name"] for item in contract["environment"]}
|
||||
self.assertNotIn("SPOOLMAN_DB_TYPE", env_names)
|
||||
self.assertNotIn("SPOOLMAN_DB_NAME", env_names)
|
||||
self.assertNotIn("SPOOLMAN_DIR_DATA", env_names)
|
||||
data_path = "/home/app/.local/share/spoolman"
|
||||
self.assertEqual([volume["container_path"] for volume in contract["volumes"]],
|
||||
[data_path])
|
||||
self.assertIn("SQLite", self.recipe["catalog_ui"]["description"]["en_US"])
|
||||
|
||||
def test_web_launch_and_first_start_healthcheck_use_container_port_8000(self):
|
||||
self.assertEqual(self.recipe["container_contract"]["ports"][0]["container_port"], 8000)
|
||||
self.assertEqual(self.recipe["catalog_ui"]["launch"]["port"], 8000)
|
||||
self.assertEqual(self.recipe["first_run"]["endpoints"][0]["port"], 8000)
|
||||
health = self.recipe["proxmox"]["installer_profile"]["startup_healthcheck"]
|
||||
self.assertEqual((health["scheme"], health["port"], health["path"]),
|
||||
("http", 8000, "/"))
|
||||
self.assertIn("7912:8000", self.recipe["container_contract"]["original_compose"])
|
||||
|
||||
def test_persistent_data_volume_and_application_uid_are_declared(self):
|
||||
path = "/home/app/.local/share/spoolman"
|
||||
volume = self.recipe["container_contract"]["volumes"][0]
|
||||
policy = json.loads(
|
||||
(ROOT / "catalog/volume-policy.json").read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertIn(path, policy["applications"]["spoolman"])
|
||||
self.assertEqual(volume["installation_choice"], ["managed-volume", "host-bind"])
|
||||
self.assertEqual(volume["default"], "managed-volume")
|
||||
preparation = self.recipe["proxmox"]["installer_profile"]["volume_preparations"][0]
|
||||
self.assertEqual(preparation["container_path"], path)
|
||||
self.assertEqual(preparation["owner_strategy"], "mapped-application-user")
|
||||
self.assertEqual(preparation["only_when_mount_type"], "managed-volume")
|
||||
self.assertEqual(self.recipe["proxmox"]["installer_profile"]["volume_owner"],
|
||||
{"uid": 1000, "gid": 1000})
|
||||
|
||||
def test_default_install_preserves_data_for_both_storage_choices(self):
|
||||
for mode in ("managed-volume", "host-bind"):
|
||||
with self.subTest(mode=mode):
|
||||
with patch("proxmenux_oci.installer.ask_storage",
|
||||
return_value="local-zfs"), \
|
||||
patch("proxmenux_oci.installer.host.default_bridge",
|
||||
return_value="vmbr0"), \
|
||||
patch("proxmenux_oci.installer.host.timezone",
|
||||
return_value="UTC"), \
|
||||
patch("proxmenux_oci.installer.access.ask_ipv4",
|
||||
return_value=("dhcp", None)):
|
||||
plan = build_deployment(
|
||||
self.recipe, StorageChoiceUI(mode), DEFAULT_MODE
|
||||
)
|
||||
|
||||
self.assertEqual(len(plan["mounts"]), 1)
|
||||
mount = plan["mounts"][0]
|
||||
self.assertEqual(mount["container_path"],
|
||||
"/home/app/.local/share/spoolman")
|
||||
self.assertEqual(mount["type"], mode)
|
||||
self.assertFalse(mount["read_only"])
|
||||
if mode == "managed-volume":
|
||||
self.assertEqual(mount["source"], "local-zfs")
|
||||
self.assertEqual(mount["size_gb"], 4)
|
||||
self.assertTrue(mount["backup"])
|
||||
else:
|
||||
self.assertEqual(
|
||||
mount["source"],
|
||||
"/mnt/oci-shared/spoolman/home-app-local-share-spoolman",
|
||||
)
|
||||
self.assertIsNone(mount["size_gb"])
|
||||
self.assertFalse(mount["backup"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,128 @@
|
||||
"""Guard the Unbound catalog contract against upstream image behavior."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
sys.path.insert(0, str(ROOT / "remote"))
|
||||
|
||||
from proxmenux_oci.catalog import Catalog
|
||||
import oci_instance_transaction
|
||||
|
||||
|
||||
class UnboundRecipeTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.curated = json.loads((ROOT / "catalog/curated/unbound.json").read_text(encoding="utf-8"))
|
||||
cls.app = json.loads((ROOT / "catalog/apps/unbound.json").read_text(encoding="utf-8"))
|
||||
cls.composed = Catalog(ROOT).compose("unbound")
|
||||
cls.index = json.loads((ROOT / "catalog/index.json").read_text(encoding="utf-8"))
|
||||
cls.index_entry = next(item for item in cls.index["applications"] if item["id"] == "unbound")
|
||||
cls.original_compose = yaml.safe_load(cls.curated["container_contract"]["original_compose"])
|
||||
|
||||
def test_both_catalog_templates_and_image_metadata_match_upstream(self):
|
||||
self.assertEqual(self.curated, self.app)
|
||||
image = self.curated["container_contract"]["image"]
|
||||
self.assertEqual(image["reference"], "klutchell/unbound:main")
|
||||
self.assertEqual(image["registry"], "docker.io")
|
||||
self.assertEqual(image["repository"], "klutchell/unbound")
|
||||
self.assertEqual(image["tag"], "main")
|
||||
self.assertIsNone(image["digest"]) # resolve the selected tag for each host architecture at install time
|
||||
self.assertEqual(
|
||||
image["pull_policy"],
|
||||
"resolve-selected-tag-to-architecture-digest-at-install",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.curated["source"]["repository"],
|
||||
"https://github.com/klutchell/unbound-docker",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.curated["source"]["revision"],
|
||||
"1efbea960bf9462b6fb6dd71aa702ad32d6820d7",
|
||||
)
|
||||
|
||||
def test_icon_and_supported_architectures_are_in_both_catalog_views(self):
|
||||
icon = "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/unbound.webp"
|
||||
self.assertEqual(self.curated["catalog_ui"]["icon"], icon)
|
||||
self.assertEqual(self.index_entry["icon"], icon)
|
||||
self.assertEqual(self.index_entry["template_id"], "image-unbound")
|
||||
# The upstream manifest has amd64, arm64, arm/v7 and arm/v6. The catalog
|
||||
# normalizes the two 32-bit ARM variants to armhf; the install index
|
||||
# only lists the architectures currently supported by ProxMenux.
|
||||
self.assertEqual(self.curated["catalog_ui"]["architectures"], ["amd64", "arm64", "armhf"])
|
||||
self.assertEqual(self.index_entry["architectures"], ["amd64", "arm64"])
|
||||
|
||||
def test_dns_listener_publishes_tcp_and_udp_53(self):
|
||||
expected = {("tcp", 53), ("udp", 53)}
|
||||
contract_ports = {
|
||||
(port["protocol"], port["container_port"])
|
||||
for port in self.curated["container_contract"]["ports"]
|
||||
}
|
||||
self.assertEqual(contract_ports, expected)
|
||||
|
||||
original = self.original_compose["services"]["unbound"]
|
||||
self.assertEqual(set(original["ports"]), {"53:53/tcp", "53:53/udp"})
|
||||
profile_service = self.curated["compose_stack"]["services"][0]["compose"]
|
||||
self.assertEqual(set(profile_service["ports"]), {"53:53/tcp", "53:53/udp"})
|
||||
|
||||
def test_upstream_entrypoint_is_preserved_and_health_uses_running_fallback(self):
|
||||
image_policy = self.curated["proxmox"]["image_metadata_policy"]
|
||||
self.assertEqual(image_policy["entrypoint"], "import-from-oci-image")
|
||||
self.assertEqual(image_policy["cmd"], "import-from-oci-image")
|
||||
|
||||
# Upstream Dockerfile sets ENTRYPOINT ["unbound"] and no HEALTHCHECK.
|
||||
# Preserve the image command; check process stability on install/update,
|
||||
# then verify DNS answers separately with a query to port 53.
|
||||
for service in (self.original_compose["services"]["unbound"],
|
||||
self.curated["compose_stack"]["services"][0]["compose"]):
|
||||
self.assertNotIn("entrypoint", service)
|
||||
self.assertNotIn("command", service)
|
||||
self.assertNotIn("healthcheck", service)
|
||||
self.assertEqual(self.curated["catalog_ui"]["launch"]["port"], None)
|
||||
self.assertEqual(self.curated["first_run"]["endpoints"], [])
|
||||
check = self.composed["proxmox"]["installer_profile"]["startup_healthcheck"]
|
||||
self.assertEqual(check, {"type": "running", "timeout_seconds": 90, "stability_seconds": 20})
|
||||
self.assertEqual(oci_instance_transaction.effective_healthcheck(self.composed), check)
|
||||
|
||||
def test_custom_configuration_mount_is_persistent_and_exposes_both_storage_modes(self):
|
||||
path = "/etc/unbound/custom.conf.d"
|
||||
volume = next(v for v in self.curated["container_contract"]["volumes"]
|
||||
if v["container_path"] == path)
|
||||
self.assertFalse(volume["read_only"])
|
||||
self.assertFalse(volume["required"])
|
||||
self.assertEqual(volume["installation_choice"], ["managed-volume", "host-bind"])
|
||||
self.assertEqual(volume["default"], "host-bind")
|
||||
self.assertTrue(volume["managed_volume"]["backup"])
|
||||
|
||||
original_service = self.original_compose["services"]["unbound"]
|
||||
self.assertIn("./config:/etc/unbound/custom.conf.d", original_service["volumes"])
|
||||
profile_service = self.curated["compose_stack"]["services"][0]["compose"]
|
||||
self.assertIn("./config:/etc/unbound/custom.conf.d", profile_service["volumes"])
|
||||
|
||||
storage = next(v for v in self.curated["compose_stack"]["storage"]
|
||||
if v["container_path"] == path)
|
||||
self.assertEqual(storage["mode"], "user-selectable")
|
||||
self.assertEqual(storage["installation_choice"], ["managed-volume", "host-bind"])
|
||||
self.assertTrue(storage["backup"])
|
||||
|
||||
prepared = next(v for v in self.composed["proxmox"]["installer_profile"]["volume_preparations"]
|
||||
if v["container_path"] == path)
|
||||
self.assertEqual(prepared["owner_strategy"], "mapped-application-user")
|
||||
|
||||
def test_dns_service_remains_unprivileged_and_data_is_not_misrepresented_as_web_ui(self):
|
||||
self.assertTrue(self.curated["proxmox"]["defaults"]["unprivileged"])
|
||||
self.assertFalse(self.index_entry["requires_privileged_lxc"])
|
||||
self.assertEqual(self.curated["catalog_ui"]["launch"]["port"], None)
|
||||
self.assertTrue(any("no web interface" in tip.casefold()
|
||||
for tip in self.curated["catalog_ui"]["tips"]))
|
||||
self.assertTrue(any("do not expose recursive dns publicly" in tip.casefold()
|
||||
for tip in self.curated["catalog_ui"]["tips"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Z-Wave JS UI's native OCI profile is safe to start without a radio controller."""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
sys.path.insert(0, str(ROOT / "remote"))
|
||||
|
||||
from proxmenux_oci.catalog import Catalog, supported_architectures
|
||||
import oci_instance_transaction as transaction
|
||||
|
||||
|
||||
class ZWaveJsUIRecipeTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.catalog = Catalog(ROOT)
|
||||
self.curated_path = ROOT / "catalog/curated/zwave-js-ui.json"
|
||||
self.generated_path = ROOT / "catalog/apps/zwave-js-ui.json"
|
||||
|
||||
def test_generated_template_matches_curated_profile(self):
|
||||
self.assertEqual(self.curated_path.read_bytes(), self.generated_path.read_bytes())
|
||||
|
||||
def test_uses_the_official_image_and_only_catalog_supported_architectures(self):
|
||||
template = self.catalog.compose("zwave-js-ui")
|
||||
self.assertEqual(template["id"], "image-zwave-js-ui")
|
||||
self.assertEqual(
|
||||
template["container_contract"]["image"]["reference"],
|
||||
"docker.io/zwavejs/zwave-js-ui:latest",
|
||||
)
|
||||
self.assertEqual(
|
||||
template["catalog_ui"]["architectures"], ["amd64", "arm64"]
|
||||
)
|
||||
self.assertEqual(
|
||||
supported_architectures(template["catalog_ui"]["architectures"]),
|
||||
["amd64", "arm64"],
|
||||
)
|
||||
self.assertIn(
|
||||
"/c38e0d2cbbb826bf2bab2db596a3452e59ce0e8e/public/apple-touch-icon-180x180.png",
|
||||
template["catalog_ui"]["icon"],
|
||||
)
|
||||
|
||||
def test_web_ui_is_the_startup_check_when_no_controller_is_attached(self):
|
||||
template = self.catalog.compose("zwave-js-ui")
|
||||
check = transaction.effective_healthcheck(template)
|
||||
self.assertEqual((check["scheme"], check["port"], check["path"]),
|
||||
("http", 8091, "/"))
|
||||
self.assertNotEqual(check["path"], "/health")
|
||||
endpoint = template["first_run"]["endpoints"][0]
|
||||
self.assertEqual((endpoint["port"], endpoint["path"]), (8091, "/"))
|
||||
devices = template["proxmox"]["installer_profile"]["optional_devices"]
|
||||
self.assertEqual(len(devices), 1)
|
||||
self.assertFalse(devices[0]["enabled_default"])
|
||||
self.assertFalse(devices[0]["required_by_compose"])
|
||||
self.assertTrue(devices[0]["host_path_default"].startswith("/dev/serial/by-id/"))
|
||||
|
||||
def test_fresh_store_enables_login_and_keeps_generated_credentials_out_of_recipe(self):
|
||||
template = self.catalog.compose("zwave-js-ui")
|
||||
profile = template["proxmox"]["installer_profile"]
|
||||
generated = profile["generated_sensitive_environment"]
|
||||
self.assertEqual(generated["SESSION_SECRET"]["bytes"], 32)
|
||||
self.assertEqual(generated["DEFAULT_PASSWORD"]["bytes"], 20)
|
||||
seeded = next(item for item in profile["generated_files"]
|
||||
if item["container_path"] == "/usr/src/app/store/settings.json")
|
||||
self.assertTrue(seeded["only_if_missing"])
|
||||
self.assertTrue(seeded["owner"] == "mapped-root")
|
||||
self.assertIn('"authEnabled":true', seeded["content"])
|
||||
credential = template["first_run"]["credentials"][0]
|
||||
self.assertEqual(credential["username"], "admin")
|
||||
self.assertIsNone(credential["password"])
|
||||
self.assertEqual(credential["password_environment"], "DEFAULT_PASSWORD")
|
||||
self.assertNotIn("mysupersecretkey", template["container_contract"]["original_compose"])
|
||||
|
||||
def test_state_is_persisted_on_a_managed_volume(self):
|
||||
template = self.catalog.compose("zwave-js-ui")
|
||||
volume = template["container_contract"]["volumes"][0]
|
||||
self.assertEqual(volume["container_path"], "/usr/src/app/store")
|
||||
self.assertEqual(volume["installation_choice"], ["managed-volume"])
|
||||
self.assertTrue(volume["managed_volume"]["backup"])
|
||||
prep = template["proxmox"]["installer_profile"]["volume_preparations"]
|
||||
self.assertEqual(prep[0]["container_path"], "/usr/src/app/store")
|
||||
self.assertEqual(prep[0]["owner_strategy"], "mapped-root")
|
||||
|
||||
def test_includes_web_and_optional_zwave_websocket_ports(self):
|
||||
template = self.catalog.compose("zwave-js-ui")
|
||||
ports = {item["container_port"]: item
|
||||
for item in template["container_contract"]["ports"]}
|
||||
self.assertTrue(ports[8091]["required"])
|
||||
self.assertFalse(ports[3000]["required"])
|
||||
self.assertEqual(ports[8091]["protocol"], "tcp")
|
||||
self.assertEqual(ports[3000]["protocol"], "tcp")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user