mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
feat(oci): add ShareCoveX to the catalog and ask for the directories it shares
ShareCoveX is a file server with a web panel that publishes directories over SMB, NFS or both. Its recipe declares that it shares directories, so the installation asks for them one after another, in the default mode as well: each is a container volume or a directory of the host, mounted under /shares with the name given to it. Modify offers to add more. The advanced installation also offers the privileged profile NFSv4 needs; the default one never asks and stays unprivileged. A secret can declare a minimum length, and the recipe opens the Files & Sharing category.
This commit is contained in:
@@ -0,0 +1,554 @@
|
||||
{
|
||||
"schema_version": "0.5.0",
|
||||
"kind": "proxmenux.oci-template",
|
||||
"id": "image-sharecovex",
|
||||
"status": "generated-unvalidated",
|
||||
"catalog_ui": {
|
||||
"title": {
|
||||
"en_US": "ShareCoveX"
|
||||
},
|
||||
"tagline": {
|
||||
"en_US": "SMB, NFS and Time Machine shares from a web panel"
|
||||
},
|
||||
"description": {
|
||||
"en_US": "File-sharing appliance with a web panel: publish directories over SMB, NFS or both, with Samba users, client networks and Time Machine destinations. Each directory to share is a container volume or a directory of the host, mounted under /shares."
|
||||
},
|
||||
"category": "sharing",
|
||||
"category_label": "Files & Sharing",
|
||||
"author": "MacRimi",
|
||||
"developer": "MacRimi",
|
||||
"icon": "https://raw.githubusercontent.com/MacRimi/ShareCoveX/refs/heads/main/assets/sharecovex-icon.svg",
|
||||
"thumbnail": null,
|
||||
"screenshots": [],
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"launch": {
|
||||
"scheme": "http",
|
||||
"port": 8080,
|
||||
"path": "/"
|
||||
},
|
||||
"website": "https://github.com/MacRimi/ShareCoveX",
|
||||
"documentation": "https://github.com/MacRimi/ShareCoveX#readme",
|
||||
"repository": "https://github.com/MacRimi/ShareCoveX",
|
||||
"tips": [
|
||||
"Each directory to share is mounted under /shares with the name given to it during the installation; more can be added later with Modify.",
|
||||
"A directory of the host keeps the owner it already has. The panel shows whether the shared identity (1000:1000 by default) can write to it.",
|
||||
"Create the Samba users in Settings of the panel, then publish each directory from Resources.",
|
||||
"NFS is served as NFSv3. NFSv4 needs the privileged profile offered by the advanced installation.",
|
||||
"/config: Proxmox volume included in backup and kept when the application is updated or modified."
|
||||
],
|
||||
"mini_changelog": [],
|
||||
"display_version": null,
|
||||
"updated_at": "2026-10-08",
|
||||
"hidden": false
|
||||
},
|
||||
"source": {
|
||||
"provider": "MacRimi",
|
||||
"repository": "https://github.com/MacRimi/ShareCoveX",
|
||||
"revision": "d8499627f459559574118781ba78ff29b50b5083",
|
||||
"image_repository_url": "https://github.com/MacRimi/ShareCoveX/pkgs/container/sharecovex",
|
||||
"readme_pushed_at": "",
|
||||
"compose_sha256": "6629a9e2aa650c430aeecc66308029304d4ffde3be8a76a9b7dc665470c67f1e",
|
||||
"generated_at": "2026-10-08T19:09:08+00:00",
|
||||
"default_branch": "main"
|
||||
},
|
||||
"container_contract": {
|
||||
"service_name": "sharecovex",
|
||||
"container_name": "sharecovex",
|
||||
"image": {
|
||||
"reference": "ghcr.io/macrimi/sharecovex:latest",
|
||||
"registry": "ghcr.io",
|
||||
"repository": "ghcr.io/macrimi/sharecovex",
|
||||
"tag": "latest",
|
||||
"digest": null,
|
||||
"pull_policy": "resolve-written-reference-to-architecture-digest-at-install"
|
||||
},
|
||||
"environment": [
|
||||
{
|
||||
"name": "SHARECOVEX_ADMIN_PASSWORD",
|
||||
"example": "",
|
||||
"prompt": "Password of the ShareCoveX panel administrator (8 characters or more)",
|
||||
"prompt_user": true,
|
||||
"required": true,
|
||||
"sensitive": true,
|
||||
"source": "upstream-documentation"
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"id": "config",
|
||||
"container_path": "/config",
|
||||
"compose_source_example": "sharecovex-config",
|
||||
"read_only": false,
|
||||
"required": true,
|
||||
"installation_choice": [
|
||||
"managed-volume"
|
||||
],
|
||||
"default": "managed-volume",
|
||||
"managed_volume": {
|
||||
"backup": true,
|
||||
"default_size_gb": 16
|
||||
}
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
"container_port": 8080,
|
||||
"published_example": 8080,
|
||||
"protocol": "tcp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
},
|
||||
{
|
||||
"container_port": 445,
|
||||
"published_example": 445,
|
||||
"protocol": "tcp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
},
|
||||
{
|
||||
"container_port": 2049,
|
||||
"published_example": 2049,
|
||||
"protocol": "tcp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
},
|
||||
{
|
||||
"container_port": 20048,
|
||||
"published_example": 20048,
|
||||
"protocol": "tcp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
}
|
||||
],
|
||||
"related_services": [],
|
||||
"restart": "unless-stopped",
|
||||
"stop_grace_period": "30s",
|
||||
"original_compose": "name: sharecovex\nservices:\n sharecovex:\n image: ghcr.io/macrimi/sharecovex:latest\n container_name: sharecovex\n restart: unless-stopped\n stop_grace_period: 30s\n environment:\n SHARECOVEX_ADMIN_PASSWORD: ${GENERATED_SHARECOVEX_ADMIN_PASSWORD}\n ports:\n - 8080:8080/tcp\n - 445:445/tcp\n - 2049:2049/tcp\n - 20048:20048/tcp\n volumes:\n - sharecovex-config:/config\n healthcheck:\n test:\n - CMD\n - python3\n - -c\n - import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/healthz',\n timeout=4)\n interval: 30s\n timeout: 5s\n retries: 3\n start_period: 20s\n"
|
||||
},
|
||||
"compose_stack": {
|
||||
"project_name": "sharecovex",
|
||||
"deployment_model": "one-native-oci-lxc-per-compose-service",
|
||||
"user_experience": "single-application-install",
|
||||
"main_service": "sharecovex",
|
||||
"service_count": 1,
|
||||
"services": [
|
||||
{
|
||||
"name": "sharecovex",
|
||||
"image": "ghcr.io/macrimi/sharecovex:latest",
|
||||
"is_main": true,
|
||||
"role": "frontend",
|
||||
"vmid_offset": 0,
|
||||
"depends_on": [],
|
||||
"frontend_network": true,
|
||||
"private_network": false,
|
||||
"compose": {
|
||||
"image": "ghcr.io/macrimi/sharecovex:latest",
|
||||
"container_name": "sharecovex",
|
||||
"restart": "unless-stopped",
|
||||
"stop_grace_period": "30s",
|
||||
"environment": {
|
||||
"SHARECOVEX_ADMIN_PASSWORD": "${GENERATED_SHARECOVEX_ADMIN_PASSWORD}"
|
||||
},
|
||||
"ports": [
|
||||
"8080:8080/tcp",
|
||||
"445:445/tcp",
|
||||
"2049:2049/tcp",
|
||||
"20048:20048/tcp"
|
||||
],
|
||||
"volumes": [
|
||||
"sharecovex-config:/config"
|
||||
],
|
||||
"healthcheck": {
|
||||
"test": [
|
||||
"CMD",
|
||||
"python3",
|
||||
"-c",
|
||||
"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/healthz', timeout=4)"
|
||||
],
|
||||
"interval": "30s",
|
||||
"timeout": "5s",
|
||||
"retries": 3,
|
||||
"start_period": "20s"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"top_level": {
|
||||
"name": "sharecovex"
|
||||
},
|
||||
"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": "sharecovex-volume-0",
|
||||
"service": "sharecovex",
|
||||
"container_path": "/config",
|
||||
"mode": "managed-volume",
|
||||
"user_selectable": false,
|
||||
"backup": true,
|
||||
"shared_with_other_lxc": false,
|
||||
"source_path": null,
|
||||
"source_path_prompt": null
|
||||
}
|
||||
],
|
||||
"orchestration": {
|
||||
"reserve_vmids_atomically": 1,
|
||||
"start_order": [
|
||||
"sharecovex"
|
||||
],
|
||||
"stop_order": [
|
||||
"sharecovex"
|
||||
],
|
||||
"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": "sharecovex-admin-password",
|
||||
"strategy": "generate-cryptographically-random-at-install",
|
||||
"bindings": [
|
||||
{
|
||||
"service": "sharecovex",
|
||||
"environment_variable": "SHARECOVEX_ADMIN_PASSWORD"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"first_run": {
|
||||
"endpoints": [
|
||||
{
|
||||
"label": "ShareCoveX panel",
|
||||
"scheme": "http",
|
||||
"port": 8080,
|
||||
"path": "/",
|
||||
"source": "upstream-documentation"
|
||||
}
|
||||
],
|
||||
"credentials": [
|
||||
{
|
||||
"label": "ShareCoveX panel",
|
||||
"type": "configured-or-installer-generated",
|
||||
"username": "admin",
|
||||
"password": null,
|
||||
"password_environment": "SHARECOVEX_ADMIN_PASSWORD",
|
||||
"change_required": false,
|
||||
"source": "https://github.com/MacRimi/ShareCoveX#readme"
|
||||
}
|
||||
]
|
||||
},
|
||||
"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": 4,
|
||||
"rootfs_storage": "local-lvm",
|
||||
"volume_storage": "local-lvm",
|
||||
"template_storage": "local",
|
||||
"bridge": "vmbr0",
|
||||
"ipv4": "dhcp",
|
||||
"firewall": true,
|
||||
"host_managed_network": true,
|
||||
"onboot": true,
|
||||
"features": [],
|
||||
"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"
|
||||
},
|
||||
"installer_profile": {
|
||||
"startup_healthcheck": {
|
||||
"type": "http",
|
||||
"scheme": "http",
|
||||
"port": 8080,
|
||||
"path": "/healthz",
|
||||
"timeout_seconds": 110,
|
||||
"request_timeout_seconds": 5,
|
||||
"stability_seconds": 0,
|
||||
"verify_tls": true,
|
||||
"required": true,
|
||||
"source": "compose-healthcheck"
|
||||
},
|
||||
"volume_owner": {
|
||||
"uid": 1000,
|
||||
"gid": 1000
|
||||
},
|
||||
"volume_preparations": [
|
||||
{
|
||||
"container_path": "/config",
|
||||
"remove_lost_found": true,
|
||||
"owner_strategy": "mapped-root",
|
||||
"only_when_mount_type": "managed-volume"
|
||||
}
|
||||
],
|
||||
"generated_sensitive_environment": {
|
||||
"SHARECOVEX_ADMIN_PASSWORD": {
|
||||
"strategy": "token-hex",
|
||||
"bytes": 8
|
||||
}
|
||||
},
|
||||
"minimum_secret_length": {
|
||||
"SHARECOVEX_ADMIN_PASSWORD": 8
|
||||
},
|
||||
"shared_directories": {
|
||||
"container_root": "/shares",
|
||||
"reserved_names": [
|
||||
"config"
|
||||
],
|
||||
"default_size_gb": 32
|
||||
},
|
||||
"security": {
|
||||
"optional_relaxations": [
|
||||
{
|
||||
"id": "nfsv4",
|
||||
"enable_prompt": "NFSv4 needs a privileged container without AppArmor or seccomp confinement: a compromised image could affect the host. NFSv3, SMB and Time Machine work without it.\n\nPrepare this container for NFSv4?",
|
||||
"enabled_default": false,
|
||||
"privileged": true,
|
||||
"options": {
|
||||
"apparmor_profile": "unconfined",
|
||||
"seccomp_profile": "unconfined"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"completion_notes": [
|
||||
"Sign in to the panel and create a Samba user in Settings before publishing a directory over SMB.",
|
||||
"Publish each directory from Resources: open it, switch on SMB, NFS or both, and save."
|
||||
]
|
||||
},
|
||||
"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": "pending-per-application"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"catalog": {
|
||||
"replaces_discovered_ids": []
|
||||
}
|
||||
},
|
||||
"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": "pending",
|
||||
"service_health": "pending",
|
||||
"restart_persistence": "pending",
|
||||
"backup_restore": "pending",
|
||||
"update_preserves_data": "pending"
|
||||
},
|
||||
"lifecycle": {
|
||||
"update_strategy": "resolve-latest-image-and-replace-rootfs-preserving-managed-/config-and-shared-directories",
|
||||
"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
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
"network": "Network & Firewall",
|
||||
"documents": "Documents & Notes",
|
||||
"downloads": "Files & Downloads",
|
||||
"sharing": "Files & Sharing",
|
||||
"arr": "*Arr Suite",
|
||||
"smarthome": "IoT & Smart Home",
|
||||
"monitoring": "Monitoring & Analytics",
|
||||
@@ -156,6 +157,7 @@
|
||||
"scummvm": "gaming",
|
||||
"sealskin": "desktop",
|
||||
"shadps4": "gaming",
|
||||
"sharecovex": "sharing",
|
||||
"shotcut": "creative",
|
||||
"sickgear": "arr",
|
||||
"signal": "communication",
|
||||
|
||||
@@ -0,0 +1,554 @@
|
||||
{
|
||||
"schema_version": "0.5.0",
|
||||
"kind": "proxmenux.oci-template",
|
||||
"id": "image-sharecovex",
|
||||
"status": "generated-unvalidated",
|
||||
"catalog_ui": {
|
||||
"title": {
|
||||
"en_US": "ShareCoveX"
|
||||
},
|
||||
"tagline": {
|
||||
"en_US": "SMB, NFS and Time Machine shares from a web panel"
|
||||
},
|
||||
"description": {
|
||||
"en_US": "File-sharing appliance with a web panel: publish directories over SMB, NFS or both, with Samba users, client networks and Time Machine destinations. Each directory to share is a container volume or a directory of the host, mounted under /shares."
|
||||
},
|
||||
"category": "sharing",
|
||||
"category_label": "Files & Sharing",
|
||||
"author": "MacRimi",
|
||||
"developer": "MacRimi",
|
||||
"icon": "https://raw.githubusercontent.com/MacRimi/ShareCoveX/refs/heads/main/assets/sharecovex-icon.svg",
|
||||
"thumbnail": null,
|
||||
"screenshots": [],
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"launch": {
|
||||
"scheme": "http",
|
||||
"port": 8080,
|
||||
"path": "/"
|
||||
},
|
||||
"website": "https://github.com/MacRimi/ShareCoveX",
|
||||
"documentation": "https://github.com/MacRimi/ShareCoveX#readme",
|
||||
"repository": "https://github.com/MacRimi/ShareCoveX",
|
||||
"tips": [
|
||||
"Each directory to share is mounted under /shares with the name given to it during the installation; more can be added later with Modify.",
|
||||
"A directory of the host keeps the owner it already has. The panel shows whether the shared identity (1000:1000 by default) can write to it.",
|
||||
"Create the Samba users in Settings of the panel, then publish each directory from Resources.",
|
||||
"NFS is served as NFSv3. NFSv4 needs the privileged profile offered by the advanced installation.",
|
||||
"/config: Proxmox volume included in backup and kept when the application is updated or modified."
|
||||
],
|
||||
"mini_changelog": [],
|
||||
"display_version": null,
|
||||
"updated_at": "2026-10-08",
|
||||
"hidden": false
|
||||
},
|
||||
"source": {
|
||||
"provider": "MacRimi",
|
||||
"repository": "https://github.com/MacRimi/ShareCoveX",
|
||||
"revision": "d8499627f459559574118781ba78ff29b50b5083",
|
||||
"image_repository_url": "https://github.com/MacRimi/ShareCoveX/pkgs/container/sharecovex",
|
||||
"readme_pushed_at": "",
|
||||
"compose_sha256": "6629a9e2aa650c430aeecc66308029304d4ffde3be8a76a9b7dc665470c67f1e",
|
||||
"generated_at": "2026-10-08T19:09:08+00:00",
|
||||
"default_branch": "main"
|
||||
},
|
||||
"container_contract": {
|
||||
"service_name": "sharecovex",
|
||||
"container_name": "sharecovex",
|
||||
"image": {
|
||||
"reference": "ghcr.io/macrimi/sharecovex:latest",
|
||||
"registry": "ghcr.io",
|
||||
"repository": "ghcr.io/macrimi/sharecovex",
|
||||
"tag": "latest",
|
||||
"digest": null,
|
||||
"pull_policy": "resolve-written-reference-to-architecture-digest-at-install"
|
||||
},
|
||||
"environment": [
|
||||
{
|
||||
"name": "SHARECOVEX_ADMIN_PASSWORD",
|
||||
"example": "",
|
||||
"prompt": "Password of the ShareCoveX panel administrator (8 characters or more)",
|
||||
"prompt_user": true,
|
||||
"required": true,
|
||||
"sensitive": true,
|
||||
"source": "upstream-documentation"
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"id": "config",
|
||||
"container_path": "/config",
|
||||
"compose_source_example": "sharecovex-config",
|
||||
"read_only": false,
|
||||
"required": true,
|
||||
"installation_choice": [
|
||||
"managed-volume"
|
||||
],
|
||||
"default": "managed-volume",
|
||||
"managed_volume": {
|
||||
"backup": true,
|
||||
"default_size_gb": 16
|
||||
}
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
"container_port": 8080,
|
||||
"published_example": 8080,
|
||||
"protocol": "tcp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
},
|
||||
{
|
||||
"container_port": 445,
|
||||
"published_example": 445,
|
||||
"protocol": "tcp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
},
|
||||
{
|
||||
"container_port": 2049,
|
||||
"published_example": 2049,
|
||||
"protocol": "tcp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
},
|
||||
{
|
||||
"container_port": 20048,
|
||||
"published_example": 20048,
|
||||
"protocol": "tcp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
}
|
||||
],
|
||||
"related_services": [],
|
||||
"restart": "unless-stopped",
|
||||
"stop_grace_period": "30s",
|
||||
"original_compose": "name: sharecovex\nservices:\n sharecovex:\n image: ghcr.io/macrimi/sharecovex:latest\n container_name: sharecovex\n restart: unless-stopped\n stop_grace_period: 30s\n environment:\n SHARECOVEX_ADMIN_PASSWORD: ${GENERATED_SHARECOVEX_ADMIN_PASSWORD}\n ports:\n - 8080:8080/tcp\n - 445:445/tcp\n - 2049:2049/tcp\n - 20048:20048/tcp\n volumes:\n - sharecovex-config:/config\n healthcheck:\n test:\n - CMD\n - python3\n - -c\n - import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/healthz',\n timeout=4)\n interval: 30s\n timeout: 5s\n retries: 3\n start_period: 20s\n"
|
||||
},
|
||||
"compose_stack": {
|
||||
"project_name": "sharecovex",
|
||||
"deployment_model": "one-native-oci-lxc-per-compose-service",
|
||||
"user_experience": "single-application-install",
|
||||
"main_service": "sharecovex",
|
||||
"service_count": 1,
|
||||
"services": [
|
||||
{
|
||||
"name": "sharecovex",
|
||||
"image": "ghcr.io/macrimi/sharecovex:latest",
|
||||
"is_main": true,
|
||||
"role": "frontend",
|
||||
"vmid_offset": 0,
|
||||
"depends_on": [],
|
||||
"frontend_network": true,
|
||||
"private_network": false,
|
||||
"compose": {
|
||||
"image": "ghcr.io/macrimi/sharecovex:latest",
|
||||
"container_name": "sharecovex",
|
||||
"restart": "unless-stopped",
|
||||
"stop_grace_period": "30s",
|
||||
"environment": {
|
||||
"SHARECOVEX_ADMIN_PASSWORD": "${GENERATED_SHARECOVEX_ADMIN_PASSWORD}"
|
||||
},
|
||||
"ports": [
|
||||
"8080:8080/tcp",
|
||||
"445:445/tcp",
|
||||
"2049:2049/tcp",
|
||||
"20048:20048/tcp"
|
||||
],
|
||||
"volumes": [
|
||||
"sharecovex-config:/config"
|
||||
],
|
||||
"healthcheck": {
|
||||
"test": [
|
||||
"CMD",
|
||||
"python3",
|
||||
"-c",
|
||||
"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/healthz', timeout=4)"
|
||||
],
|
||||
"interval": "30s",
|
||||
"timeout": "5s",
|
||||
"retries": 3,
|
||||
"start_period": "20s"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"top_level": {
|
||||
"name": "sharecovex"
|
||||
},
|
||||
"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": "sharecovex-volume-0",
|
||||
"service": "sharecovex",
|
||||
"container_path": "/config",
|
||||
"mode": "managed-volume",
|
||||
"user_selectable": false,
|
||||
"backup": true,
|
||||
"shared_with_other_lxc": false,
|
||||
"source_path": null,
|
||||
"source_path_prompt": null
|
||||
}
|
||||
],
|
||||
"orchestration": {
|
||||
"reserve_vmids_atomically": 1,
|
||||
"start_order": [
|
||||
"sharecovex"
|
||||
],
|
||||
"stop_order": [
|
||||
"sharecovex"
|
||||
],
|
||||
"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": "sharecovex-admin-password",
|
||||
"strategy": "generate-cryptographically-random-at-install",
|
||||
"bindings": [
|
||||
{
|
||||
"service": "sharecovex",
|
||||
"environment_variable": "SHARECOVEX_ADMIN_PASSWORD"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"first_run": {
|
||||
"endpoints": [
|
||||
{
|
||||
"label": "ShareCoveX panel",
|
||||
"scheme": "http",
|
||||
"port": 8080,
|
||||
"path": "/",
|
||||
"source": "upstream-documentation"
|
||||
}
|
||||
],
|
||||
"credentials": [
|
||||
{
|
||||
"label": "ShareCoveX panel",
|
||||
"type": "configured-or-installer-generated",
|
||||
"username": "admin",
|
||||
"password": null,
|
||||
"password_environment": "SHARECOVEX_ADMIN_PASSWORD",
|
||||
"change_required": false,
|
||||
"source": "https://github.com/MacRimi/ShareCoveX#readme"
|
||||
}
|
||||
]
|
||||
},
|
||||
"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": 4,
|
||||
"rootfs_storage": "local-lvm",
|
||||
"volume_storage": "local-lvm",
|
||||
"template_storage": "local",
|
||||
"bridge": "vmbr0",
|
||||
"ipv4": "dhcp",
|
||||
"firewall": true,
|
||||
"host_managed_network": true,
|
||||
"onboot": true,
|
||||
"features": [],
|
||||
"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"
|
||||
},
|
||||
"installer_profile": {
|
||||
"startup_healthcheck": {
|
||||
"type": "http",
|
||||
"scheme": "http",
|
||||
"port": 8080,
|
||||
"path": "/healthz",
|
||||
"timeout_seconds": 110,
|
||||
"request_timeout_seconds": 5,
|
||||
"stability_seconds": 0,
|
||||
"verify_tls": true,
|
||||
"required": true,
|
||||
"source": "compose-healthcheck"
|
||||
},
|
||||
"volume_owner": {
|
||||
"uid": 1000,
|
||||
"gid": 1000
|
||||
},
|
||||
"volume_preparations": [
|
||||
{
|
||||
"container_path": "/config",
|
||||
"remove_lost_found": true,
|
||||
"owner_strategy": "mapped-root",
|
||||
"only_when_mount_type": "managed-volume"
|
||||
}
|
||||
],
|
||||
"generated_sensitive_environment": {
|
||||
"SHARECOVEX_ADMIN_PASSWORD": {
|
||||
"strategy": "token-hex",
|
||||
"bytes": 8
|
||||
}
|
||||
},
|
||||
"minimum_secret_length": {
|
||||
"SHARECOVEX_ADMIN_PASSWORD": 8
|
||||
},
|
||||
"shared_directories": {
|
||||
"container_root": "/shares",
|
||||
"reserved_names": [
|
||||
"config"
|
||||
],
|
||||
"default_size_gb": 32
|
||||
},
|
||||
"security": {
|
||||
"optional_relaxations": [
|
||||
{
|
||||
"id": "nfsv4",
|
||||
"enable_prompt": "NFSv4 needs a privileged container without AppArmor or seccomp confinement: a compromised image could affect the host. NFSv3, SMB and Time Machine work without it.\n\nPrepare this container for NFSv4?",
|
||||
"enabled_default": false,
|
||||
"privileged": true,
|
||||
"options": {
|
||||
"apparmor_profile": "unconfined",
|
||||
"seccomp_profile": "unconfined"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"completion_notes": [
|
||||
"Sign in to the panel and create a Samba user in Settings before publishing a directory over SMB.",
|
||||
"Publish each directory from Resources: open it, switch on SMB, NFS or both, and save."
|
||||
]
|
||||
},
|
||||
"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": "pending-per-application"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"catalog": {
|
||||
"replaces_discovered_ids": []
|
||||
}
|
||||
},
|
||||
"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": "pending",
|
||||
"service_health": "pending",
|
||||
"restart_persistence": "pending",
|
||||
"backup_restore": "pending",
|
||||
"update_preserves_data": "pending"
|
||||
},
|
||||
"lifecycle": {
|
||||
"update_strategy": "resolve-latest-image-and-replace-rootfs-preserving-managed-/config-and-shared-directories",
|
||||
"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
|
||||
}
|
||||
}
|
||||
@@ -9765,6 +9765,38 @@
|
||||
"hidden": false,
|
||||
"template_id": "linuxserver-shadps4"
|
||||
},
|
||||
{
|
||||
"id": "sharecovex",
|
||||
"provider": "MacRimi",
|
||||
"template_family": "curated-profile",
|
||||
"title": "ShareCoveX",
|
||||
"repository": "https://github.com/MacRimi/ShareCoveX",
|
||||
"description": "File-sharing appliance with a web panel: publish directories over SMB, NFS or both, with Samba users, client networks and Time Machine destinations. Each directory to share is a container volume or a directory of the host, mounted under /shares.",
|
||||
"website": "https://github.com/MacRimi/ShareCoveX",
|
||||
"icon": "https://raw.githubusercontent.com/MacRimi/ShareCoveX/refs/heads/main/assets/sharecovex-icon.svg",
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"updated_at": "2026-10-08",
|
||||
"main_image": "ghcr.io/macrimi/sharecovex:latest",
|
||||
"category": "sharing",
|
||||
"category_label": "Files & Sharing",
|
||||
"replaces_discovered_ids": [],
|
||||
"curated_path": "catalog/curated/sharecovex.json",
|
||||
"template": "apps/sharecovex.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,
|
||||
"hidden": false,
|
||||
"template_id": "image-sharecovex"
|
||||
},
|
||||
{
|
||||
"id": "shotcut",
|
||||
"provider": "linuxserver.io",
|
||||
|
||||
@@ -137,6 +137,62 @@ def _answers_from_recipe(ui) -> bool:
|
||||
return isinstance(ui, DefaultsUI)
|
||||
|
||||
|
||||
SHARED_DIRECTORY_NAME = re.compile(r"[a-z][a-z0-9_-]{0,31}")
|
||||
|
||||
|
||||
def ask_shared_directories(ui, template: dict[str, Any], mounts: list[dict[str, Any]], storage: str,
|
||||
advanced: bool, optional: bool = False) -> list[dict[str, Any]]:
|
||||
"""The directories an application exists to share. Its profile declares one
|
||||
container directory, and each directory the user names is mounted below
|
||||
it, on a container volume or from a directory of the host. A new
|
||||
installation asks for the first one and then for more until the user says
|
||||
no; `optional` is for an installed application, which may add none."""
|
||||
profile = template["proxmox"]["installer_profile"]
|
||||
declared = profile["shared_directories"]
|
||||
root = declared["container_root"].rstrip("/")
|
||||
reserved = set(declared.get("reserved_names", []))
|
||||
taken = {mount["container_path"] for mount in mounts}
|
||||
app_id = template["id"].removeprefix("image-")
|
||||
labels = [("managed-volume", translate("Container volume (included in backups)")),
|
||||
("host-bind", translate("Host directory (not included in Proxmox backups)"))]
|
||||
added: list[dict[str, Any]] = []
|
||||
if optional and not ui.confirm(translate("Add a directory to share"), False):
|
||||
return added
|
||||
while True:
|
||||
name = str(ui.ask(translate("Name of the directory to share (lowercase letters, digits, - or _)"))).strip()
|
||||
target = f"{root}/{name}"
|
||||
if not SHARED_DIRECTORY_NAME.fullmatch(name) or name in reserved:
|
||||
raise ValueError(translate("The name must start with a lowercase letter and use only lowercase "
|
||||
"letters, digits, - or _"))
|
||||
if target in taken:
|
||||
raise ValueError(f"{translate('This directory is already shared:')} {target}")
|
||||
location = ui.choose(f"{translate('Where to store')} {target}", labels, "managed-volume")
|
||||
if location is None:
|
||||
raise UserCancelled(translate("Volume configuration cancelled"))
|
||||
mount = {"type": location, "container_path": target, "custom": True, "source": storage,
|
||||
"size_gb": None, "backup": location == "managed-volume", "read_only": False,
|
||||
"create_if_missing": location == "host-bind"}
|
||||
if location == "managed-volume":
|
||||
if advanced:
|
||||
mount["source"] = ask_storage(ui, f"{translate('Storage for')} {target}", "rootdir", storage)
|
||||
mount["size_gb"] = _ask_size(ui, target, int(declared.get("default_size_gb", 32)))
|
||||
# The application writes to it as its own user, not as root.
|
||||
preparation = {"container_path": target, "remove_lost_found": True,
|
||||
"owner_strategy": "mapped-application-user",
|
||||
"only_when_mount_type": "managed-volume"}
|
||||
if preparation not in profile.setdefault("volume_preparations", []):
|
||||
profile["volume_preparations"].append(preparation)
|
||||
else:
|
||||
mount["source"] = ui.ask(f"{translate('Host path for')} {target}",
|
||||
f"/mnt/oci-shared/{app_id}/{name}")
|
||||
from .custom_mounts import validate_mount
|
||||
mount["container_path"] = validate_mount(mount, [*mounts, *added])
|
||||
added.append(mount)
|
||||
taken.add(target)
|
||||
if not ui.confirm(translate("Add another directory to share?"), False):
|
||||
return added
|
||||
|
||||
|
||||
def ask_application_extra_paths(ui, existing: list[str], storage: str) -> list[dict[str, Any]]:
|
||||
"""Extra paths for the application container of a multi-container
|
||||
application. An advanced installation asks them; a default one does not."""
|
||||
@@ -327,6 +383,9 @@ def build_deployment(
|
||||
if enabled:
|
||||
selected_security_options.update(item.get("options", {}))
|
||||
security_relaxation_acknowledged = True
|
||||
if item.get("privileged"):
|
||||
unprivileged = False
|
||||
privileged_acknowledged = True
|
||||
runtime_profile = installer_profile.get("runtime", {})
|
||||
hostname_default = _hostname_default(runtime_profile.get("hostname")
|
||||
or template["container_contract"]["container_name"])
|
||||
@@ -425,6 +484,9 @@ def build_deployment(
|
||||
}
|
||||
)
|
||||
|
||||
if installer_profile.get("shared_directories"):
|
||||
mounts += ask_shared_directories(ui, template, mounts, volume_storage, advanced)
|
||||
|
||||
if advanced:
|
||||
mounts = ask_custom_mounts(ui, mounts, volume_storage)
|
||||
|
||||
@@ -522,6 +584,9 @@ def build_deployment(
|
||||
value = secrets.token_hex(int(generated.get("bytes", 16)))
|
||||
if not value and item["required"]:
|
||||
raise InstallError(f"{name}: {translate('a value is required')}")
|
||||
minimum = installer_profile.get("minimum_secret_length", {}).get(name)
|
||||
if minimum and len(value) < int(minimum):
|
||||
raise InstallError(f"{name}: {translate('the minimum number of characters is')} {minimum}")
|
||||
elif not advanced and default not in (None, ""):
|
||||
value = default
|
||||
else:
|
||||
|
||||
@@ -111,6 +111,11 @@ def edit_recreation(record, ui):
|
||||
resources = deployment['resources']
|
||||
resources['cores'] = positive_integer(ui, translate('Cores'), resources['cores'])
|
||||
resources['memory_mb'] = positive_integer(ui, translate('RAM in MiB'), resources['memory_mb'], 128)
|
||||
template = candidate.get('template', {})
|
||||
if template.get('proxmox', {}).get('installer_profile', {}).get('shared_directories'):
|
||||
from .installer import ask_shared_directories
|
||||
deployment['mounts'] += ask_shared_directories(
|
||||
ui, template, deployment['mounts'], deployment['rootfs']['storage'], True, optional=True)
|
||||
from .custom_mounts import ask_custom_mounts
|
||||
deployment['mounts'] = ask_custom_mounts(
|
||||
ui, deployment['mounts'], deployment['rootfs']['storage'])
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
"""An application that exists to share directories asks for them in every
|
||||
installation, and its privileged profile is offered by the advanced one only."""
|
||||
|
||||
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"))
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
from proxmenux_oci.catalog import Catalog
|
||||
from proxmenux_oci.installer import ADVANCED_MODE, DEFAULT_MODE, InstallError, build_deployment
|
||||
from proxmenux_oci.recreation import edit_recreation
|
||||
from test_advanced_flow_order import storages
|
||||
|
||||
NAME = "Name of the directory to share (lowercase letters, digits, - or _)"
|
||||
MORE = "Add another directory to share?"
|
||||
NFSV4 = "Prepare this container for NFSv4?"
|
||||
|
||||
|
||||
class ScriptedUI:
|
||||
"""Answers a question with the next scripted answer for it, then with its default."""
|
||||
|
||||
def __init__(self, answers=None):
|
||||
self.answers = {key: list(value) for key, value in (answers or {}).items()}
|
||||
self.asked = []
|
||||
|
||||
def _answer(self, text, default):
|
||||
self.asked.append(text)
|
||||
for key, values in self.answers.items():
|
||||
if (text == key or text.endswith(key)) and values:
|
||||
return values.pop(0)
|
||||
return default
|
||||
|
||||
def ask(self, text, default=None, required=True):
|
||||
return self._answer(text, default if default is not None else "")
|
||||
|
||||
def password(self, text, required=True):
|
||||
return self._answer(text, "")
|
||||
|
||||
def choose(self, text, options, default=None):
|
||||
return self._answer(text, default)
|
||||
|
||||
def confirm(self, text, default=False):
|
||||
return self._answer(text, default)
|
||||
|
||||
def message(self, text):
|
||||
pass
|
||||
|
||||
|
||||
@patch("proxmenux_oci.i18n.language", return_value="en")
|
||||
@patch("proxmenux_oci.installer.host.storages", side_effect=storages)
|
||||
@patch("proxmenux_oci.installer.host.default_storage", side_effect=lambda content, default: default)
|
||||
@patch("proxmenux_oci.installer.host.default_bridge", side_effect=lambda default: default)
|
||||
@patch("proxmenux_oci.installer.host.bridges", return_value=[{"iface": "vmbr0", "cidr": "192.0.2.10/24"}])
|
||||
@patch("proxmenux_oci.installer.host.timezone", return_value="Europe/Madrid")
|
||||
@patch("proxmenux_oci.installer.host.gpus", return_value={"intel": [], "amd": [], "nvidia": False})
|
||||
@patch("proxmenux_oci.installer.access.ask_ipv4", return_value=("dhcp", None))
|
||||
class SharedDirectoriesTests(unittest.TestCase):
|
||||
catalog = Catalog(ROOT)
|
||||
|
||||
def build(self, answers, mode=DEFAULT_MODE):
|
||||
template = self.catalog.compose("sharecovex")
|
||||
ui = ScriptedUI(answers)
|
||||
return ui, template, build_deployment(template, ui, mode)
|
||||
|
||||
def test_a_default_installation_asks_for_directories_until_the_answer_is_no(self, *_):
|
||||
ui, template, plan = self.build({
|
||||
NAME: ["media", "backups"],
|
||||
"Where to store /shares/media": ["host-bind"],
|
||||
"Host path for /shares/media": ["/tank/media"],
|
||||
"Size in GB of /shares/backups": ["200"],
|
||||
MORE: [True, False],
|
||||
})
|
||||
mounts = {m["container_path"]: m for m in plan["mounts"]}
|
||||
self.assertEqual(list(mounts), ["/config", "/shares/media", "/shares/backups"])
|
||||
self.assertEqual((mounts["/shares/media"]["type"], mounts["/shares/media"]["source"],
|
||||
mounts["/shares/media"]["backup"], mounts["/shares/media"]["create_if_missing"]),
|
||||
("host-bind", "/tank/media", False, True))
|
||||
self.assertEqual((mounts["/shares/backups"]["type"], mounts["/shares/backups"]["size_gb"],
|
||||
mounts["/shares/backups"]["backup"], mounts["/shares/backups"]["custom"]),
|
||||
("managed-volume", 200, True, True))
|
||||
self.assertEqual(mounts["/shares/backups"]["source"], mounts["/config"]["source"])
|
||||
self.assertEqual(ui.asked.count(NAME), 2)
|
||||
self.assertEqual(ui.asked.count(MORE), 2)
|
||||
# The volume is prepared for the identity the application writes with; /config stays with root.
|
||||
prepared = {item["container_path"]: item["owner_strategy"]
|
||||
for item in template["proxmox"]["installer_profile"]["volume_preparations"]}
|
||||
self.assertEqual(prepared, {"/config": "mapped-root", "/shares/backups": "mapped-application-user"})
|
||||
|
||||
def test_a_default_installation_stays_unprivileged_and_generates_the_panel_password(self, *_):
|
||||
ui, _template, plan = self.build({NAME: ["media"]})
|
||||
self.assertFalse(any(NFSV4 in text for text in ui.asked))
|
||||
self.assertFalse(any("Add an extra custom path" in text for text in ui.asked))
|
||||
self.assertEqual(plan["security"]["unprivileged"], True)
|
||||
self.assertEqual(plan["security"]["options"], {})
|
||||
password = next(item for item in plan["environment"] if item["name"] == "SHARECOVEX_ADMIN_PASSWORD")
|
||||
self.assertRegex(password["value"], r"^[0-9a-f]{16}$")
|
||||
self.assertFalse(any("ShareCoveX panel administrator" in text for text in ui.asked))
|
||||
self.assertEqual([m["container_path"] for m in plan["mounts"]], ["/config", "/shares/media"])
|
||||
self.assertEqual(plan["mounts"][1]["size_gb"], 32)
|
||||
|
||||
def test_only_the_advanced_installation_offers_nfsv4_and_it_makes_the_container_privileged(self, *_):
|
||||
ui, _template, plan = self.build({NAME: ["media"]}, ADVANCED_MODE)
|
||||
self.assertTrue(any(NFSV4 in text for text in ui.asked))
|
||||
self.assertEqual((plan["security"]["unprivileged"], plan["security"]["privileged_acknowledged"]), (True, False))
|
||||
question = next(text for text in ui.asked if NFSV4 in text)
|
||||
ui, _template, plan = self.build({NAME: ["media"], question: [True]}, ADVANCED_MODE)
|
||||
security = plan["security"]
|
||||
self.assertEqual((security["unprivileged"], security["privileged_acknowledged"],
|
||||
security["relaxation_acknowledged"]), (False, True, True))
|
||||
self.assertEqual(security["options"], {"apparmor_profile": "unconfined", "seccomp_profile": "unconfined"})
|
||||
|
||||
def test_an_advanced_installation_places_each_directory_and_takes_the_password(self, *_):
|
||||
ui, _template, plan = self.build({
|
||||
NAME: ["media"],
|
||||
"Storage for /shares/media": ["Public"],
|
||||
"(empty = generate)": ["a-long-panel-password"],
|
||||
}, ADVANCED_MODE)
|
||||
mounts = {m["container_path"]: m["source"] for m in plan["mounts"]}
|
||||
self.assertEqual(mounts["/shares/media"], "Public")
|
||||
self.assertEqual(next(item["value"] for item in plan["environment"]), "a-long-panel-password")
|
||||
with self.assertRaisesRegex(InstallError, "minimum number of characters is 8"):
|
||||
self.build({NAME: ["media"], "(empty = generate)": ["short"]}, ADVANCED_MODE)
|
||||
|
||||
def test_a_name_that_cannot_identify_a_share_is_refused(self, *_):
|
||||
for name in ("Media", "my files", "9lives", "config", "", "a" * 33):
|
||||
with self.assertRaisesRegex(ValueError, "lowercase"):
|
||||
self.build({NAME: [name]})
|
||||
with self.assertRaisesRegex(ValueError, "already shared"):
|
||||
self.build({NAME: ["media", "media"], MORE: [True, False]})
|
||||
|
||||
def test_modify_can_add_another_directory_and_may_add_none(self, *_):
|
||||
_ui, template, plan = self.build({NAME: ["media"]})
|
||||
record = {"template": template, "deployment": plan}
|
||||
with patch("proxmenux_oci.recreation.refresh_template"), \
|
||||
patch("proxmenux_oci.recreation.edit_acceleration"), \
|
||||
patch("proxmenux_oci.recreation.edit_environment"):
|
||||
ui = ScriptedUI({"Add a directory to share": [True], NAME: ["photos"],
|
||||
"Where to store /shares/photos": ["host-bind"],
|
||||
"Host path for /shares/photos": ["/tank/photos"]})
|
||||
proposal = edit_recreation(record, ui)
|
||||
mounts = [m["container_path"] for m in proposal["candidate"]["deployment"]["mounts"]]
|
||||
self.assertEqual(mounts, ["/config", "/shares/media", "/shares/photos"])
|
||||
untouched = edit_recreation(record, ScriptedUI())
|
||||
self.assertEqual([m["container_path"] for m in untouched["candidate"]["deployment"]["mounts"]],
|
||||
["/config", "/shares/media"])
|
||||
self.assertEqual([m["container_path"] for m in plan["mounts"]], ["/config", "/shares/media"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user