{"id":51902404,"url":"https://github.com/linuxserver/composer","last_synced_at":"2026-07-26T17:30:59.101Z","repository":{"id":312468073,"uuid":"1047512782","full_name":"linuxserver/composer","owner":"linuxserver","description":"Web based Node graph interface for building and sharing Docker Compose files","archived":false,"fork":false,"pushed_at":"2025-10-20T15:50:05.000Z","size":250,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-07-07T16:42:50.698Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/linuxserver.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"linuxserver","open_collective":"linuxserver"}},"created_at":"2025-08-30T15:33:15.000Z","updated_at":"2025-10-20T15:50:09.000Z","dependencies_parsed_at":"2025-08-30T21:24:03.458Z","dependency_job_id":"7b866326-7312-421e-89aa-9c0bcf351478","html_url":"https://github.com/linuxserver/composer","commit_stats":null,"previous_names":["linuxserver/composer"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/linuxserver/composer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/linuxserver%2Fcomposer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/linuxserver%2Fcomposer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/linuxserver%2Fcomposer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/linuxserver%2Fcomposer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/linuxserver","download_url":"https://codeload.github.com/linuxserver/composer/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/linuxserver%2Fcomposer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35922387,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-26T02:00:06.503Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-26T17:30:58.811Z","updated_at":"2026-07-26T17:30:59.082Z","avatar_url":"https://github.com/linuxserver.png","language":"JavaScript","funding_links":["https://github.com/sponsors/linuxserver","https://opencollective.com/linuxserver"],"categories":[],"sub_categories":[],"readme":"# LSIO Composer\n\n## Overview\n\nLSIO Composer is a visual, node-based builder for Docker Compose files. It allows for the creation, modification, and sharing of complex container configurations through a graphical interface.\n\nThe primary use case is for container experts to design sophisticated templates where end-users only need to modify a few high-level variables (e.g., PUID, PGID, base paths) to adapt the configuration to their specific host environment. The application generates a standard `docker-compose.yml` file that includes embedded metadata, allowing the file to be dragged back into the application to restore the visual workspace for further editing.\n\n## How It Works\n\nThe application state is managed in `src/App.jsx`, which holds the list of workspace items (nodes) and their connections.\n\n1.  **Node-Based Graph:** Users add nodes from the sidebar onto a workspace. These nodes represent Docker services (containers), top-level compose elements (networks, volumes), or configuration overrides (environment variables, user IDs, paths).\n2.  **Connections:** Users draw connections between node outputs and inputs. This defines the data flow. For example, a \"Parent Path\" node's output can connect to a \"Mount Path\" node's input, which in turn connects to a specific volume input on a container node.\n3.  **State Propagation:** When a node's data is changed or a connection is made, the application logic in `App.jsx` propagates these changes through the graph. The `propagateItemUpdate` function traverses downstream connections, updating the data of connected nodes accordingly.\n4.  **Compose Generation:** The `generateComposeFile` function iterates through all items on the workspace. It identifies items representing services, top-level networks, volumes, etc., and constructs a JavaScript object that mirrors the structure of a `docker-compose.yml` file. This object is then serialized into YAML format using the `js-yaml` library.\n5.  **Metadata Embedding:** Before outputting the final file, the current workspace state (items, connections, transform) is serialized to a JSON string, encoded in Base64, and appended to the YAML file as a specially formatted comment (`# LSIO_COMPOSER_DATA::...`).\n6.  **Re-importing:** When a file is dropped onto the application, it reads the file content, looks for the metadata comment, decodes the Base64 string, and parses the JSON to restore the entire workspace state.\n\n---\n\n## Developer Guide\n\nThis section details the dynamic architecture of the application, enabling developers to extend its functionality by creating custom nodes or integrating with custom container registries.\n\n### Dynamic Item Loading\n\nThe application discovers and loads all available node types at runtime. This is achieved through two mechanisms:\n\n1.  **Local Item Modules:** `import.meta.glob('./components/items/*.jsx', { eager: true })` in `App.jsx` loads every local item module synchronously on application start. These are used for generic overrides and top-level compose elements.\n2.  **Remote API Fetching:** Container definitions are fetched from a remote API endpoint. The default endpoint for LinuxServer.io containers is `https://api.linuxserver.io/api/v1/images?include_config=true\u0026include_deprecated=false`. The application parses the JSON response from this endpoint to dynamically generate container nodes.\n\nFor a local module to be recognized as a valid item, it must export two specific named constants:\n1.  `itemDefinition`: A static JavaScript object that defines the node's properties, including its name, default size, and its static `inputs` and `outputs` (connectors).\n2.  `ItemComponent`: A React component that renders the UI within the node's body. This component receives `itemData` and `onItemDataChange` as props to read and update the node's internal state.\n\n### API Data Format\n\nTo integrate a custom container registry, the API endpoint must return a JSON object with a structure compatible with the application's parser. The core data is expected under `data.repositories.\u003crepository_name\u003e`. The full API spec can be seen [here](https://api.linuxserver.io/).\n\nThe following is a breakdown of the expected format for each container object in the repository array:\n\n```json\n{\n  \"name\": \"adguardhome-sync\",\n  \"project_logo\": \"https://.../adguardhomesync-icon.png\",\n  \"config\": {\n    \"env_vars\": [\n      { \"name\": \"PUID\", \"value\": \"1000\" },\n      { \"name\": \"PGID\", \"value\": \"1000\" }\n    ],\n    \"volumes\": [\n      { \"path\": \"/config\", \"host_path\": \"/path/to/config\" }\n    ],\n    \"ports\": [\n      { \"external\": \"8080\", \"internal\": \"8080\" }\n    ],\n    \"devices\": [\n      { \"path\": \"/dev/snd\", \"host_path\": \"/dev/snd\" }\n    ],\n    \"security_opt\": [\n      { \"compose_var\": \"seccomp:unconfined\" }\n    ],\n    \"custom\": [\n      { \"name_compose\": \"shm_size\", \"value\": \"1gb\" }\n    ],\n    \"networking\": \"host\",\n    \"privileged\": true\n  }\n}\n```\n\n**Key Fields:**\n\n*   **`name`**: (string) The unique identifier for the container. Used as the node type and default `container_name`.\n*   **`project_logo`**: (string) URL to the icon displayed in the container node.\n*   **`config`**: (object) Contains the default Docker Compose configuration.\n    *   **`env_vars`**: (array of objects) Each object with a `name` and `value` key will be a default environment variable.\n    *   **`volumes`**: (array of objects) Each object's `path` key defines a container-side path for a volume mount.\n    *   **`ports`**: (array of objects) Each object with `external` and `internal` keys defines a default port mapping.\n    *   **`devices`**: (array of objects) Each object with `host_path` and `path` defines a device mapping.\n    *   **`security_opt`**: (array of objects) Each object's `compose_var` is added to the `security_opt` list.\n    *   **`custom`**: (array of objects) For arbitrary top-level service keys. The `name_compose` field is the YAML key, and `value` is its value (e.g., `shm_size: 1gb`).\n    *   **`networking`**: (string) Sets the default `network_mode`.\n    *   **`privileged`**: (boolean) Sets the `privileged` flag.\n\n### Dynamic Connectors\n\nCertain nodes, such as containers and the `EnvVarOverride` node, feature connectors that are generated dynamically based on the node's internal state. This logic resides in the `getDynamicDefinition` helper function in `src/components/WorkspaceItem.jsx`.\n\n-   **EnvVarOverride Example:** The `EnvVarOverride` node's output connector is defined by the \"Key\" field in its UI. If a user enters `PUID` as the key, the `getDynamicDefinition` function generates an output connector with `id: 'env_out:PUID'` and `name: 'PUID'`. This allows for a direct, context-aware connection to a container's environment variable input.\n\n-   **Container Example:** Container nodes generate dynamic inputs for every environment variable and volume mount defined in their configuration forms. For an environment variable with the key `TZ`, an input with `id: 'env:TZ'` is created. For a volume mapping like `changeme:/config`, an input with `id: 'volume:/config'` is created. This allows other nodes to target and override specific entries in these lists.\n\n### Creating a Custom Override Module\n\nTo add a new draggable node (e.g., a new type of override), follow these steps:\n\n1.  **Create the File:** Add a new `.jsx` file in `src/components/items/`, for example, `MyOverride.jsx`.\n\n2.  **Export `itemDefinition`:** Define the static properties of your node. The `outputs` array is critical. The `type` property of an output determines which inputs it can connect to.\n\n    ```javascript\n    // src/components/items/MyOverride.jsx\n    export const itemDefinition = {\n      name: 'My Override',\n      defaultSize: { width: 14, height: 8 },\n      inputs: [], // This is a provider, so no inputs.\n      outputs: [\n        {\n          id: 'my_override_out',\n          name: 'Value',\n          type: 'env_value', // Connects to container props like 'user', 'shm_size', or env vars.\n          multiple: true,\n          color: '#your_hex_color',\n        },\n      ],\n    };\n    ```\n\n3.  **Export `ItemComponent`:** Create the React component for the node's UI. It must call the `onItemDataChange` prop to update its state in the main application.\n\n    ```javascript\n    // src/components/items/MyOverride.jsx\n    import React from 'react';\n\n    export const ItemComponent = ({ itemData, onItemDataChange }) =\u003e {\n      const value = itemData.data?.value ?? 'default-value';\n      const handleChange = (e) =\u003e {\n        onItemDataChange(itemData.id, { value: e.target.value });\n      };\n      return (\n        \u003cdiv\u003e\n          \u003clabel\u003eMy Value\u003c/label\u003e\n          \u003cinput type=\"text\" value={value} onChange={handleChange} /\u003e\n        \u003c/div\u003e\n      );\n    };\n    ```\n\n4.  **Update Propagation Logic:** The `getProviderOutputValue` function in `App.jsx` must be updated to extract the primary output value from your new node type. Add a case for your new node's `name`.\n\n    ```javascript\n    // src/App.jsx -\u003e getProviderOutputValue\n    switch (itemDef.name) {\n      // ... existing cases\n      case 'My Override':\n        return item.data.value ?? '';\n    }\n    ```\n\n### Adding Custom Connectors to Containers\n\nTo add a new static input to all container nodes:\n\n1.  **Modify Container Definition:** Open `src/components/items/Container.jsx`. Locate the `createContainerDefinition` function.\n2.  **Add to `inputs` array:** Add a new object to the `inputs` array.\n    -   `id`: Must be unique. Use the prefix `prop:` for simple property overrides (e.g., `prop:hostname`).\n    -   `name`: The label displayed in the UI.\n    -   `compatibleTypes`: An array of `type` strings from output connectors that can connect here.\n    -   `multiple`: `false` for single-value properties.\n\n    ```javascript\n    // src/components/items/Container.jsx -\u003e createContainerDefinition -\u003e inputs array\n    inputs: [\n        // ... existing inputs\n        {\n          id: 'prop:hostname',\n          name: 'Hostname',\n          compatibleTypes: ['env_value'],\n          multiple: false,\n          color: '#your_hex_color'\n        },\n    ],\n    ```\n\n3.  **Update Propagation Logic:** In `App.jsx`, the `propagateItemUpdate` function must handle this new input. Add a case that checks for the connector `id`.\n\n    ```javascript\n    // src/App.jsx -\u003e propagateItemUpdate\n    } else if (toConnectorId.startsWith('prop:')) {\n        const propName = toConnectorId.substring('prop:'.length);\n        if (newToData[propName] !== incomingValue) {\n            newToData[propName] = incomingValue;\n            dataChanged = true;\n        }\n    }\n    ```\n\n4.  **Update Container UI:** In the `ItemComponent` within `Container.jsx`, find the corresponding form field (e.g., the hostname input) and make it `readOnly` when it is linked.\n\n    ```javascript\n    // src/components/items/Container.jsx -\u003e ItemComponent\n    const isPropLinked = (propName) =\u003e !!linkedInputs[`prop:${propName}`];\n    // ...\n    \u003cinput\n      type=\"text\"\n      value={serviceData.hostname || ''}\n      readOnly={isPropLinked('hostname')}\n    /\u003e\n    ```\n\n## Project Structure\n\n```\n.\n└── src\n    ├── App.jsx            # Main application component, state management, and core logic.\n    ├── components\n    │   ├── items/         # Directory for all dynamic node modules.\n    │   │   ├── Container.jsx # The component and definition factory for all containers.\n    │   │   └── ...        # Each file is a unique node type.\n    │   ├── Sidebar.jsx    # Renders the list of draggable items.\n    │   ├── Workspace.jsx  # The interactive canvas for nodes and connections.\n    │   └── WorkspaceItem.jsx # Renders a single node and its dynamic connectors.\n    └── styles             # CSS modules for components.\n```\n\n## Getting Started\n\n1.  Clone the repository.\n2.  Install dependencies:\n    ```sh\n    npm install\n    ```\n3.  Run the development server:\n    ```sh\n    npm run dev\n    ```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flinuxserver%2Fcomposer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flinuxserver%2Fcomposer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flinuxserver%2Fcomposer/lists"}