- "Install an image that is not in the catalog" also takes an oci-compose pasted in the terminal, next to reading it from a file or downloading it from an address. - oci/README.md and the documentation page of the option describe the three ways, and the README links to the thread for suggesting applications (#440). - oci/VALIDATION.md points to discussion #440 instead of the closed #360. - The oci-compose tests pass on a host set to another language and without python3-jsonschema.
OCI manager Apps
OCI manager Apps runs official OCI images, the ones published for Docker, as native Proxmox VE LXC containers. No Docker engine is installed inside them: Proxmox VE imports the filesystem and the process metadata of the image, and the application becomes the main process of an LXC that is managed like any other container of the node.
The image stays the one its maintainer publishes. What ProxMenux adds is the environment Docker Compose would have created around it: persistent data, network, variables, devices and start order, written with Proxmox VE primitives.
This document describes the engine in this directory: how it is organised, how a container is created and updated, how a Compose file or a docker run command becomes an oci-compose, and how to write an oci-compose by hand and install it. The user documentation, with screenshots, is the OCI manager Apps section of the ProxMenux documentation.
- The model
- Structure
- How a container is created
- Update, modify, recreate and remove
- From a Compose file or a docker run command to an oci-compose
- The oci-compose, block by block
- Writing and testing an oci-compose
- Adding an application to the catalog
- Design and laboratory notes
The model
One OCI image becomes one native LXC.
| Requirement | Docker expresses it as | OCI manager Apps turns it into |
|---|---|---|
| Run the application | image, entrypoint, command |
The rootfs built from the image and the main process of the LXC |
| Keep data | volumes |
A container disk (mpN) included in the backup, or a host directory |
| Publish the service | ports |
An address of its own for the LXC; the service answers on its original port |
| Use hardware | devices, group_add, runtime: nvidia |
devN entries with the group of the host device, and a reviewed profile |
| Connect dependencies | networks, depends_on |
One LXC per service, a private bridge, fixed addresses, start order and healthchecks |
| Update | pull and recreate |
A new rootfs with the same data, network and devices |
A Compose key with no safe equivalent is not dropped: it is listed as a blocker and the application is not offered for installation until it is resolved.
Structure
oci/
├── catalog/
│ ├── index.json list of applications shown by the menu
│ ├── apps/<app>.json the oci-compose generated for each image
│ ├── curated/<app>.json an oci-compose written and reviewed by hand
│ ├── overlays/<app>.json adjustments laid over a generated oci-compose
│ ├── categories.json category of each application
│ ├── volume-policy.json which paths hold content of the user
│ └── verification.json applications tested for real
├── examples/ oci-compose files written by hand
├── schemas/ the JSON schema every oci-compose follows
├── src/proxmenux_oci/ menu, converter and deployment plan
├── remote/ what creates and maintains the LXC on the node
└── tests/
An oci-compose is the JSON file an application is installed from. Its kind is proxmenux.oci-template. It says which image runs, which ports, variables and volumes it has, what the installation asks, and how each thing is done in Proxmox VE.
There are three places an oci-compose comes from:
- The catalog. The converter writes
catalog/apps/<app>.jsonfrom the Compose file the project of the image publishes. When that file needs something Compose cannot say, such as a question, a generated password or a GPU profile, it goes incatalog/overlays/<app>.jsonand is laid over the generated file each time it is read. An application whose definition is written whole by hand lives incatalog/curated/. - A definition given at the moment. The option Install an image that is not in the catalog translates a Compose file, a
docker runcommand or an image reference with the same converter. - A file written by hand, read by the same option. It is described in Writing and testing an oci-compose.
On a node, the engine is installed in /usr/local/share/proxmenux/oci/engine/. Every installation keeps its record in /usr/local/share/proxmenux/oci/instances/<VMID>/oci-compose.json: the oci-compose it was installed from, under template, together with the answers given during the installation and the configuration Proxmox VE ended with. Updates, changes and removals read that record.
How a container is created
Selecting an application, or giving a definition, opens the same sequence.
- Read. The oci-compose is composed and checked against the schema. One with blockers is not offered.
- Ask. Install with default settings asks what every installation decides: where the data of the user goes, the address, required values with no default, and whether it starts with the node. Install with advanced settings opens every decision: VMID, cores, memory, storage of each path, bridge, optional variables and devices.
- Review. A summary shows image, resources, storage, each path and where it goes, network, acceleration, security and variables, with secrets masked. Nothing has been created yet.
- Resolve. The registry is queried, the architecture of the host is selected and the digest the tag points to at that moment is fixed.
- Download and verify. Skopeo downloads the image as an OCI archive. Every layer is checked against its digest before anything is created.
- Build.
pct createbuilds the rootfs from the archive and importsEntrypoint,Cmd,Env,User,WorkingDirandStopSignalfrom the image. - Connect. The volumes are added as mount points, then the devices, the environment, the network and the security settings the oci-compose declares.
- Prepare. Each new volume is made ready for the first start:
lost+foundis removed and the directory is given to the user the application writes with. The files the oci-compose asks to write are written. - Start and check. The container starts, an address is waited for, the settings that need the running application are applied, and the service is checked. A failed first start is reported as a failed installation.
- Register. The record of the installation is written, and a copy travels inside the container so that a backup carries it.
The result is an ordinary LXC:
entrypoint: /jellyfin/jellyfin
hostname: jellyfin
cores: 2
memory: 2048
unprivileged: 1
rootfs: local-zfs:subvol-126-disk-0,size=8G
mp0: local-zfs:subvol-126-disk-1,mp=/config,backup=1,size=16G
mp1: local-zfs:subvol-126-disk-2,mp=/cache,backup=1,size=16G
mp2: local-zfs:subvol-126-disk-3,mp=/media,backup=1,size=32G
dev0: path=/dev/dri/renderD128,mode=0660,deny-write=0,gid=993
net0: name=eth0,bridge=vmbr0,firewall=1,host-managed=1,ip=dhcp,type=veth
tags: media;oci;proxmenux
An application made of several images is one entry of the catalog and one installation. It creates one LXC per service on a private bridge, with fixed addresses and the names the services use to find each other, and starts them in order.
Update, modify, recreate and remove
Manage installed OCI applications lists the registered installations. Before any action a container is compared with its record; one that no longer matches is neither modified nor deleted.
| Operation | What changes | What is kept |
|---|---|---|
| Update | The rootfs is replaced by the image the saved tag publishes today | Record, container disks, host directories, network and devices |
| Modify | An editor opens with the current resources, network, paths and GPU; the container is rebuilt with the changes | The data of container disks and host directories |
| Recreate (multi-container) | Every container is rebuilt from the image it was installed with | Data, network and devices |
| Watchdog | Whether the application is started again when it stops on its own | Everything; only the choice is saved |
| Remove | The LXC, or every container of the application, its disks and its record | Host directories, with their content |
An update is a transaction:
- The tag is resolved. When the registry still serves the installed digest, nothing is downloaded and the container is not touched.
- The new image is downloaded and verified, and the operation checks that the backup fits, before the container stops.
- The container is stopped and backed up.
- The new rootfs is imported and the record is applied again: mount points, environment, network, devices and security.
- The application is checked. When it answers, the record is published with the new digest. When it does not, the previous container is restored from the backup.
Settings changed in Proxmox VE after the installation, such as memory, cores or start with the node, are carried into the new record. An operation interrupted by a closed terminal or a reboot is detected the next time the menu opens, and the previous installation can be recovered. A container restored from a backup on another host is registered again from the copy of the record it carries.
A rolling tag is not an unattended update: the digest changes only through an explicit update.
From a Compose file or a docker run command to an oci-compose
The converter reads a Compose file with one service. A docker run command and an image reference alone are first written as a Compose file and follow the same path, so the three produce the same kind of oci-compose.
Starting from this definition:
services:
app:
image: ghcr.io/example/app:latest
ports:
- 8080:8080
volumes:
- ./config:/config
environment:
- TZ=Europe/Madrid
- API_KEY=
restart: unless-stopped
each key becomes a field of the oci-compose:
| Compose key | Field of the oci-compose | Result in Proxmox VE |
|---|---|---|
image |
container_contract.image |
The image that is downloaded; its tag is resolved to a digest at each installation |
ports |
container_contract.ports, first_run.endpoints, catalog_ui.launch |
The LXC has its own address, so the published port is not used: the service answers on the container port. The first TCP port is taken as the web address |
volumes |
container_contract.volumes, proxmox.installer_profile.volume_preparations |
A container disk or a host directory for each path |
environment |
container_contract.environment |
Variables of the LXC. A name that reads as a password, token or key is asked as a secret |
restart |
container_contract.restart |
The watchdog is proposed by default |
mem_limit |
proxmox.installer_profile.resources.memory_default_mb |
The memory proposed for the LXC |
cpu_shares |
proxmox.installer_profile.resources.cpu_shares |
cpuunits of the LXC |
ulimits |
proxmox.installer_profile.resources.rlimits |
lxc.prlimit.* |
shm_size |
proxmox.installer_profile.tmpfs_mounts |
A tmpfs at /dev/shm of that size |
user, working_dir, entrypoint, command, hostname, group_add |
proxmox.installer_profile.runtime |
The process the LXC starts, its user, directory and groups |
extra_hosts |
proxmox.installer_profile.extra_hosts |
Entries of /etc/hosts in the container |
devices |
proxmox.installer_profile.device_requests |
A devN entry for each device, confirmed during the installation |
runtime: nvidia |
proxmox.installer_profile.device_requests |
The NVIDIA devices and the driver files of the host |
healthcheck |
proxmox.installer_profile.startup_healthcheck |
The check of the first start. It runs during installations and updates, not continuously |
cap_add, sysctls, security_opt |
proxmox.installer_profile.security |
Capabilities checked against the LXC model, lxc.sysctl entries and AppArmor or seccomp settings, each confirmed |
privileged |
proxmox.security_profile |
The LXC stays unprivileged; the privileged mode is offered as an explicit choice |
network_mode |
proxmox.installer_profile.network |
host, bridge and default all mean the network of the LXC, never the one of the Proxmox host |
stop_grace_period |
proxmox.defaults.shutdown_timeout_seconds |
The time a shutdown waits |
labels, logging |
Kept in container_contract.original_compose only |
Not applied: they are read by Docker tools |
What has no translation stops the installation and is named: pid: host, a Compose file with several services given to this option, configs, external secrets, a build section without a published image, and shell substitutions such as $(command).
Besides those fields, the converter writes the blocks that describe the translation itself: where the definition came from, the keys it understood, the adaptations it applied and the state of each validation. They are listed in Blocks ProxMenux writes.
The oci-compose, block by block
A complete oci-compose has around 550 lines, and most of them are written by the converter. One written by hand contains only what its author decides; ProxMenux completes the rest when it reads the file.
A file written this way describes one image. An application made of several images is read from its complete file, which carries the compose_stack block with its services.
Two files of examples/ are used below. Both install the same as the entry of the catalog with the same name:
jellyfin-official.oci-compose.json: two ports, an optional variable, three volumes with different owners, a GPU profile, a setting applied after the first start and a check of the service.duplicati-official.oci-compose.json: two secrets generated during the installation and the credentials shown at the end.
The smallest valid file names the image and nothing else:
{
"kind": "proxmenux.oci-template",
"container_contract": {
"image": { "reference": "traefik/whoami:latest" }
}
}
kind
Always "proxmenux.oci-template". It is how the file is recognised.
catalog_ui: how the application is presented
"catalog_ui": {
"title": { "en_US": "Jellyfin Official" },
"tagline": { "en_US": "Official Jellyfin image with optional VA-API or NVIDIA acceleration." },
"category": "media",
"icon": "https://raw.githubusercontent.com/linuxserver/docker-templates/master/linuxserver.io/img/jellyfin-icon.png",
"website": "https://jellyfin.org/",
"documentation": "https://jellyfin.org/docs/general/installation/container/",
"repository": "https://github.com/jellyfin/jellyfin-packaging"
}
| Field | What it is |
|---|---|
title |
The name shown in the menu, in the notes of the container and in ProxMenux Monitor. The name of the installation is taken from it |
tagline, description |
One line and a longer text, shown on the page of the application |
category |
One of the keys of catalog/categories.json: media, backup, network, databases, documents, downloads, sharing, monitoring, smarthome, security, ai, tools, misc and the others listed there |
icon |
Address of the logo, used in the notes of the container and in the Monitor |
website, documentation, repository |
Links of the project. The installation shows the documentation link when it asks about an optional variable or names a service the application needs |
launch |
scheme, port and path of the web interface. "port": null declares a service with no web interface, together with an empty first_run.endpoints |
Without this block the title is taken from the name of the image and the category is misc.
container_contract: what the image needs
This block is the equivalent of the service of a Compose file.
"container_contract": {
"service_name": "jellyfin",
"container_name": "jellyfin",
"image": { "reference": "jellyfin/jellyfin:latest" },
"ports": [
{ "container_port": 8096 },
{ "container_port": 7359, "protocol": "udp", "required": false }
],
"environment": [
{ "name": "JELLYFIN_PublishedServerUrl", "required": false,
"prompt": "Optional published URL for Jellyfin" }
],
"volumes": [
{ "container_path": "/config" },
{ "container_path": "/cache" },
{ "container_path": "/media", "default": "host-bind", "managed_volume": { "default_size_gb": 32 } }
],
"restart": "unless-stopped"
}
image.reference is the only required field of the file: the image as it would be written after docker pull, with its tag. The registry, the repository and the tag are worked out from it.
service_name and container_name are the name of the service and the hostname proposed for the LXC. Without them both come from the title.
ports. One entry for each port the application listens on.
| Field | Default | What it is |
|---|---|---|
container_port |
— | The port inside the container. It is the one the service answers on |
protocol |
tcp |
tcp or udp |
required |
true |
false marks a port the application works without; the summary shows it as optional |
The first TCP port is taken as the web address unless first_run.endpoints says otherwise.
environment. One entry for each variable the installation sets.
| Field | Default | What it is |
|---|---|---|
name |
— | The name of the variable |
example |
empty | The value proposed. A variable with a value is installed with it in default mode and can be changed in advanced mode |
required |
true |
A required variable with no value is asked in both modes. An optional one is offered only in advanced mode |
sensitive |
false |
true asks the value hidden, asks it twice and masks it in the summary |
prompt |
the name | The text of the question |
container_address |
— | A value built from the address the container gets, written with the mark @CONTAINER_ADDRESS@, for example "http://@CONTAINER_ADDRESS@". With a static address it is known before the installation; with DHCP it is resolved when the container gets its lease |
The variables the image itself defines are imported with it and are not written here.
volumes. One entry for each path that has to survive an update.
| Field | Default | What it is |
|---|---|---|
container_path |
— | The path inside the container |
installation_choice |
see below | Where the path may go: managed-volume (a container disk), host-bind (a host directory) |
default |
managed-volume |
The choice proposed |
managed_volume.default_size_gb |
16, or 8 for a path with content of the user | The size proposed for the container disk |
managed_volume.backup |
true |
Whether the disk is part of the backup of the container |
read_only |
false |
Mounts the path read-only |
Where a path goes follows one rule. A path that holds content of the user, such as /media, /downloads, /music or /backups, is offered as a container disk or as a host directory, and the installation asks which. The list is in catalog/volume-policy.json. Every other path is state of the application and goes to a container disk without asking. Writing "installation_choice": ["managed-volume", "host-bind"] or "default": "host-bind" on a volume offers the host directory for a path that is not in that list.
restart. With unless-stopped, always or on-failure, the watchdog is proposed by default: an application that stops on its own is started again.
first_run: what is shown when the installation ends
"first_run": {
"endpoints": [
{ "label": "Jellyfin WebUI", "scheme": "http", "port": 8096, "path": "/",
"source": "official-container-documentation" }
]
}
endpoints are the addresses printed at the end, written in the notes of the container and opened from the Monitor. Each one needs label, scheme, port, path and source, a free text that says where the address is documented. An empty list declares a service with no web interface: its ports are then shown as IP:port text and no link is offered.
credentials are the logins printed at the end. From the Duplicati example:
"credentials": [
{ "label": "Duplicati web interface (password only)",
"type": "configured-or-installer-generated",
"username": "Not required (password only)", "password": null,
"password_environment": "DUPLICATI__WEBSERVICE_PASSWORD",
"change_required": false, "source": "duplicati-documentation", "retrieval": null }
]
type |
Meaning |
|---|---|
static-default |
A login the image documents, written in username and password |
configured-or-installer-generated |
The value of the variable named in password_environment, typed or generated during the installation |
runtime-generated |
A password the application prints at its first start; retrieval says how it is read |
proxmox.defaults: the container that is proposed
"proxmox": {
"defaults": { "memory_mb": 2048 }
}
Only what differs from the defaults is written. The defaults are 2 cores, 1024 MB of memory, 512 MB of swap, an 8 GB rootfs, an unprivileged LXC with nesting=1, DHCP on vmbr0, the firewall on, and no start with the node. The fields are cores, memory_mb, swap_mb, rootfs_size_gb, unprivileged, features, onboot and shutdown_timeout_seconds. The storage and the bridge proposed are the ones of the node.
proxmox.installer_profile: what Compose cannot say
This block holds the decisions of the installation. Each part is optional.
generated_sensitive_environment: secrets the installation generates.
"generated_sensitive_environment": {
"SETTINGS_ENCRYPTION_KEY": { "strategy": "token-hex", "bytes": 16, "prompt": false },
"DUPLICATI__WEBSERVICE_PASSWORD": { "strategy": "token-hex", "bytes": 12, "prompt": true }
}
The variable is declared in environment with "sensitive": true. In default mode its value is generated. In advanced mode a variable with "prompt": true is asked, and left empty it is generated. The generated value is shown at the end when first_run.credentials names the variable.
volume_preparations: who owns each new volume.
"volume_preparations": [
{ "container_path": "/config", "remove_lost_found": true,
"owner_strategy": "mapped-root", "only_when_mount_type": "managed-volume" },
{ "container_path": "/media", "remove_lost_found": true,
"owner_strategy": "mapped-application-user", "only_when_mount_type": "managed-volume" }
]
A new container disk is an empty filesystem owned by root that carries a lost+found directory. owner_strategy gives it to mapped-root, the root of the container, for an image that starts as root and takes ownership of its directories itself, or to mapped-application-user, the user 1000 the application writes with. Without this list every volume is prepared for mapped-application-user.
startup_healthcheck: how the first start is checked.
"startup_healthcheck": {
"scheme": "http", "port": 8096, "path": "/System/Info/Public",
"timeout_seconds": 180, "request_timeout_seconds": 10,
"stability_seconds": 4, "verify_tls": false
}
The installation and every update wait up to timeout_seconds for that address to answer, and then stability_seconds more to see that it keeps answering. An update that does not pass the check is undone. Without this block the web address is checked, or that the container keeps running when there is no web address.
hardware_acceleration: the choice of GPU.
"hardware_acceleration": {
"prompt": "Hardware acceleration for Jellyfin",
"default": "none",
"profiles": [
{ "id": "none", "label": "No acceleration (CPU)", "device_requests": [] },
{ "id": "vaapi", "label": "Intel/AMD (VA-API)",
"device_requests": [
{ "id": "vaapi-render", "kind": "character-device", "purpose": "vaapi",
"path_prompt": "GPU render device", "host_path_default": "/dev/dri/renderD128",
"container_path_strategy": "same-as-host", "mode": "0660", "deny_write": false,
"gid_strategy": "host-device-gid",
"environment": [
{ "name": "LIBVA_DRIVER_NAME", "prompt": "VA-API driver",
"choices": ["auto", "radeonsi", "iHD", "i965"], "default": "auto", "omit_values": ["auto"] }
] }
] },
{ "id": "nvidia", "label": "NVIDIA (NVENC/NVDEC)",
"device_requests": [
{ "id": "nvidia-runtime", "kind": "nvidia-runtime", "purpose": "nvidia-cuda-nvenc-nvdec",
"device_selection": "all-requested-by-compose" }
],
"environment": [ { "name": "NVIDIA_VISIBLE_DEVICES", "value": "all" } ] }
]
}
The installation shows one option for each profile the host can serve: a profile whose device is not on the host is not listed. Each profile declares the devices it passes and the variables it sets.
| Field of a device request | What it is |
|---|---|
kind |
character-device for a device node such as a render node, or nvidia-runtime for an NVIDIA GPU with the driver files of the host |
host_path_default, path_prompt |
The device proposed and the text that asks for it |
container_path_strategy |
same-as-host keeps the path of the host inside the container |
mode, gid_strategy |
The permissions of the device. host-device-gid reads the group from the device of the host, so no fixed video or render number is assumed |
environment |
Variables asked with the device, with choices and the values that are left unset (omit_values) |
A profile may also change the image, with image, for projects that publish one tag for each GPU vendor, and apply settings once the application is running, with post_start_configurations. In the Jellyfin example the VA-API and NVIDIA profiles write the transcoding and tone mapping settings to encoding.xml after Jellyfin creates it.
Other parts of the profile.
| Part | What it does |
|---|---|
completion_notes |
A list of texts printed when the installation ends, for what is left to do by hand |
external_services |
Names of services the application needs already running, such as ["MariaDB"]. They are announced before the installation asks how to reach them |
generated_files |
Files written in the container before its first start, each with container_path, content, owner and mode. With "only_if_missing": true a file that already exists in a kept volume is not replaced |
volume_owner |
uid and gid of the user the application writes with, when it is not 1000 |
non_persistent_image_volumes |
Paths the image declares as volumes that are left inside the rootfs |
runtime, resources, security, device_requests, tmpfs_mounts, extra_hosts |
What the Compose keys of the translation table become |
proxmox.security_profile: privileges
Without this block the LXC is unprivileged with the default AppArmor profile. "optional_privileged_lxc": true offers the privileged mode as a choice that defaults to no; "requires_privileged_lxc": true asks for it with a confirmation that states the risk. A GPU, a USB device or a serial port are passed as individual devices and need neither.
Blocks ProxMenux writes
These blocks are part of every complete oci-compose. They are not written by hand: when a file does not bring them, they are added as the converter writes them for the same image, ports, variables and volumes.
| Block | What it holds |
|---|---|
schema_version, id, status |
The version of the format, the identifier and the state of the review. laboratory-validated is given only by catalog/verification.json: a file that declares itself verified is read as not verified |
source |
Where the definition came from and when it was generated |
container_contract.original_compose |
The Compose file the oci-compose was translated from |
compose_stack |
The services of the application, their order and their private network. For one image it describes a single service |
compatibility |
The Compose keys that were understood and the blockers left |
proxmox.adaptations |
Each difference with Docker, its reason and its effect |
validation, lifecycle |
The state of each test and how updates are detected |
What the author writes always wins over what is added.
Writing and testing an oci-compose
-
Write the file. Start from one of the
examples/or from the smallest file above, and add the blocks the application needs. Any file ofcatalog/apps/orcatalog/curated/is also a valid starting point. -
Take it to the node. It can be pasted in the terminal, copied to the node, for example to
/root/oci-compose.json, or published where the node can download it. -
Open OCI manager Apps from the ProxMenux menu and choose Install an image that is not in the catalog.
-
Choose how it is given: Paste an oci-compose in the terminal, Read an oci-compose from a file of this host or Download an oci-compose from an address.
-
Read the result. A file with errors is not installed, and each error names the field:
The oci-compose has errors: container_contract/ports: array container_contract/volumes/3/container_path: required first_run/endpoints/0: 'label' is a required propertyA file that is not valid JSON reports the line and the column of the fault.
-
Review. A valid file shows what it installs: image, web address, ports, persistent paths, the variables the installation asks for and the files it writes. The image is looked up in its registry, and its architectures are listed. Answering no leaves the node untouched.
-
Install with default or advanced settings. The installation is the one of any application of the catalog.
-
Adjust. Remove the test installation from Manage installed OCI applications, edit the file and read it again.
An oci-compose decides what is mounted and with which privileges, so the review says it before installing. Install only files that come from a source you trust.
The record of an installation, /usr/local/share/proxmenux/oci/instances/<VMID>/oci-compose.json, can be read the same way: only its template block is used, as it was installed, and none of the answers of that installation. That record also holds the values typed during the installation, passwords included, so it is not a file to publish as it is.
The checks of the engine run with:
cd oci
python3 -m unittest discover -s tests -p 'test_*.py'
tests/test_install_from_oci_compose.py checks, among other things, that the two files of examples/ install what the catalog installs.
Adding an application to the catalog
An application reaches the catalog in three steps.
- Suggest it in the thread Suggest an application for the OCI catalog with the image, the registry it is published in and the category.
- Try it. Installing it from its Compose file or its
docker runcommand shows whether the image runs as a native LXC. When it needs something Compose cannot say, an oci-compose written by hand lets that be tested as well. The file that worked, attached to the suggestion, is what the entry of the catalog is written from. - Validate it. An application tested for real is recorded in
catalog/verification.jsonand shown as verified in the installer. VALIDATION.md lists the verified applications and describes a test: install, check that it works, restart, update and remove.
The sections that follow are the design and laboratory notes of the engine.
Design
catalog/index.jsonis the lightweight application listing.catalog/apps/<app>.jsonis the canonical template for one image.schemas/oci-template.schema.jsonvalidates generated templates.container_contractpreserves the official Compose contract and source text.catalog_uicontains neutral store metadata and attributes each image to its actual image repository or publisher.first_runrecords detected web endpoints, documented default credentials, and supported methods for recovering credentials generated at runtime.proxmoxcontains only native OCI/LXC translation and documented adaptations.- A multi-image application remains one catalog entry and one user-facing
installation. Its
compose_stackcreates one native OCI LXC per service, allocates a private network automatically and orchestrates dependencies. - Discovery catalogs are never recorded as image authors or repositories in public templates.
- Imported store text keeps only
en_USandes_ES; when Spanish is missing,es_ESfalls back to English instead of retaining unused source locales. - Distributions and deployment profiles remain separate entries. For example,
nextcloudis the LinuxServer image,nextcloud-officialis the official single-image deployment andnextcloud-stackis the laboratory-derived Nextcloud, PostgreSQL and Redis profile. The namenextcloud-aiois reserved for the distinct upstream All-in-One project. - Hardware choices share one application image: LinuxServer
jellyfinoffers CPU, VA-API, AMD/OpenCL, Intel/OpenCL and NVIDIA in its installer profile. OpenCL choices use official LinuxServer mods, not separate image variants. See Jellyfin GPU laboratory for tested capabilities, native OCI device permissions and the distinction between VA-API, OpenCL and Vulkan tone mapping. - Curated laboratory profiles can replace a duplicate discovery identifier.
jdownloaderuses the maintained JDownloader 2 image from jlesage and replaces the less clear discovered namejdownloader2; these are not two different generations of JDownloader. - The curated
frigateprofile replaces the generic discovered deployment and preserves the AMD VA-API and OpenVINO CPU choices. The installer keeps the official rollingstableimage intact; package substitutions require a separate validation on stable hardware and are not enabled automatically. - The curated
paperless-ngxprofile translates the official PostgreSQL Compose into three native LXCs: Paperless-ngx, PostgreSQL and Valkey. Private state uses backed-up Proxmox volumes, whileconsumeandexportcan use either managed volumes or host directories shared with scanners and other applications. - The curated
rcloneprofile uses a two-phase workflow. Installation starts the official authenticated WebUI so the user can create and authorize a private remote. The separaterclone-mountaction then validates that remote, enables the official FUSE mount and publishes distinct read/write and recursively read-only paths for other native LXCs.
Catalog maintenance
The catalog is generated rather than written by hand. A contributor adding an application runs the orchestrator directly from this directory:
export PYTHONPATH=src
python3 -m proxmenux_oci sync
python3 -m proxmenux_oci list --filter sonarr
python3 -m proxmenux_oci generate sonarr
python3 -m proxmenux_oci show sonarr
GITHUB_TOKEN=github_pat_xxx python3 -m proxmenux_oci generate-all
generate writes one application's template from its published recipe; show
prints what the installation would create, which is the fastest way to see
whether a translation came out right before installing anything.
It needs python3-yaml and python3-jsonschema. On Debian and Proxmox both
are distribution packages; install them with APT before generating the catalog.
GITHUB_TOKEN is optional and only raises the public API rate limit, which the
full generate-all pass reaches. Never commit a token; .env files are
ignored.
Safety boundary
Automatic installation is allowed only when every Compose behavior has a
reviewed and tested native Proxmox translation. Multi-image applications are
modeled as one installation; generic stacks remain blocked until their native
multi-LXC orchestrator is implemented and validated, while curated Immich,
Nextcloud and Paperless-ngx stacks have dedicated orchestrators. Privileged
mode and relaxed AppArmor/seccomp profiles require an explicit high-risk
confirmation. Security requirements are classified by capability instead of
assuming that every Compose privileged: true is an image requirement:
requires_privileged_lxcis reserved for a reviewed profile whose native LXC adaptation has proved that broad privilege is necessary.optional_privileged_lxcrecords an upstream compatibility request. The installer keeps the LXC unprivileged by default and offers the broader mode only after explicit confirmation.- GPU, USB and serial hardware are passed as individual Proxmox devices and do not imply a privileged LXC.
- Required Compose AppArmor/seccomp relaxations need explicit confirmation.
Relaxations marked
#optionalremain disabled by default and are offered as individual compatibility choices instead of being labeled as mandatory. pid: hostis tracked separately as host PID namespace access. It remains blocked until a safe native LXC translation is validated; it must never be mislabeled as ordinary GPU access or silently promoted to privileged mode.
Before creating an LXC, the installer shows the installation summary with every
secret masked and asks for confirmation: Install with this configuration? in
the menu and Install? [y/N] in the install command. Remote credentials are
handled by normal SSH; they are never placed in a template or command argument.
Image downloads run in a pseudo-terminal when script is available, allowing
Skopeo layer progress to remain visible. The final result lists the detected IP,
one complete URL per documented web endpoint, and any public default login from
the upstream LinuxServer Application Setup section. Non-web service ports are
not presented as browser URLs. Public default passwords should be changed after
the first login.
Some images do not publish a static password. For example, qBittorrent prints a
temporary password for admin during startup. When this behavior is explicitly
documented upstream, the installer enables a short-lived native LXC console log
for the first boot, extracts the password, removes the log and its temporary
configuration, and prints the credential in the terminal. The generated secret
is never written back to the reusable catalog JSON or to Proxmox metadata.
When run as root directly on a Proxmox node, destination auto selects local
execution and does not open an SSH connection back to the same node. Outside
Proxmox, specify root@<node-address> using --host or the interactive prompt;
the installer never assumes a laboratory address.
Native Proxmox behavior
Proxmox VE 9 imports OCI Entrypoint, Cmd, Env, User, WorkingDir and
StopSignal when pct create extracts the OCI archive. The installer preserves
that behavior and only overlays values explicitly present in Compose.
Proxmox VE 9.2 cannot extract the tested OCI archive directly as a privileged LXC. When a reviewed profile explicitly requires privileges, the installer imports it with the standard unprivileged idmap, converts ownership before the first start while preserving extended attributes, and only then switches the native LXC configuration to privileged mode. This conversion is never applied without the user's high-risk confirmation.
Compose stop_grace_period is recorded as the timeout for ProxMenux-managed
pct shutdown operations. It is not mapped to Proxmox startup.down, because
that field controls sequencing between guests rather than the CT stop timeout.
Persistent paths can be installed as:
- Proxmox-managed
mpNvolumes with the image's original container path andbackup=1by default. - Existing host bind mounts for data intentionally shared with other LXCs.
- Omitted mounts only when LinuxServer marks them optional.
When a host bind is selected, the installer proposes
/mnt/oci-shared/<application>/<volume>. Missing data directories are created
automatically with ownership mapped for the unprivileged LXC; existing
directories are never re-owned. System files and runtime sockets are excluded
from automatic creation.
Hardware devices keep their host path and obtain their numeric GID directly
from the selected host device; ProxMenux does not assume fixed video or
render group IDs. NVIDIA profiles additionally require a working host driver
and NVIDIA Container Toolkit. At installation time, nvidia-container-cli
supplies the current device, binary, firmware and driver-library inventory;
ProxMenux translates it to native Proxmox devN entries and read-only LXC file
mounts, including the compatibility links expected by the image. No driver
version or library list is hardcoded in the template.
Compose network_mode: host means the network namespace of the dedicated LXC,
not the Proxmox host network. bridge and default use the same native LXC
model because no Docker NAT layer exists. Recognized cap_add values are
checked against the LXC capability model instead of using lxc.cap.keep, which
would accidentally remove other capabilities. WireGuard images that request
SYS_MODULE preload and verify the wireguard kernel module on the Proxmox
host while keeping the LXC unprivileged.
Namespaced IPv4/IPv6 Compose sysctls are written to an LXC include profile in
/etc/pve/lxc/<VMID>.proxmenux-sysctls; this is required because Proxmox 9.2
does not accept arbitrary network sysctl keys directly in the managed CT
configuration. The profile is stored on the Proxmox cluster filesystem and is
removed on a failed installation. Cross-host backup/restore validation remains
pending, like the versioned NVIDIA host-driver mounts.
seccomp:unconfined uses a valid empty LXC denylist profile, never /dev/null.
apparmor:unconfined is applied with its native LXC directive, and
no-new-privileges maps to lxc.no_new_privs. Docker's label:disable and
logging drivers remain source metadata because the native LXC runtime has no
Docker SELinux label or Docker log object. Compose supplementary groups are
resolved from the image's /etc/group and applied through lxc.init.groups.
The current laboratory implementation records versioned NVIDIA driver paths in the LXC configuration. After updating the NVIDIA host driver, those mounts must be regenerated before affected LXCs are started. Automatic profile refresh is part of the future update lifecycle and is not yet implemented.
For multi-image applications, users choose only normal deployment values such as storage destinations, frontend network and shared data paths. ProxMenux must reserve all VMIDs atomically, create the dependency network, assign internal addresses and service aliases, generate shared secrets, create every LXC and start dependencies in health-checked order. Private configuration and database paths use Proxmox-managed volumes with backup enabled; only intentionally shared user data uses host bind mounts.
Validated multi-LXC installers attach an official Proxmox hookscript to the
main LXC. ProxMenux only creates this lifecycle configuration during
installation; no ProxMenux daemon remains running. On every later pre-start,
Proxmox starts any stopped dependency in declared order and waits for its
healthcheck before allowing the main LXC to start. Stopping the main LXC does
not implicitly stop its dependencies, so a restart cannot interrupt a database
or cache before the application has shut down.
Host bind paths selected by the user are created when missing and need an
independent backup policy; no shared path is created unless host-bind was
chosen. The default LXC is unprivileged and uses an 8 GB thin-provisioned
rootfs.
Validation lifecycle
Generated templates start as generated-unvalidated. Promotion requires:
- Clean installation.
- Application health check.
- Restart persistence.
- Proxmox backup and restore.
- Image replacement with persistent volumes preserved.
- Review of every platform adaptation and unsupported feature.
Real tests are recorded in catalog/verification.json, which the catalog
reads when it loads and applies on top of the generated templates and the
overlays, so a new entry shows without regenerating and a regeneration keeps
it: "status": "laboratory-validated" for an application tested in the
ProxMenux lab, or "community_tested": {"by": "<GitHub user>", "date": "<YYYY-MM-DD>", "report": "<link to the report>"} for one tested by the
community. Both show as verified in the OCI installer.
VALIDATION.md lists the verified applications and explains
how to take part. It is generated from catalog/verification.json with
python3 .github/scripts/oci_validation.py render, and CI checks that it is
current.
The mini changelog comes from the LinuxServer README Versions section. At
installation, the architecture-specific registry digest and image labels are
recorded back into the local app JSON for future update comparisons.
Generic multi-LXC installer
The generic stack compiler now handles an application with official PostgreSQL
and Redis/Valkey dependencies. It reuses install_oci.sh for image verification
and native OCI import. Docmost and Blinko are the first eligible catalog entries;
real installation and restart validation of this new driver remain pending.
Each other stack records its unresolved semantics in proxmox.generic_stack_review.
The compiler resolves generated secrets once per stack and binds PostgreSQL URL
credentials to the named database service. The installer allocates a private
network, writes service aliases, creates data volumes with the image's initial
files and ownership, and registers the Proxmox dependency hook before starting
the main LXC. It replaces example localhost public URLs with the assigned LAN IP
after a successful first boot and performs a graceful restart to apply them.
Using a stable DHCP lease or a domain is necessary if that address later changes.
For the rolling official PostgreSQL image, legacy /var/lib/postgresql/data
mounts become /var/lib/postgresql, preserving its versioned data directory.
Allocation is serialized among ProxMenux installers on the node; native pct
creation checks still arbitrate collisions with concurrent external operations.
A failed generic stack keeps completed LXCs and their volumes for diagnosis.
After a partial failure, inspect these resources before starting a new install.
No claim of atomic cross-cluster allocation or coordinated backup is made.
The hook snippet and /etc/pve/priv/proxmenux-stack-<vmid>.json are host artifacts;
back them up separately and remap VMIDs/recreate the bridge when restoring a
whole stack on another host. A normal LXC backup alone does not package these
host artifacts. This restoration workflow is still pending validation.
Suite Arr (selectable media stack)
The suite-arr catalog entry offers Prowlarr, Sonarr, Radarr, qBittorrent,
Lidarr, Bazarr, SABnzbd, Seerr and Unpackerr. The first four are checked by
default. A separate single-choice menu offers Jellyfin (default), Plex, Emby
or no media server. Only selected services are created.
It reuses individual image templates, including the official Seerr and Unpackerr
images. Each /config (Seerr: /app/config) is a separate managed Proxmox volume
with backup enabled. Media applications share a user-selected host directory at
/data; media is not included
in container backups. New media directories receive mapped UID/GID 1000 ownership;
existing directories and their permissions are not recursively changed.
Web applications have LAN access and a private address for inter-service APIs.
Unpackerr has only a private address and no WebUI. Its initial start is deferred
until the selected Arr apps have generated API keys, then official UN_*
environment variables are persisted in its LXC config. Subsequent starts are independent and controlled by Proxmox onboot on each LXC.
Save the LXC configuration alongside application-volume backups.
The installer reads generated API keys, creates media root folders, and connects
selected Sonarr/Radarr instances to Prowlarr using its API schema. It does not
patch application binaries, add indexers, download content, disable authentication,
or install a VPN. First-login Arr authentication, indexers and quality profiles
still require user configuration. If qBittorrent is not selected, the download
client also needs manual configuration.
The shared tree is downloads/{tv,movies,music,incomplete,usenet,usenet-incomplete}
and media/{movies,series,music}. Subtitles reside beside their media files.
SABnzbd's new persistent config sets both incomplete and complete download paths
under /data; no download content defaults to the container rootfs.
Jellyfin retains its 4 cores, 4096 MB RAM, 16 GB config volume and hardware selector.
Other media servers retain their image's hardware options. Accounts/library setup,
Seerr and Bazarr connections, Lidarr profiles/root folder/download client, SABnzbd
Usenet credentials/client connections and subtitle providers still require user
configuration. These integrations are not claimed as automatic. Gluetun/VPN is
explicitly deferred, not installed or advertised as protecting traffic.
Seerr preserves the upstream non-root image user. Its documented security settings
map to lxc.no_new_privs: 1 and lxc.cap.keep: none after clearing inherited drop
entries. All-capability removal is incompatible with requested additional capabilities.
With qBittorrent selected, the user chooses the password for admin. The installer
copies the image's own default configuration into its new /config volume and
sets the upstream PBKDF2-SHA512 password hash; existing config is never overwritten.
No image files are patched. The API configures /data/downloads/, its incomplete
subdirectory, and selected tv/movies categories. Sonarr/Radarr download clients
are tested before saving. All apps use the same paths, with no remote path mappings.
Login accepts the legacy HTTP 200/Ok./SID response or the qBittorrent 5.2
HTTP 204/empty-body/QBT_SID_<port> response. A protected preferences request must
also succeed before any settings are changed. The password is masked in deployment
previews and returned in the final terminal credentials; protect terminal output.
It is stored hashed in qBittorrent and by the Arr apps in their private databases.
Suite Arr has no primary container, dependency hook or lifecycle contract. The installer starts each selected application once to perform initial setup; that one-time sequence does not couple future starts or stops. The user's onboot choice is applied to each LXC individually. True dependent stacks (Immich, Nextcloud and generic multi-image stacks) retain their existing hook lifecycle. Failure preserves created containers/data. The suite remains unvalidated by a complete real installation; tests cover compilation, independent startup and APIs.
Common dependency profiles
The generic stack compiler also supports official mariadb, mongo and
getmeili/meilisearch dependencies. It uses MariaDB's bundled
healthcheck.sh --connect --innodb_initialized, MongoDB's mongosh ping, and
Meilisearch's /health endpoint. Missing implicit data volumes are materialized
as backed-up Proxmox volumes at their official paths; dependencies stay on the
private network with no LAN interface. The Proxmox host can still reach them.
Per-stack stack_environment_overrides and stack_generators in catalog overlays
correct imported metadata without editing upstream images. Monica gets a 32-byte
base64 application key and a boolean random-root-password switch. Linkwarden gets
boolean credential login and a storage path matching its persisted volume.
Petio's setup should use MongoDB host mongo, port 27017; Plex credentials remain
user-provided. No third-party API credentials are fabricated.
These profiles enable monica-official, petio and linkwarden as installable,
not runtime-validated. Latest image tags remain selected; compatibility between
current upstream releases, initial application setup and restart/restore need
real laboratory testing. In particular, current MongoDB images require compatible
CPU instructions. Profiles do not grant LAN access to database services, enable
unattended upgrades or promise that a new database major version can reuse an old
data directory without migration.
RomM and optional external credentials
RomM now uses the common MariaDB profile and binds DB_PASSWD to the same
installation-generated value as MARIADB_PASSWORD. Root credentials remain
independent. The overlay declares optional IGDB, ScreenScraper and SteamGridDB
credential groups via stack_optional_environment. They are requested only if
selected; skipped providers have no fabricated credentials. Entered secrets are
preserved literally (including dollar signs) and hidden in deployment summaries.
All five RomM data/configuration volumes default to backed-up private storage;
users may explicitly select shared host directories.
RomM remains installable without runtime validation. The hook starts dependencies before the main application but does not implement Compose's propagation of an explicit dependency restart to RomM. Upstream latest versions, first-run setup and restart/restore behavior still need real installation testing.
Teable and authenticated Redis
Teable uses three OCI LXC services: the app, PostgreSQL and Redis. The overlay
replaces the malformed imported Redis argument with a reviewed three-argument
redis-server --requirepass command, preserving the image entrypoint. The Redis
server password, REDISCLI_AUTH and Teable's Redis URI share one generated value.
Only this explicit authenticated Redis command profile is accepted; arbitrary
custom dependency commands remain blocked.
Redis healthchecks now require an exact PONG response. The hook does not embed
the password: the authenticated check obtains REDISCLI_AUTH inside the LXC.
Credentials remain readable to administrators in native runtime configuration.
A Compose internal network's name is treated as a label, not a fixed Proxmox
bridge name; external networks and custom IPAM remain unsupported by this driver.
Teable is installable but not yet runtime-validated; tests include a fake Redis CLI
that returns authentication errors with exit code zero, plus successful PONG.
LinuxServer multi-image definitions and Kimai
The README converter now retains the full Compose stack instead of only the main image and dependency names. Translation blockers remain until the generic driver supports the full stack. Kimai reuses LinuxServer's own MariaDB image and its /config persistence. Its generated database password matches DATABASE_URL; readiness executes an authenticated SELECT 1 rather than merely checking a port. The app uses Doctrine's automatic server-version detection and allows IPv4 host names for the initial LAN deployment. Configure a specific domain when adding a reverse proxy. Completion prints upstream instructions for creating the first administrator inside the Kimai LXC; no default account is invented.
Kimai is installable but runtime-unvalidated. Diskover remains blocked: its upstream example includes an Elasticsearch dependency and a privileged helper changing host vm.max_map_count. Host kernel settings and Elasticsearch version compatibility need separate review, not silent removal of these requirements.
HAOS One native OCI profile
The community image ghcr.io/sevenrats/haos:latest, derived from HAOS One, is
installable with the laboratory adaptation: unprivileged LXC, ostype=unmanaged,
nesting=1, keyctl=1, and a managed /mnt/data volume included in Proxmox
backups (32 GB default, 16 GB minimum). The image's entrypoint, command and stop
signal remain imported from OCI metadata. No Docker daemon or compatibility proxy
is installed by ProxMenux; the nested runtime belongs to the image itself. The
profile is offered unprivileged only.
The installer asks for explicit acknowledgement of the experimental profile and
its inner AppArmor limitations. First boot may pull several images. A bounded
20-minute check reports progress and requires healthy/supported Supervisor, the
real Core container rather than the landing page, running CLI/DNS/audio/multicast/
Observer containers, and working Core and Observer HTTP endpoints. The recipe sets
SETUP_PORT=8123, so Core answers on port 8123 during onboarding and afterwards;
the check probes 8123 first and then 80. A first-boot failure preserves the LXC,
data and root-only console log for diagnosis; it never reports success. If
starting is declined, no unverified URL is printed.
The upstream Compose file asks Docker to restart the container, so the watchdog
is proposed by default: a shutdown asked from Home Assistant itself is followed
by a new start, as it is under Docker. A reboot asked from Home Assistant is
handled by Proxmox, and the DHCP address of the container is given back by
remote/oci_dhcp_lease.py.
Update and Modify work as for any other application: the installer runs the same check on the rebuilt container, and a recovery restores the backup and waits for that check too. Rebuilding with the data kept and recovering from the backup are tested; the update to a newer image waits for the first one the project publishes. Home Assistant Core, Supervisor and add-ons update themselves from Home Assistant. See the upstream project: https://github.com/sevenrats/docker-haos
Native Compose resource limits
mem_limit now supplies the editable Proxmox RAM default (MiB rounded upwards,
minimum 16 MiB). A conflicting deploy.resources.limits.memory remains blocked
for review. Swap is still an independent Proxmox setting, not a claim of exact
Docker swap defaults. ulimits maps soft/hard values to native lxc.prlimit.*;
-1 becomes unlimited. Invalid resource names, malformed values, duplicate
remote entries and soft limits greater than hard limits are rejected. The
installation summary shows these limits, including per-service stack limits.
No host sysctls, service-manager limits or privileges are silently changed to
make a requested limit succeed. LXC/kernel restrictions still apply. In
particular nproc is per real UID, not per container, and is not a replacement
for a cgroup PID limit. Reference: https://linuxcontainers.org/lxc/manpages/man5/lxc.container.conf.5.html
Diskover and RagFlow no longer carry the generic mem_limit/ulimits translation
blockers, but remain unavailable for automatic installation until their other
dependencies, healthchecks and host requirements are adapted. Diskover retains
Elasticsearch 7.17.22 in original_compose; the normalized candidate follows the
catalog's latest policy. Compatibility and tag availability therefore require
review before promotion, not an unverified Elasticsearch upgrade. The full Compose is now preserved in the active JSON,
as it already is when regenerating from the LinuxServer README.
Host monitors: experimental native profile
Glances offers an isolated alternative when host access is declined: an
unprivileged LXC with its own IP, default AppArmor, no host mounts, and no extra
capabilities. It monitors only itself, not Proxmox. The final summary states this
scope explicitly. GLANCES_OPT=-w remains automatic in either mode. Netdata's
host profile still requires acceptance; this fallback applies only to Glances.
Glances and Netdata have an installable host_monitor profile, tested on amd64
Proxmox 9.2.18 on 2026-09-14. Both expose host CPU/RAM/process metrics and survive
a shutdown/start cycle. Netdata also exposes LXC cgroup charts and preserves its
registry identity in a managed volume. Observed image digests and versions are
recorded in each template; rolling tags and arm64 are not universally validated.
The profile uses a privileged LXC, explicitly inherits host PID and network namespaces, and uses unconfined AppArmor. It requires informed consent; a compromised monitor could affect the host. Its endpoint uses the host IP and host firewall, with a port-conflict check before image download. Do not expose these unauthenticated dashboards to untrusted networks.
Only the monitor's LXCFS mount hook is cleared, to avoid reporting container
CPU/RAM limits as host metrics. Proxmox pre-start/autodev/post-stop hooks are
retained. No Docker socket or host root filesystem is mounted. Netdata's native
/host paths receive read-only proc/sys/identity mounts, and its three private
data directories remain managed volumes included in backup. Full filesystem,
SMART, Docker inventory and GPU monitoring are not implied by this profile.
The host cgroup mount is explicitly bound read-only below /host/sys/fs/cgroup.
CPU consumption is bounded with cpulimit rather than a restricted CPU affinity,
so the monitor sees the host's real processor count.
Proxmox does not accept lxc.namespace.share.* directly in CT configuration.
The installer uses supported lxc.include referencing the static companion
/etc/pve/proxmenux/host-monitor. /etc/pve/proxmenux is the same on every node
of a cluster, unlike /etc/pve/lxc, which is the folder of the local node, so a
migrated container finds it. It is NOT included in a CT vzdump: copy it when
restoring outside the cluster, in addition to restoring managed volumes.
Installations made before this keep /etc/pve/lxc/proxmenux-host-monitor. Full restore/image replacement
have not been tested. No custom supervisor or image entrypoint is introduced.
DeepSeek OCR remains deferred at the user's request: registry inspection on
2026-09-14 found only v2.2.0 for both IceWhaleTech images and no latest tag.
No fixed-version exception or deployment has been introduced.