feat(oci): add Scanopy and force the stop of a container that ignores it

This commit is contained in:
MacRimi
2026-11-01 01:25:04 +01:00
parent cb97fc4c98
commit e8e9e07e6b
16 changed files with 1397 additions and 3 deletions
+2
View File
@@ -626,6 +626,7 @@
"Automated post-installation script": "Automatisiertes Nachinstallationsskript",
"Automatic Video Library Manager for TV Shows. It watches for new episodes of your favorite shows, and when they are posted it does its magic.": "Automatic Video Library Manager für TV-Shows. Es schaut nach neuen episoden ihrer lieblingsshows, und wenn sie gepostet werden, macht es seine magie.",
"Automatic detection": "Automatische Erkennung",
"Automatic discovery and live diagrams of your network": "Automatische Erkennung und aktuelle Diagramme deines Netzwerks",
"Automatic private network allocation requires a /24 subnet": "Automatische private Netzwerkzuweisung requires a /24 Subnetz",
"Automatic public URLs require static frontend addresses or explicit URLs": "Automatische öffentliche URLs erfordern statische Frontend-Adressen oder explizite URLs",
"Automatic video library manager for TV Shows": "Automatischer Videobibliotheksmanager für TV-Shows",
@@ -4891,6 +4892,7 @@
"Open the dashboard from this host on port 8008 to create a new admin account.": "Öffnen Sie das Dashboard von diesem Host auf Port 8008, um ein neues Administratorkonto zu erstellen.",
"Open the dashboard to create a new admin account:": "Öffnen Sie das Dashboard, um ein neues Administratorkonto zu erstellen:",
"Open the page of an application in the menu": "Die Seite einer Anwendung im Menü öffnen",
"Open the web address and create the first account: the daemon registers by itself and starts the first scan.": "Öffne die Webadresse und erstelle das erste Konto: Der Daemon registriert sich selbst und startet den ersten Scan.",
"Open the web address and create the password of the akadmin user in the first-run setup.": "Öffnen Sie die Webadresse und erstellen Sie im ersten Setup das Passwort des akadmin-Benutzers.",
"Open-source AI-powered coding assistant": "Open-Source AI-powered Coding Assistant",
"Open-source UI for building and debugging multi-agent and RAG applications": "Open-Source-Benutzeroberfläche zum Erstellen und Debuggen von Multi-Agenten- und RAG-Anwendungen",
+2
View File
@@ -624,6 +624,7 @@
"Automated post-installation script": "Script automático Post-Install",
"Automatic Video Library Manager for TV Shows. It watches for new episodes of your favorite shows, and when they are posted it does its magic.": "Gestor automático de videoteca para series de TV. Vigila la aparición de nuevos episodios de tus series favoritas y, cuando se publican, hace su magia.",
"Automatic detection": "Detección automática",
"Automatic discovery and live diagrams of your network": "Descubrimiento automático y diagramas actualizados de tu red",
"Automatic private network allocation requires a /24 subnet": "Asignación automática de red privada requiere una subred /24",
"Automatic public URLs require static frontend addresses or explicit URLs": "Las URL públicas automáticas requieren direcciones estáticas para los servicios web o URL explícitas",
"Automatic video library manager for TV Shows": "Gestor automático de videoteca para series de TV",
@@ -4882,6 +4883,7 @@
"Open the dashboard from this host on port 8008 to create a new admin account.": "Abre el panel de este host en el puerto 8008 para crear una nueva cuenta de administrador.",
"Open the dashboard to create a new admin account:": "Abre el panel para crear una nueva cuenta de administrador:",
"Open the page of an application in the menu": "Abrir la página de una aplicación en el menú",
"Open the web address and create the first account: the daemon registers by itself and starts the first scan.": "Abre la dirección web y crea la primera cuenta: el daemon se registra solo y empieza el primer escaneo.",
"Open the web address and create the password of the akadmin user in the first-run setup.": "abra la dirección web y cree la contraseña del usuario akaadmin en la configuración de primera ejecución.",
"Open-source AI-powered coding assistant": "Asistente de programación con IA de código abierto",
"Open-source UI for building and debugging multi-agent and RAG applications": "Interfaz de código abierto para crear y depurar aplicaciones multiagente y RAG",
+2
View File
@@ -626,6 +626,7 @@
"Automated post-installation script": "Script de post-installation automatisé",
"Automatic Video Library Manager for TV Shows. It watches for new episodes of your favorite shows, and when they are posted it does its magic.": "Gestionnaire automatique de bibliothèque vidéo pour les émissions de télévision. Il regarde de nouveaux épisodes de vos spectacles préférés, et quand ils sont affichés, il fait sa magie.",
"Automatic detection": "Détection automatique",
"Automatic discovery and live diagrams of your network": "Découverte automatique et diagrammes à jour de votre réseau",
"Automatic private network allocation requires a /24 subnet": "Allocation de réseau privé automatique requires un sous-réseau /24",
"Automatic public URLs require static frontend addresses or explicit URLs": "Les URL publiques automatiques nécessitent des adresses statiques pour les interfaces web ou des URL explicites",
"Automatic video library manager for TV Shows": "Gestionnaire automatique de bibliothèque vidéo pour TV Shows",
@@ -4891,6 +4892,7 @@
"Open the dashboard from this host on port 8008 to create a new admin account.": "Ouvrez le tableau de bord de cet hôte sur le port 8008 pour créer un nouveau compte administrateur.",
"Open the dashboard to create a new admin account:": "Ouvrez le tableau de bord pour créer un nouveau compte administrateur :",
"Open the page of an application in the menu": "Ouvrir la page d'une application dans le menu",
"Open the web address and create the first account: the daemon registers by itself and starts the first scan.": "Ouvrez l'adresse web et créez le premier compte : le daemon s'enregistre tout seul et lance la première analyse.",
"Open the web address and create the password of the akadmin user in the first-run setup.": "ouvrez l'adresse Web et créez le mot de passe de l'utilisateur akadmin lors de la configuration de première exécution.",
"Open-source AI-powered coding assistant": "Assistant de codage à source ouverte",
"Open-source UI for building and debugging multi-agent and RAG applications": "UI open-source pour la construction et le débogage d'applications multi-agents et RAG",
+2
View File
@@ -626,6 +626,7 @@
"Automated post-installation script": "Script post-installazione automatizzato",
"Automatic Video Library Manager for TV Shows. It watches for new episodes of your favorite shows, and when they are posted it does its magic.": "Gestore automatico di videoteche per serie TV. Monitora l'uscita di nuovi episodi delle serie preferite e li gestisce quando sono disponibili.",
"Automatic detection": "Rilevamento automatico",
"Automatic discovery and live diagrams of your network": "Rilevamento automatico e diagrammi aggiornati della tua rete",
"Automatic private network allocation requires a /24 subnet": "L’assegnazione automatica della rete privata richiede una sottorete /24",
"Automatic public URLs require static frontend addresses or explicit URLs": "Gli URL pubblici automatici richiedono indirizzi statici per i frontend o URL espliciti",
"Automatic video library manager for TV Shows": "Gestore automatico di librerie video per serie TV",
@@ -4891,6 +4892,7 @@
"Open the dashboard from this host on port 8008 to create a new admin account.": "Apri la dashboard da questo host sulla porta 8008 per creare un nuovo account amministratore.",
"Open the dashboard to create a new admin account:": "Apri la dashboard per creare un nuovo account amministratore:",
"Open the page of an application in the menu": "Apri la pagina di un'applicazione nel menu",
"Open the web address and create the first account: the daemon registers by itself and starts the first scan.": "Apri l'indirizzo web e crea il primo account: il daemon si registra da solo e avvia la prima scansione.",
"Open the web address and create the password of the akadmin user in the first-run setup.": "apri l'indirizzo web e crea la password dell'utente akadmin nella configurazione di prima esecuzione.",
"Open-source AI-powered coding assistant": "Assistente open source per la programmazione basato sull'IA",
"Open-source UI for building and debugging multi-agent and RAG applications": "Interfaccia open source per creare ed eseguire il debug di applicazioni multiagente e RAG",
+2
View File
@@ -626,6 +626,7 @@
"Automated post-installation script": "script de pós-instalação automatizado",
"Automatic Video Library Manager for TV Shows. It watches for new episodes of your favorite shows, and when they are posted it does its magic.": "Gestor de Biblioteca de Vídeo Automático para Programas de TV. Ele assiste a novos episódios de seus programas favoritos, e quando eles são postados ele faz sua mágica.",
"Automatic detection": "Detecção automática",
"Automatic discovery and live diagrams of your network": "Deteção automática e diagramas atualizados da sua rede",
"Automatic private network allocation requires a /24 subnet": "Alocação automática de rede privada requires a /24 subnet",
"Automatic public URLs require static frontend addresses or explicit URLs": "Os URL públicos automáticos exigem endereços estáticos para os serviços web ou URL explícitos",
"Automatic video library manager for TV Shows": "Gerenciador automático de bibliotecas de vídeo para programas de TV",
@@ -4891,6 +4892,7 @@
"Open the dashboard from this host on port 8008 to create a new admin account.": "Abra o painel deste host na porta 8008 para criar uma nova conta de administrador.",
"Open the dashboard to create a new admin account:": "Abra o painel para criar uma nova conta de administrador:",
"Open the page of an application in the menu": "Abrir a página de uma aplicação no menu",
"Open the web address and create the first account: the daemon registers by itself and starts the first scan.": "Abra o endereço web e crie a primeira conta: o daemon regista-se sozinho e inicia a primeira análise.",
"Open the web address and create the password of the akadmin user in the first-run setup.": "Abra o endereço da web e crie a senha do usuário akadmin na configuração inicial.",
"Open-source AI-powered coding assistant": "Assistente de codificação com código aberto",
"Open-source UI for building and debugging multi-agent and RAG applications": "UI de código aberto para a construção e depuração de aplicações multi-agente e RAG",
+2
View File
@@ -626,6 +626,7 @@
"Automated post-installation script": "Automatický skript po inštalácii",
"Automatic Video Library Manager for TV Shows. It watches for new episodes of your favorite shows, and when they are posted it does its magic.": "Automatický správca knižnice televíznych seriálov. Sleduje nové epizódy obľúbených seriálov a spracuje ich, keď sú dostupné.",
"Automatic detection": "Automatická detekcia",
"Automatic discovery and live diagrams of your network": "Automatické zisťovanie a aktuálne diagramy vašej siete",
"Automatic private network allocation requires a /24 subnet": "Automatické pridelenie súkromnej siete vyžaduje podsieť /24",
"Automatic public URLs require static frontend addresses or explicit URLs": "Automatické verejné URL vyžadujú statické adresy frontendov alebo explicitné URL",
"Automatic video library manager for TV Shows": "Automatický správca knižnice televíznych seriálov",
@@ -4893,6 +4894,7 @@
"Open the dashboard from this host on port 8008 to create a new admin account.": "Otvorte dashboard z tohto servera na porte 8008 a vytvorte nový admin účet.",
"Open the dashboard to create a new admin account:": "Otvorte dashboard a vytvorte nový admin účet:",
"Open the page of an application in the menu": "Otvoriť stránku aplikácie v ponuke",
"Open the web address and create the first account: the daemon registers by itself and starts the first scan.": "Otvorte webovú adresu a vytvorte prvé konto: daemon sa zaregistruje sám a spustí prvé skenovanie.",
"Open the web address and create the password of the akadmin user in the first-run setup.": "Otvorte webovú adresu a v prvotnom nastavení vytvorte heslo používateľa akadmin.",
"Open-source AI-powered coding assistant": "Programátorský asistent využívajúci AI s otvoreným zdrojovým kódom",
"Open-source UI for building and debugging multi-agent and RAG applications": "Rozhranie s otvoreným zdrojovým kódom na tvorbu a ladenie aplikácií s viacerými agentmi a RAG",
+2
View File
@@ -626,6 +626,7 @@
"Automated post-installation script": "Automatiserat skript efter installation",
"Automatic Video Library Manager for TV Shows. It watches for new episodes of your favorite shows, and when they are posted it does its magic.": "Automatisk videobibliotekschef för TV-program. Den tittar på nya avsnitt av dina favoritprogram, och när de publiceras gör den sin magi.",
"Automatic detection": "Automatisk upptäckt",
"Automatic discovery and live diagrams of your network": "Automatisk upptäckt och aktuella diagram över ditt nätverk",
"Automatic private network allocation requires a /24 subnet": "Automatisk privat nätverksallokering requires a /24 subnet",
"Automatic public URLs require static frontend addresses or explicit URLs": "Automatiska publika URL:er kräver statiska frontendadresser eller uttryckliga URL:er",
"Automatic video library manager for TV Shows": "Automatisk videobibliotekschef för TV-program",
@@ -4891,6 +4892,7 @@
"Open the dashboard from this host on port 8008 to create a new admin account.": "Öppna instrumentpanelen från denna värd på port 8008 för att skapa ett nytt administratörskonto.",
"Open the dashboard to create a new admin account:": "Öppna instrumentpanelen för att skapa ett nytt administratörskonto:",
"Open the page of an application in the menu": "Öppna en applikations sida i menyn",
"Open the web address and create the first account: the daemon registers by itself and starts the first scan.": "Öppna webbadressen och skapa det första kontot: daemonen registrerar sig själv och startar den första skanningen.",
"Open the web address and create the password of the akadmin user in the first-run setup.": "Öppna webbadressen och skapa lösenordet för akadmin-användaren i den första körningen.",
"Open-source AI-powered coding assistant": "Open-source AI-driven kodningsassistent",
"Open-source UI for building and debugging multi-agent and RAG applications": "Open-source UI för att bygga och debugga multi-agent- och RAG-applikationer",
+1 -1
View File
@@ -2,7 +2,7 @@
<!-- Generated from oci/catalog/verification.json by .github/scripts/oci_validation.py. Do not edit by hand. -->
44 of 531 applications in the OCI catalog have been tested for real: 41 in the ProxMenux lab and 3 by the community. The OCI installer shows them as verified, with a ✓ in the lists.
44 of 532 applications in the OCI catalog have been tested for real: 41 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.
+635
View File
@@ -0,0 +1,635 @@
{
"schema_version": "0.5.0",
"kind": "proxmenux.oci-template",
"id": "image-scanopy",
"status": "generated-unvalidated",
"catalog_ui": {
"title": {
"en_US": "Scanopy"
},
"tagline": {
"en_US": "Automatic discovery and live diagrams of your network"
},
"description": {
"en_US": "Scanopy discovers the hosts and services of your network and keeps its diagrams up to date. This recipe installs the server, PostgreSQL and the scanning daemon, each in its own Proxmox OCI LXC."
},
"category": "network",
"category_label": "Network & Firewall",
"author": "Scanopy",
"developer": "Scanopy",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/scanopy.webp",
"thumbnail": null,
"screenshots": [],
"architectures": [
"amd64",
"arm64"
],
"launch": {
"scheme": "http",
"port": 60072,
"path": "/"
},
"website": "https://scanopy.net/",
"documentation": "https://scanopy.net/docs/",
"repository": "https://github.com/scanopy/scanopy",
"tips": [
"The daemon gets an interface on the selected Proxmox bridge and scans that network; PostgreSQL stays on the private stack network.",
"The Docker-socket mount of the daemon is omitted, so Docker service discovery is not available."
],
"mini_changelog": [],
"display_version": null,
"updated_at": "2026-10-30"
},
"source": {
"provider": "scanopy",
"repository": "https://github.com/scanopy/scanopy",
"default_branch": "main",
"revision": "c98a726047481c7ae8b2a468b24862aad82342e3",
"readme_raw_url": "https://raw.githubusercontent.com/scanopy/scanopy/refs/heads/main/docker-compose.yml",
"image_repository_url": "https://github.com/scanopy/scanopy/pkgs/container/scanopy%2Fserver",
"readme_pushed_at": "2026-10-31T12:40:09Z",
"compose_sha256": "d1812133c0497d91e53b3597d857109e6429b89763ab8adf430acb3cb4f6f922",
"generated_at": "2026-10-31T18:33:28+00:00"
},
"container_contract": {
"service_name": "server",
"container_name": "scanopy",
"image": {
"reference": "ghcr.io/scanopy/scanopy/server:latest",
"registry": "ghcr.io",
"repository": "ghcr.io/scanopy/scanopy/server",
"tag": "latest",
"digest": null,
"pull_policy": "resolve-written-reference-to-architecture-digest-at-install"
},
"environment": [
{
"name": "SCANOPY_DATABASE_URL",
"example": "postgresql://postgres:${GENERATED_DB_PASSWORD}@postgres:5432/scanopy",
"required": true,
"sensitive": false,
"source": "docker-compose"
},
{
"name": "SCANOPY_INTEGRATED_DAEMON_URL",
"example": "http://daemon:60073",
"required": true,
"sensitive": false,
"source": "docker-compose"
},
{
"name": "SCANOPY_LOG_LEVEL",
"example": "info",
"required": true,
"sensitive": false,
"source": "docker-compose"
},
{
"name": "SCANOPY_WEB_EXTERNAL_PATH",
"example": "/app/static",
"required": true,
"sensitive": false,
"source": "docker-compose"
}
],
"volumes": [
{
"id": "volume-0",
"container_path": "/data",
"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": 8
}
}
],
"ports": [
{
"container_port": 60072,
"published_example": 60072,
"protocol": "tcp",
"required": true,
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
}
],
"related_services": [
{
"name": "daemon",
"image": "ghcr.io/scanopy/scanopy/daemon:latest"
},
{
"name": "postgres",
"image": "postgres:17-alpine"
}
],
"restart": "unless-stopped",
"stop_grace_period": null,
"original_compose": "name: scanopy\nservices:\n daemon:\n environment:\n SCANOPY_LOG_LEVEL: info\n SCANOPY_SERVER_URL: http://server:60072\n healthcheck:\n interval: 5s\n retries: 15\n test:\n - CMD-SHELL\n - curl -f http://localhost:60073/api/health || exit 1\n timeout: 3s\n image: ghcr.io/scanopy/scanopy/daemon:latest\n ports:\n - 60073:60073\n restart: unless-stopped\n volumes:\n - read_only: false\n source: daemon-config\n target: /root/.config/scanopy/daemon\n postgres:\n environment:\n POSTGRES_DB: scanopy\n POSTGRES_PASSWORD: ${GENERATED_DB_PASSWORD}\n POSTGRES_USER: postgres\n healthcheck:\n interval: 10s\n retries: 5\n test:\n - CMD-SHELL\n - pg_isready -U postgres\n timeout: 5s\n image: postgres:17-alpine\n restart: unless-stopped\n volumes:\n - read_only: false\n source: postgres_data\n target: /var/lib/postgresql/data\n server:\n depends_on:\n daemon:\n condition: service_started\n postgres:\n condition: service_healthy\n environment:\n SCANOPY_DATABASE_URL: postgresql://postgres:${GENERATED_DB_PASSWORD}@postgres:5432/scanopy\n SCANOPY_INTEGRATED_DAEMON_URL: http://daemon:60073\n SCANOPY_LOG_LEVEL: info\n SCANOPY_WEB_EXTERNAL_PATH: /app/static\n image: ghcr.io/scanopy/scanopy/server:latest\n ports:\n - 60072:60072\n restart: unless-stopped\n volumes:\n - read_only: false\n source: data\n target: /data\nvolumes:\n daemon-config: null\n data: null\n postgres_data: null\n"
},
"compose_stack": {
"project_name": "scanopy",
"deployment_model": "one-native-oci-lxc-per-compose-service",
"user_experience": "single-application-install",
"main_service": "server",
"service_count": 3,
"services": [
{
"name": "daemon",
"image": "ghcr.io/scanopy/scanopy/daemon:latest",
"is_main": false,
"role": "dependency",
"vmid_offset": 1,
"depends_on": [],
"frontend_network": true,
"private_network": true,
"compose": {
"environment": {
"SCANOPY_LOG_LEVEL": "info",
"SCANOPY_SERVER_URL": "http://server:60072"
},
"healthcheck": {
"interval": "5s",
"retries": 15,
"test": [
"CMD-SHELL",
"curl -f http://localhost:60073/api/health || exit 1"
],
"timeout": "3s"
},
"image": "ghcr.io/scanopy/scanopy/daemon:latest",
"ports": [
"60073:60073"
],
"restart": "unless-stopped",
"volumes": [
{
"source": "daemon-config",
"target": "/root/.config/scanopy/daemon",
"read_only": false
}
],
"stop_signal": "SIGINT"
}
},
{
"name": "postgres",
"image": "docker.io/library/postgres:17-alpine",
"is_main": false,
"role": "dependency",
"vmid_offset": 2,
"depends_on": [],
"frontend_network": false,
"private_network": true,
"compose": {
"environment": {
"POSTGRES_DB": "scanopy",
"POSTGRES_USER": "postgres",
"POSTGRES_PASSWORD": "${GENERATED_DB_PASSWORD}"
},
"healthcheck": {
"interval": "10s",
"retries": 5,
"test": [
"CMD-SHELL",
"pg_isready -U postgres"
],
"timeout": "5s"
},
"image": "docker.io/library/postgres:17-alpine",
"restart": "unless-stopped",
"volumes": [
{
"source": "postgres_data",
"target": "/var/lib/postgresql/data",
"read_only": false
}
]
}
},
{
"name": "server",
"image": "ghcr.io/scanopy/scanopy/server:latest",
"is_main": true,
"role": "frontend",
"vmid_offset": 0,
"depends_on": [
"daemon",
"postgres"
],
"frontend_network": true,
"private_network": true,
"compose": {
"depends_on": {
"daemon": {
"condition": "service_started"
},
"postgres": {
"condition": "service_healthy"
}
},
"environment": {
"SCANOPY_LOG_LEVEL": "info",
"SCANOPY_DATABASE_URL": "postgresql://postgres:${GENERATED_DB_PASSWORD}@postgres:5432/scanopy",
"SCANOPY_WEB_EXTERNAL_PATH": "/app/static",
"SCANOPY_INTEGRATED_DAEMON_URL": "http://daemon:60073"
},
"image": "ghcr.io/scanopy/scanopy/server:latest",
"ports": [
"60072:60072"
],
"restart": "unless-stopped",
"volumes": [
{
"source": "data",
"target": "/data",
"read_only": false
}
]
}
}
],
"top_level": {
"name": "scanopy",
"volumes": {
"daemon-config": null,
"data": null,
"postgres_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": "daemon-through-selected-proxmox-bridge; postgres-private-only",
"prompt_user_for_private_network": false
},
"storage": [
{
"id": "daemon-volume-0",
"service": "daemon",
"container_path": "/root/.config/scanopy/daemon",
"mode": "managed-volume",
"user_selectable": false,
"backup": true,
"shared_with_other_lxc": false,
"source_path": null,
"source_path_prompt": null
},
{
"id": "postgres-volume-0",
"service": "postgres",
"container_path": "/var/lib/postgresql/data",
"mode": "managed-volume",
"user_selectable": false,
"backup": true,
"shared_with_other_lxc": false,
"source_path": null,
"source_path_prompt": null
},
{
"id": "server-volume-0",
"service": "server",
"container_path": "/data",
"mode": "managed-volume",
"user_selectable": false,
"backup": true,
"shared_with_other_lxc": false,
"source_path": null,
"source_path_prompt": null
}
],
"orchestration": {
"reserve_vmids_atomically": 3,
"start_order": [
"daemon",
"postgres",
"server"
],
"stop_order": [
"server",
"postgres",
"daemon"
],
"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": "db-password",
"strategy": "generate-cryptographically-random-at-install",
"bindings": [
{
"service": "postgres",
"environment_variable": "POSTGRES_PASSWORD"
},
{
"service": "server",
"environment_variable": "SCANOPY_DATABASE_URL"
}
]
}
]
}
},
"first_run": {
"endpoints": [
{
"label": "Web interface",
"scheme": "http",
"port": 60072,
"path": "/",
"source": "official-compose"
}
],
"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"
}
],
"custom_compose_stack": true,
"custom_compose_interpolated": false,
"stack_image_entrypoints": {
"ghcr.io/scanopy/scanopy/server:latest": []
},
"installer_profile": {
"stack_driver": "generic-multi-lxc-stack",
"volume_preparations": [
{
"container_path": "/root/.config/scanopy/daemon",
"remove_lost_found": true,
"owner_strategy": "mapped-application-user",
"only_when_mount_type": "managed-volume"
},
{
"container_path": "/var/lib/postgresql/data",
"remove_lost_found": true,
"owner_strategy": "mapped-application-user",
"only_when_mount_type": "managed-volume"
},
{
"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 create the first account: the daemon registers by itself and starts the first scan."
]
},
"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-local-2026-10-31",
"clean_install": "pending",
"service_health": "pending",
"restart_persistence": "pending",
"backup_restore": "pending",
"update_preserves_data": "pending"
},
"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
}
}
}
+1
View File
@@ -322,6 +322,7 @@
"rsshub": "tools",
"rustdesk": "remote",
"rustfs": "sharing",
"scanopy": "network",
"scrutiny": "monitoring",
"scummvm": "gaming",
"sealskin": "desktop",
+635
View File
@@ -0,0 +1,635 @@
{
"schema_version": "0.5.0",
"kind": "proxmenux.oci-template",
"id": "image-scanopy",
"status": "generated-unvalidated",
"catalog_ui": {
"title": {
"en_US": "Scanopy"
},
"tagline": {
"en_US": "Automatic discovery and live diagrams of your network"
},
"description": {
"en_US": "Scanopy discovers the hosts and services of your network and keeps its diagrams up to date. This recipe installs the server, PostgreSQL and the scanning daemon, each in its own Proxmox OCI LXC."
},
"category": "network",
"category_label": "Network & Firewall",
"author": "Scanopy",
"developer": "Scanopy",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/scanopy.webp",
"thumbnail": null,
"screenshots": [],
"architectures": [
"amd64",
"arm64"
],
"launch": {
"scheme": "http",
"port": 60072,
"path": "/"
},
"website": "https://scanopy.net/",
"documentation": "https://scanopy.net/docs/",
"repository": "https://github.com/scanopy/scanopy",
"tips": [
"The daemon gets an interface on the selected Proxmox bridge and scans that network; PostgreSQL stays on the private stack network.",
"The Docker-socket mount of the daemon is omitted, so Docker service discovery is not available."
],
"mini_changelog": [],
"display_version": null,
"updated_at": "2026-10-30"
},
"source": {
"provider": "scanopy",
"repository": "https://github.com/scanopy/scanopy",
"default_branch": "main",
"revision": "c98a726047481c7ae8b2a468b24862aad82342e3",
"readme_raw_url": "https://raw.githubusercontent.com/scanopy/scanopy/refs/heads/main/docker-compose.yml",
"image_repository_url": "https://github.com/scanopy/scanopy/pkgs/container/scanopy%2Fserver",
"readme_pushed_at": "2026-10-31T12:40:09Z",
"compose_sha256": "d1812133c0497d91e53b3597d857109e6429b89763ab8adf430acb3cb4f6f922",
"generated_at": "2026-10-31T18:33:28+00:00"
},
"container_contract": {
"service_name": "server",
"container_name": "scanopy",
"image": {
"reference": "ghcr.io/scanopy/scanopy/server:latest",
"registry": "ghcr.io",
"repository": "ghcr.io/scanopy/scanopy/server",
"tag": "latest",
"digest": null,
"pull_policy": "resolve-written-reference-to-architecture-digest-at-install"
},
"environment": [
{
"name": "SCANOPY_DATABASE_URL",
"example": "postgresql://postgres:${GENERATED_DB_PASSWORD}@postgres:5432/scanopy",
"required": true,
"sensitive": false,
"source": "docker-compose"
},
{
"name": "SCANOPY_INTEGRATED_DAEMON_URL",
"example": "http://daemon:60073",
"required": true,
"sensitive": false,
"source": "docker-compose"
},
{
"name": "SCANOPY_LOG_LEVEL",
"example": "info",
"required": true,
"sensitive": false,
"source": "docker-compose"
},
{
"name": "SCANOPY_WEB_EXTERNAL_PATH",
"example": "/app/static",
"required": true,
"sensitive": false,
"source": "docker-compose"
}
],
"volumes": [
{
"id": "volume-0",
"container_path": "/data",
"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": 8
}
}
],
"ports": [
{
"container_port": 60072,
"published_example": 60072,
"protocol": "tcp",
"required": true,
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
}
],
"related_services": [
{
"name": "daemon",
"image": "ghcr.io/scanopy/scanopy/daemon:latest"
},
{
"name": "postgres",
"image": "postgres:17-alpine"
}
],
"restart": "unless-stopped",
"stop_grace_period": null,
"original_compose": "name: scanopy\nservices:\n daemon:\n environment:\n SCANOPY_LOG_LEVEL: info\n SCANOPY_SERVER_URL: http://server:60072\n healthcheck:\n interval: 5s\n retries: 15\n test:\n - CMD-SHELL\n - curl -f http://localhost:60073/api/health || exit 1\n timeout: 3s\n image: ghcr.io/scanopy/scanopy/daemon:latest\n ports:\n - 60073:60073\n restart: unless-stopped\n volumes:\n - read_only: false\n source: daemon-config\n target: /root/.config/scanopy/daemon\n postgres:\n environment:\n POSTGRES_DB: scanopy\n POSTGRES_PASSWORD: ${GENERATED_DB_PASSWORD}\n POSTGRES_USER: postgres\n healthcheck:\n interval: 10s\n retries: 5\n test:\n - CMD-SHELL\n - pg_isready -U postgres\n timeout: 5s\n image: postgres:17-alpine\n restart: unless-stopped\n volumes:\n - read_only: false\n source: postgres_data\n target: /var/lib/postgresql/data\n server:\n depends_on:\n daemon:\n condition: service_started\n postgres:\n condition: service_healthy\n environment:\n SCANOPY_DATABASE_URL: postgresql://postgres:${GENERATED_DB_PASSWORD}@postgres:5432/scanopy\n SCANOPY_INTEGRATED_DAEMON_URL: http://daemon:60073\n SCANOPY_LOG_LEVEL: info\n SCANOPY_WEB_EXTERNAL_PATH: /app/static\n image: ghcr.io/scanopy/scanopy/server:latest\n ports:\n - 60072:60072\n restart: unless-stopped\n volumes:\n - read_only: false\n source: data\n target: /data\nvolumes:\n daemon-config: null\n data: null\n postgres_data: null\n"
},
"compose_stack": {
"project_name": "scanopy",
"deployment_model": "one-native-oci-lxc-per-compose-service",
"user_experience": "single-application-install",
"main_service": "server",
"service_count": 3,
"services": [
{
"name": "daemon",
"image": "ghcr.io/scanopy/scanopy/daemon:latest",
"is_main": false,
"role": "dependency",
"vmid_offset": 1,
"depends_on": [],
"frontend_network": true,
"private_network": true,
"compose": {
"environment": {
"SCANOPY_LOG_LEVEL": "info",
"SCANOPY_SERVER_URL": "http://server:60072"
},
"healthcheck": {
"interval": "5s",
"retries": 15,
"test": [
"CMD-SHELL",
"curl -f http://localhost:60073/api/health || exit 1"
],
"timeout": "3s"
},
"image": "ghcr.io/scanopy/scanopy/daemon:latest",
"ports": [
"60073:60073"
],
"restart": "unless-stopped",
"volumes": [
{
"source": "daemon-config",
"target": "/root/.config/scanopy/daemon",
"read_only": false
}
],
"stop_signal": "SIGINT"
}
},
{
"name": "postgres",
"image": "docker.io/library/postgres:17-alpine",
"is_main": false,
"role": "dependency",
"vmid_offset": 2,
"depends_on": [],
"frontend_network": false,
"private_network": true,
"compose": {
"environment": {
"POSTGRES_DB": "scanopy",
"POSTGRES_USER": "postgres",
"POSTGRES_PASSWORD": "${GENERATED_DB_PASSWORD}"
},
"healthcheck": {
"interval": "10s",
"retries": 5,
"test": [
"CMD-SHELL",
"pg_isready -U postgres"
],
"timeout": "5s"
},
"image": "docker.io/library/postgres:17-alpine",
"restart": "unless-stopped",
"volumes": [
{
"source": "postgres_data",
"target": "/var/lib/postgresql/data",
"read_only": false
}
]
}
},
{
"name": "server",
"image": "ghcr.io/scanopy/scanopy/server:latest",
"is_main": true,
"role": "frontend",
"vmid_offset": 0,
"depends_on": [
"daemon",
"postgres"
],
"frontend_network": true,
"private_network": true,
"compose": {
"depends_on": {
"daemon": {
"condition": "service_started"
},
"postgres": {
"condition": "service_healthy"
}
},
"environment": {
"SCANOPY_LOG_LEVEL": "info",
"SCANOPY_DATABASE_URL": "postgresql://postgres:${GENERATED_DB_PASSWORD}@postgres:5432/scanopy",
"SCANOPY_WEB_EXTERNAL_PATH": "/app/static",
"SCANOPY_INTEGRATED_DAEMON_URL": "http://daemon:60073"
},
"image": "ghcr.io/scanopy/scanopy/server:latest",
"ports": [
"60072:60072"
],
"restart": "unless-stopped",
"volumes": [
{
"source": "data",
"target": "/data",
"read_only": false
}
]
}
}
],
"top_level": {
"name": "scanopy",
"volumes": {
"daemon-config": null,
"data": null,
"postgres_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": "daemon-through-selected-proxmox-bridge; postgres-private-only",
"prompt_user_for_private_network": false
},
"storage": [
{
"id": "daemon-volume-0",
"service": "daemon",
"container_path": "/root/.config/scanopy/daemon",
"mode": "managed-volume",
"user_selectable": false,
"backup": true,
"shared_with_other_lxc": false,
"source_path": null,
"source_path_prompt": null
},
{
"id": "postgres-volume-0",
"service": "postgres",
"container_path": "/var/lib/postgresql/data",
"mode": "managed-volume",
"user_selectable": false,
"backup": true,
"shared_with_other_lxc": false,
"source_path": null,
"source_path_prompt": null
},
{
"id": "server-volume-0",
"service": "server",
"container_path": "/data",
"mode": "managed-volume",
"user_selectable": false,
"backup": true,
"shared_with_other_lxc": false,
"source_path": null,
"source_path_prompt": null
}
],
"orchestration": {
"reserve_vmids_atomically": 3,
"start_order": [
"daemon",
"postgres",
"server"
],
"stop_order": [
"server",
"postgres",
"daemon"
],
"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": "db-password",
"strategy": "generate-cryptographically-random-at-install",
"bindings": [
{
"service": "postgres",
"environment_variable": "POSTGRES_PASSWORD"
},
{
"service": "server",
"environment_variable": "SCANOPY_DATABASE_URL"
}
]
}
]
}
},
"first_run": {
"endpoints": [
{
"label": "Web interface",
"scheme": "http",
"port": 60072,
"path": "/",
"source": "official-compose"
}
],
"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"
}
],
"custom_compose_stack": true,
"custom_compose_interpolated": false,
"stack_image_entrypoints": {
"ghcr.io/scanopy/scanopy/server:latest": []
},
"installer_profile": {
"stack_driver": "generic-multi-lxc-stack",
"volume_preparations": [
{
"container_path": "/root/.config/scanopy/daemon",
"remove_lost_found": true,
"owner_strategy": "mapped-application-user",
"only_when_mount_type": "managed-volume"
},
{
"container_path": "/var/lib/postgresql/data",
"remove_lost_found": true,
"owner_strategy": "mapped-application-user",
"only_when_mount_type": "managed-volume"
},
{
"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 create the first account: the daemon registers by itself and starts the first scan."
]
},
"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-local-2026-10-31",
"clean_install": "pending",
"service_health": "pending",
"restart_persistence": "pending",
"backup_restore": "pending",
"update_preserves_data": "pending"
},
"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
}
}
}
+2 -1
View File
@@ -28,6 +28,7 @@
"poznote",
"rclone",
"roonserver",
"scanopy",
"statping",
"stremio",
"strfry",
@@ -35,7 +36,7 @@
"tandoor",
"tor"
],
"generated_count": 34,
"generated_count": 35,
"failed": [],
"failed_count": 0
}
+33
View File
@@ -14447,6 +14447,39 @@
"hidden": false,
"template_id": "linuxserver-sabnzbd"
},
{
"id": "scanopy",
"provider": "scanopy",
"template_family": "curated-profile",
"title": "Scanopy",
"repository": "https://github.com/scanopy/scanopy",
"description": "Scanopy discovers the hosts and services of your network and keeps its diagrams up to date. This recipe installs the server, PostgreSQL and the scanning daemon, each in its own Proxmox OCI LXC.",
"website": "https://scanopy.net/",
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/scanopy.webp",
"architectures": [
"amd64",
"arm64"
],
"updated_at": "2026-10-30",
"main_image": "ghcr.io/scanopy/scanopy/server:latest",
"category": "network",
"category_label": "Network & Firewall",
"replaces_discovered_ids": [],
"curated_path": "catalog/curated/scanopy.json",
"template_id": "image-scanopy",
"template": "apps/scanopy.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": "scrutiny",
"provider": "AnalogJ",
+5 -1
View File
@@ -824,7 +824,11 @@ def candidate_host_sources(journal, state):
def stop(vmid):
if run('pct', 'status', str(vmid)).strip() == b'status: running':
run('pct', 'shutdown', str(vmid), '--timeout', '60')
try:
run('pct', 'shutdown', str(vmid), '--timeout', '60')
except RuntimeError:
# What `docker stop` does with a process that ignores the signal.
run('pct', 'stop', str(vmid))
if run('pct', 'status', str(vmid)).strip() != b'status: stopped':
raise ValueError(translate('The container did not stop; its disks are not touched'))
+54
View File
@@ -0,0 +1,54 @@
"""Scanopy keeps its three services as native OCI LXCs, without host networking."""
import json
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[1]
class ScanopyRecipeTests(unittest.TestCase):
def setUp(self):
self.curated = json.loads((ROOT / "catalog/curated/scanopy.json").read_text(encoding="utf-8"))
self.generated = json.loads((ROOT / "catalog/apps/scanopy.json").read_text(encoding="utf-8"))
self.services = {service["name"]: service for service in self.curated["compose_stack"]["services"]}
def test_generated_catalog_copy_matches_curated_profile(self):
self.assertEqual(self.curated, self.generated)
def test_the_server_is_the_main_service(self):
self.assertEqual(set(self.services), {"daemon", "postgres", "server"})
self.assertEqual(self.curated["compose_stack"]["main_service"], "server")
self.assertEqual(self.services["server"]["depends_on"], ["daemon", "postgres"])
def test_only_postgres_stays_off_the_lan(self):
self.assertTrue(self.services["daemon"]["frontend_network"])
self.assertTrue(self.services["server"]["frontend_network"])
self.assertFalse(self.services["postgres"]["frontend_network"])
def test_the_daemon_needs_no_docker_only_settings(self):
compose = self.services["daemon"]["compose"]
self.assertNotIn("network_mode", compose)
self.assertNotIn("privileged", compose)
self.assertNotIn("docker.sock", json.dumps(compose))
def test_the_daemon_stops_with_the_signal_it_handles(self):
self.assertEqual(self.services["daemon"]["compose"]["stop_signal"], "SIGINT")
def test_the_daemon_is_checked_where_it_answers(self):
self.assertIn("/api/health", json.dumps(self.services["daemon"]["compose"]["healthcheck"]))
def test_the_database_password_is_generated_and_shared(self):
password = "${GENERATED_DB_PASSWORD}"
self.assertEqual(self.services["postgres"]["compose"]["environment"]["POSTGRES_PASSWORD"], password)
self.assertIn(password, self.services["server"]["compose"]["environment"]["SCANOPY_DATABASE_URL"])
secrets = self.curated["compose_stack"]["installer_inputs"]["generated_secrets"]
self.assertEqual(len(secrets), 1)
self.assertEqual({binding["service"] for binding in secrets[0]["bindings"]}, {"postgres", "server"})
def test_no_password_of_the_test_install_remains(self):
self.assertNotIn("pmxLab", json.dumps(self.curated))
if __name__ == "__main__":
unittest.main()
+17
View File
@@ -64,6 +64,23 @@ class TagSetInProxmox(unittest.TestCase):
{"bridge": "vmbr1", "vlan_tag": 4})
class Stopping(unittest.TestCase):
def test_a_container_that_ignores_the_shutdown_is_stopped(self):
calls, states = [], [b"status: running", b"status: stopped"]
def run(*args):
calls.append(args[:2])
if args[1] == "status":
return states.pop(0)
if args[1] == "shutdown":
raise RuntimeError("container did not stop")
return b""
with patch.object(transaction, "run", run):
transaction.stop(108)
self.assertIn(("pct", "stop"), calls)
class Kept(unittest.TestCase):
def test_the_update_names_the_tag_it_keeps(self):
import oci_update_current as update