# Publishing a Project to Unraid Community Applications (CA) A reusable playbook for getting a self-hosted Docker project listed in the Unraid **Community Applications** store (the "Apps" tab inside Unraid). Written so a future maintainer or AI agent can repeat the process for a new project quickly. `actual-sync` is used as the worked example throughout. Replace the repo name, image, ports, and paths with the new project's values. > **Style note:** keep replies and docs in this repo free of em dashes and en dashes. --- ## 1. The model in one paragraph Community Applications does not host your files. You keep a public GitHub repo that contains (a) a maintainer profile file and (b) one Docker template XML per app. You then register the repo through the official submission portal. CA's scanner reads your repo's **default branch** over raw GitHub URLs, validates it, checks for duplicates, and (after moderation) lists it. Users install from the Apps tab; Unraid pulls the image by tag, so a `:latest` tag means they always get your newest build with no template change. --- ## 2. Key websites | Purpose | URL | |---|---| | Submission portal (start here, sign in with Unraid/forum account) | https://ca.unraid.net/submit | | Submission help / docs | https://ca.unraid.net/submit/help | | Repository XML format guide | https://ca.unraid.net/submit/help/repository-xml | | XML field reference (required vs optional) | https://ca.unraid.net/submit/help/xml-field-reference | | Official CA documentation | https://docs.unraid.net/community-applications/ | | Browse catalog (check for duplicate names before submitting) | https://ca.unraid.net/apps | | Community how-to thread (older, still useful context) | https://forums.unraid.net/topic/101424-how-to-publish-docker-templates-to-community-applications-on-unraid/ | | Legacy "request" repo (being retired, do NOT use as the route) | https://github.com/selfhosters/unRAID-CA-templates | The portal pages are the source of truth. The forum thread and selfhosters repo are older mechanisms kept here for context only. --- ## 3. Prerequisites Before touching CA, the project needs: 1. A **public** GitHub repo, reasonably active. 2. An **OSI-approved `LICENSE`** file at the repo root (MIT, Apache 2.0, GPL, etc). 3. A published container image on a public registry, ideally tagged **`:latest`** (GHCR, Docker Hub). Pin the template to `:latest` so you never regenerate it per release (see Section 7). 4. A square **icon** (PNG, commonly 64x64 to 256x256) committed to the repo and reachable over a raw URL. --- ## 4. Files to create Two files. Put the profile at the repo root. The template can live anywhere the scanner can read (root, `/unraid`, or the documented `/templates` folder all work); what matters is that `` points at its real raw path. ### 4a. `ca_profile.xml` (repo root) Repository overview / author metadata. **The root element is ``, with `` (non-empty) as a child.** This is the root the official submission portal and the starter repo use. (Note: some long-established repos like binhex use a `` root, which the legacy CA still accepts, but new submissions should follow the documented `` root to pass the scan cleanly.) ```xml Describe the repo: what apps/plugins you maintain and where to get support. Markdown supported. https://raw.githubusercontent.com/USER/REPO/main/path/to/icon.png https://github.com/USER/REPO https://forums.unraid.net/topic/YOUR_SUPPORT_TOPIC ``` Source of truth: the starter repo's `ca_profile.xml` (https://github.com/unraid/unraid-community-apps-starter) and https://ca.unraid.net/submit/help/repository-info-xml. ### 4b. The container template, e.g. `unraid/PROJECT.xml` ```xml PROJECT ghcr.io/USER/PROJECT:latest https://github.com/USER/REPO/pkgs/container/PROJECT bridge sh false https://github.com/USER/REPO/issues https://github.com/USER/REPO Short summary shown in the Apps listing. Mention first-run setup. Productivity: Tools:Utilities http://[IP]:[PORT:8080]/ https://raw.githubusercontent.com/USER/REPO/main/unraid/PROJECT.xml https://raw.githubusercontent.com/USER/REPO/main/unraid/PROJECT-icon.png Longer description. BAKE IN the gotchas that cause support load (see Section 8). Use for paragraph breaks. 8080 /mnt/user/appdata/PROJECT/config 99 100 Europe/Madrid ``` ### 4c. (Optional) A CI check for well-formedness Mirror the official gate (xmllint well-formedness) so a broken edit fails CI: ```yaml name: Validate Unraid templates on: push: paths: ['unraid/**.xml', 'ca_profile.xml', '.github/workflows/unraid-xmllint.yml'] pull_request: paths: ['unraid/**.xml', 'ca_profile.xml', '.github/workflows/unraid-xmllint.yml'] workflow_dispatch: jobs: xmllint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: sudo apt-get update && sudo apt-get install -y libxml2-utils - run: xmllint --noout unraid/*.xml ca_profile.xml ``` Locally, prefer `xmllint --noout ca_profile.xml unraid/*.xml` (install `libxml2-utils`). If you must check in Python, use `defusedxml` rather than the stdlib parser, which is vulnerable to XXE and billion-laughs by default: `python3 -c "from defusedxml.ElementTree import parse; [parse(f) for f in ['ca_profile.xml','unraid/PROJECT.xml']]"` (These are your own trusted files, so the risk is low, but do not teach the unsafe pattern in a doc others will copy.) --- ## 5. Template fields: required vs recommended | Field | Status | Notes | |---|---|---| | `Name` | Required | Display name in Apps | | `Repository` | Required | Image reference; use `:latest` | | `Overview` | Recommended | Primary summary shown to users | | `Category` | Recommended | Space-separated tags, each colon-terminated, e.g. `Productivity: Tools:Utilities` | | `Icon` | Recommended | Raw URL to a square PNG | | `Project` | Recommended | Homepage / source repo | | `Support` | Recommended | Forum thread or GitHub issues | | `TemplateURL` | Recommended | Canonical raw URL of THIS file; used for identity and review. Do not leave it empty. | | `Registry`, `Network`, `WebUI`, `Description`, `Config`, `Privileged`, `Shell`, `ExtraParams` | Optional | Fill what applies | --- ## 6. Submission steps (done by a human at the portal) The scanner reads your default branch, so make sure Sections 4 files are merged to **main** first (Section 7). 1. Go to https://ca.unraid.net/submit and sign in. 2. Point it at the GitHub repo. 3. Run **Validate** and **Scan**. Fix anything flagged, push, re-scan. 4. Preview the listing. 5. Submit to join the catalog. It then goes through CA moderation/review. An AI agent cannot complete this step: it is an authenticated action on the maintainer's account. Prepare the repo, then hand the human the four steps above. --- ## 7. Branch and release behavior (important) - **The scanner reads the DEFAULT branch (usually `main`).** `` and `` use `/main/` raw URLs. If your work lands only on a `development` branch, the scanner will not see it. Merge to `main` before submitting. - **Nothing here is regenerated per release.** The template pins `:latest`, carries no version string, and the profile is maintainer-level metadata. New releases reach users through the image tag, not template edits. - **You only edit the template when the container's configuration surface changes:** a new env var, port, volume, renamed config path, or a description/category update. Those are occasional and unrelated to version bumps. --- ## 8. Major painpoints (read this before you start) These caused real time loss. Front-loading them saves the next attempt. 1. **`ca_profile.xml` root is ``** (per the official portal and starter), with `` as a non-empty child. The phrasing "non-empty Profile section" misleads into thinking `` is the root. (The legacy `` root that some old repos use also parses, but follow the documented root.) 2. **Default-branch requirement.** Files on `development` are invisible to the scanner. Get them onto `main`, and make `` point at the branch that actually holds the file. 3. **Use `:latest`, not a pinned tag**, unless you want to edit and re-submit the template every release. `:latest` plus Unraid's update check (or Watchtower) keeps users current hands-off. 4. **The selfhosters "request" repo is being retired.** Use `ca.unraid.net/submit`. Old guides that tell you to open a PR there are stale. 5. **Template location is flexible but `` is the identity.** Root, `/unraid`, or `/templates` all scan fine. The one hard rule: `` must resolve to the real raw path. An empty `` is a common miss. 6. **Category format.** Space-separated, each token ends in a colon, optional subcategory after it: `Productivity: Tools:Utilities`. Check existing categories at https://ca.unraid.net/apps. 7. **Icon must resolve publicly** over its raw URL and be roughly square. A 404 icon is an easy rejection. 8. **Bridge vs host networking is the number one end-user support issue.** Bake the guidance into `` so you field fewer tickets: - Keep the app's bind host at `0.0.0.0` (do not set it to the Unraid LAN IP, or the WebUI is unreachable in bridge mode). - On host networking, Docker container-name DNS stops working, so any internal URL the app needs (for example a sibling service) must use the LAN IP, or both containers must share a user-defined Docker network. 9. **PUID/PGID for appdata writability.** On Unraid use `99:100` (nobody:users) and chown the data dirs on startup, or the app cannot write its volumes. 10. **First-run config.** If the app needs a config file, ship a self-seeding first run (write an example and exit) and say so in ``. Saves a round trip. 11. **Moderation expectations.** As maintainer you are expected to respond in your support thread, keep the listing working across new Unraid versions, and clearly label beta or experimental builds. 12. **No repeatable config groups.** An Unraid template is a fixed flat list of `` elements (Variable / Path / Port / Label / Device) — there is **no array or repeatable-group construct**, so you cannot offer a clean "+ add another X" in the UI for a multi-field group (e.g. N servers each with url+password+id). A user can hand-add individual entries via Advanced → "Add another Path, Port, Variable or Label", but that is undiscoverable and per-field. See the pattern in Section 11. (Sources: selfhosters.net/docker/templating, wiki.unraid.net/DockerTemplateSchema.) --- ## 9. Checklist for a new project ``` [ ] Repo is public and active [ ] OSI LICENSE at repo root [ ] Image published with a :latest tag on a public registry [ ] Square icon committed and reachable via raw URL [ ] ca_profile.xml at repo root ( root, non-empty ) [ ] Template XML present, well-formed, with Name + Repository [ ] filled with the real raw /main/ path (not empty) [ ] valid (space-separated, colon-terminated) [ ] Description bakes in networking + first-run gotchas [ ] (optional) xmllint CI workflow added [ ] All of the above merged to the DEFAULT branch (main) [ ] Duplicate name check at ca.unraid.net/apps [ ] Human submits at ca.unraid.net/submit (Validate, Scan, Preview, Join) [ ] Only announce / tell testers to use the Apps tab AFTER it is published ``` --- ## 10. Worked example `actual-sync` shipped: - `ca_profile.xml` at repo root. - `unraid/actual-sync.xml` (template, `:latest`, filled ``). As of 1.8.0 it also carries the single-budget `ACTUAL_SYNC_SERVER_*` Variable fields (the "single instance via UI, many via a file" pattern from Section 11), with the two password fields `Mask="true"`. - `unraid/actual-sync-icon.png` (icon). - `.github/workflows/unraid-xmllint.yml` (well-formedness gate). See those files in this repo for a concrete, working reference. --- ## 11. Config UX: single instance via UI, many via a file Because the template has no repeatable groups (Section 8, point 12), an app that supports an arbitrary **number** of configured instances (N servers, N targets, …) cannot expose "add as many as you want" in the Unraid UI. The idiomatic pattern: - **One instance via env-var template fields.** Predefine `` entries for a single instance (e.g. URL / password / id). A user with one of the thing fills the fields and runs — no config file, no pre-created config folder. This is how config-light apps (e.g. the Actual *server* template) avoid first-run friction entirely; an app that genuinely needs config can offer it for the common single-instance case. - **Many via a mounted config file.** Keep the JSON/YAML config as the canonical multi-instance source, mounted at a Path the user provides. - **Merge the two sources, deduped by a STABLE IDENTITY, not the display name.** If the same instance is configured in both the env vars and the file, dedup on a real identity key and drop the duplicate with a warning — otherwise you double-process it (double bank-sync, duplicate notifications, state collisions). Deduping on the user-facing *name* alone is not enough; two entries can name the same underlying resource differently. **Shipped in actual-sync 1.8.0 (#119 + #120).** Concrete behavior, for reference: - **Env-var server (#120).** A single budget is configured entirely via `ACTUAL_SYNC_SERVER_*` env vars: `URL`, `PASSWORD`, `SYNC_ID` (required), plus optional `NAME`, `ENCRYPTION_PASSWORD`, `DATA_DIR`, `SCHEDULE`. With these set, an Unraid/Docker user needs no `config.json` and no pre-created config folder. The template exposes the matching `` fields, with `PASSWORD` and `ENCRYPTION_PASSWORD` set `Mask="true"`. - **Precedence.** The file stays the canonical multi-server source. The env-var server is merged into the file's list; if its `NAME` collides with a file server it is auto-renamed rather than overwriting. A budget is never synced twice. - **Budget identity for dedup (#119).** Two servers are the same BUDGET when their normalized `url` + `syncId` match (host compared case-insensitively, path case-sensitively). `validateLogic()` also warns when two servers share a `dataDir` (which would corrupt the same cache). This is implemented in `src/lib/configLoader.js`. - **Advisory for now.** Duplicate-budget and shared-dataDir detection currently *warns*; turning it into a hard startup failure is tracked by #121.