docs/UNRAID_CA_PUBLISHING.md
Rama main · réplica de lectura del repositorio original.
# 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 `<TemplateURL>` points at its real raw path.
### 4a. `ca_profile.xml` (repo root)
Repository overview / author metadata. **The root element is `<CommunityApplications>`,
with `<Profile>` (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
`<Maintainer>` root, which the legacy CA still accepts, but new submissions should
follow the documented `<CommunityApplications>` root to pass the scan cleanly.)
```xml
<?xml version="1.0" encoding="utf-8"?>
<CommunityApplications>
<Profile>Describe the repo: what apps/plugins you maintain and where to get support. Markdown supported.</Profile>
<Icon>https://raw.githubusercontent.com/USER/REPO/main/path/to/icon.png</Icon>
<WebPage>https://github.com/USER/REPO</WebPage>
<Forum>https://forums.unraid.net/topic/YOUR_SUPPORT_TOPIC</Forum>
<!-- Optional: <Discord>, <DonateLink>, <DonateText>, <Photo>, <Video> -->
</CommunityApplications>
```
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
<?xml version="1.0"?>
<Container version="2">
<Name>PROJECT</Name>
<Repository>ghcr.io/USER/PROJECT:latest</Repository>
<Registry>https://github.com/USER/REPO/pkgs/container/PROJECT</Registry>
<Network>bridge</Network>
<MyIP/>
<Shell>sh</Shell>
<Privileged>false</Privileged>
<Support>https://github.com/USER/REPO/issues</Support>
<Project>https://github.com/USER/REPO</Project>
<Overview>Short summary shown in the Apps listing. Mention first-run setup.</Overview>
<Category>Productivity: Tools:Utilities</Category>
<WebUI>http://[IP]:[PORT:8080]/</WebUI>
<TemplateURL>https://raw.githubusercontent.com/USER/REPO/main/unraid/PROJECT.xml</TemplateURL>
<Icon>https://raw.githubusercontent.com/USER/REPO/main/unraid/PROJECT-icon.png</Icon>
<ExtraParams/>
<PostArgs/>
<Description>Longer description. BAKE IN the gotchas that cause support load (see Section 8). Use 

 for paragraph breaks.</Description>
<!-- One <Config> per port, path, and env var the user must set -->
<Config Name="WebUI Port" Target="8080" Default="8080" Mode="tcp" Description="..." Type="Port" Display="always" Required="true" Mask="false">8080</Config>
<Config Name="Config" Target="/app/config" Default="/mnt/user/appdata/PROJECT/config" Mode="rw" Description="..." Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/PROJECT/config</Config>
<Config Name="PUID" Target="PUID" Default="99" Mode="" Description="Unraid: 99 (nobody) so it can write appdata." Type="Variable" Display="always" Required="false" Mask="false">99</Config>
<Config Name="PGID" Target="PGID" Default="100" Mode="" Description="Unraid: 100 (users)." Type="Variable" Display="always" Required="false" Mask="false">100</Config>
<Config Name="Timezone" Target="TZ" Default="Europe/Madrid" Mode="" Description="..." Type="Variable" Display="always" Required="false" Mask="false">Europe/Madrid</Config>
</Container>
```
### 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`).** `<TemplateURL>` and
`<Icon>` 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 `<CommunityApplications>`** (per the official portal and
starter), with `<Profile>` as a non-empty child. The phrasing "non-empty Profile
section" misleads into thinking `<Profile>` is the root. (The legacy `<Maintainer>`
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 `<TemplateURL>` 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 `<TemplateURL>` is the identity.** Root,
`/unraid`, or `/templates` all scan fine. The one hard rule: `<TemplateURL>` must
resolve to the real raw path. An empty `<TemplateURL/>` 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 `<Description>` 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 `<Description>`. 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
`<Config>` 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 (<CommunityApplications> root, non-empty <Profile>)
[ ] Template XML present, well-formed, with Name + Repository
[ ] <TemplateURL> filled with the real raw /main/ path (not empty)
[ ] <Category> 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 `<TemplateURL>`). 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 `<Config Type="Variable">`
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 `<Config Type="Variable">` 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.