# Archipelago App Developer Guide Build and package containerized apps for Archipelago. ## Overview Apps run as rootless Podman containers on user nodes. You describe an app in `apps//manifest.yml`; the backend validates that manifest, compiles it into rootless container/runtime behavior, and the release pipeline generates catalog surfaces from the same manifest-owned metadata. Archipelago's app contract is deliberately manifest-first. A developer should be able to describe images or local builds, ports, volumes, generated files, dependencies, health/readiness, data ownership, networking, secrets, and supported bridge integrations in the app manifest without asking for a custom OS image or app-specific backend patch. When a real app needs a capability that is not represented yet, the preferred path is to add a reusable manifest/orchestrator primitive that other apps can use too. The historical marketplace-publish design is not the active local developer contract for `1.8-alpha`. For this release, local manifests are the source of truth and catalog JSON is generated from them. ## App Manifest Every app needs a manifest at `apps//manifest.yml`. The root key is `app`; runtime, catalog, and integration fields live below that key. ### Template Manifest ```yaml # apps/my-app/manifest.yml app: id: my-app # Unique, lowercase kebab-case name: My App version: 1.0.0 # Semantic versioning description: My App does one thing well. container: image: docker.io/myorg/my-app:1.0.0 pull_policy: if-not-present network: archy-net entrypoint: ["sh", "-lc"] custom_args: - /app/start.sh derived_env: - key: PUBLIC_URL template: https://{{HOST_MDNS}}:8180 secret_env: - key: APP_PASSWORD secret_file: my-app-password dependencies: - storage: 1Gi resources: cpu_limit: 2 memory_limit: 512Mi security: capabilities: [] readonly_root: true no_new_privileges: true network_policy: isolated ports: - host: 8180 container: 8080 protocol: tcp volumes: - type: bind source: /var/lib/archipelago/my-app target: /data options: [rw] environment: - APP_MODE=production health_check: type: http endpoint: http://localhost:8080 path: /health interval: 30s timeout: 5s retries: 3 files: - path: /var/lib/archipelago/my-app/config.yml content: | bind: 0.0.0.0:8080 overwrite: false metadata: icon: /assets/img/app-icons/my-app.svg category: tools tier: optional repo: https://github.com/myorg/my-app launch: open_in_new_tab: false ``` ### Required Fields | Field | Description | |-------|-------------| | `app.id` | Unique identifier, lowercase, kebab-case only | | `app.name` | Human-readable name | | `app.version` | Version string containing at least one digit; semantic versions are preferred | | `container.image` or `container.build` | Exactly one image source must be present | | `security.readonly_root` | Should remain `true` for normal apps | | `security.no_new_privileges` | Should remain `true` for normal apps | ### Current Manifest Fields | Field | Purpose | |-------|---------| | `app.id`, `app.name`, `app.version`, `app.description` | App identity and release metadata | | `app.container.image` | Registry image to pull | | `app.container.build` | Local build definition with `context`, `dockerfile`, `tag`, and optional `build_args` | | `app.container.pull_policy` | Pull behavior, usually `if-not-present` | | `app.container.network` | Podman network setting such as `archy-net` or `pasta`; dangerous namespace-sharing modes are rejected | | `app.container.entrypoint` / `custom_args` | Entrypoint and command override | | `app.container.derived_env` | Environment values rendered from allowed host facts such as `HOST_IP`, `HOST_MDNS`, and `DISK_GB` | | `app.container.secret_env` | Environment values read from `/var/lib/archipelago/secrets/` | | `app.container.data_uid` | UID:GID ownership repair for app data directories | | `app.dependencies` | Storage requirements and app dependencies | | `app.resources` | CPU, memory, and disk limits | | `app.security` | Capabilities, read-only root, no-new-privileges, network policy, optional AppArmor profile | | `app.ports` | Host-to-container port mappings | | `app.volumes` | `bind`, `volume`, or `tmpfs` mounts | | `app.files` | Generated files under declared bind-mounted host paths | | `app.environment` | Static `KEY=value` environment entries | | `app.health_check` | HTTP or TCP health check settings | | `app.devices` | Explicit device paths | | `app.metadata` | Catalog-facing presentation metadata such as icon, category, tier, repo/source, author, feature bullets, and launch hints | | `app.interfaces.main` | Optional primary UI launch surface with `port`, `protocol`, and `path` | Additional extension keys may exist for current integrations, for example Bitcoin, Lightning, or app-specific launch/interface metadata. Treat extension keys as transitional unless they are documented as reusable platform primitives. Use `metadata.launch.open_in_new_tab: true` when the app UI is known to reject iframe embedding with headers such as `X-Frame-Options` or restrictive CSP. The frontend app-session metadata is generated from this flag during release work. ### Launch Interfaces If an app exposes a user-facing web UI, declare its primary launch surface in `interfaces.main`. Runtime package listings prefer this interface over inferred port mappings, which matters for apps that expose non-UI service ports or use a companion wait/proxy UI. ```yaml interfaces: main: name: Web UI description: Primary app interface type: ui port: 8180 protocol: http path: / ``` For simple HTTP apps without `interfaces.main`, Archipelago can still infer the launch URL from the first declared TCP host port when the app has an HTTP health check. TCP-only service ports, such as Bitcoin RPC/P2P, are not treated as UI launch URLs. Interface keys must use lowercase ASCII letters, digits, hyphens, or underscores. Supported interface types are `ui`, `api`, and `metrics`; only `type: ui` is treated as a launchable app surface. Supported protocols are `http` and `https`, and `path` must start with `/`. ## Security Requirements These are enforced by the marketplace/catalog pipeline and the node. Non-compliant apps are flagged. ### Mandatory 1. **No `:latest` tag** — Pin a specific version: `myapp:1.0.0` 2. **Read-only root filesystem** — `security.readonly_root: true` (use volumes for writable data) 3. **No privilege escalation** — `security.no_new_privileges: true` 4. **Minimal capabilities** — Drop all caps, only add required ones 5. **No host network unless explicitly approved** — keep `security.network_policy` isolated or bridge ### Allowed Capabilities The parser currently accepts this allow-list. Keep capability requests minimal; some accepted capabilities still require release review before a public package should depend on them. | Capability | When Needed | |-----------|-------------| | `CHOWN` | App needs to change file ownership | | `DAC_OVERRIDE` | App needs to bypass file permissions | | `FOWNER` | App needs ownership-related file operations | | `NET_ADMIN` | Network administration; requires extra scrutiny | | `NET_BIND_SERVICE` | App binds to ports below 1024 | | `NET_RAW` | Raw network sockets; requires extra scrutiny | | `SETUID`, `SETGID` | App manages user switching | | `SYS_ADMIN` | Broad administrative capability; avoid for normal apps | ### Forbidden - Namespace-sharing network modes such as `container:` or `ns:` - Mounting system paths: `/`, `/etc`, `/var`, `/usr`, `/proc`, `/sys` - `SYS_PTRACE`, privileged containers, Docker socket mounts, or rootful execution - Hardcoded secrets in environment variables or images ## Container Best Practices ### Volumes ```yaml volumes: - type: bind source: /var/lib/archipelago/my-app target: /data options: [rw] ``` Data is stored at `/var/lib/archipelago/{app-id}/` on the host. Generated files must live under a declared bind-mounted host path: ```yaml files: - path: /var/lib/archipelago/my-app/config.yml content: | bind: 0.0.0.0:8080 overwrite: false ``` Use `overwrite: false` for first-run defaults that users or the app may later modify. Use `overwrite: true` only for generated files the platform must own. ### Health Checks Define a health check endpoint in your container: ```dockerfile HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 ``` ### Logging - Log to stdout/stderr (Podman captures container logs) - Never log secrets, passwords, or keys - Use structured logging (JSON) for machine parsing ### Networking Apps get their own network namespace. To connect to other Archipelago apps: ```yaml # If your app needs to talk to Bitcoin dependencies: - bitcoin-knots container: network: archy-net derived_env: - key: BITCOIN_RPC_HOST template: bitcoin-knots - key: BITCOIN_RPC_PORT template: "8332" ``` The `archy-net` Podman network provides DNS resolution between containers. Use `derived_env` for host facts like `HOST_MDNS` instead of hardcoding node-specific URLs. ## Catalog Generation Catalog JSON is generated from manifests during release work. Do not manually edit generated fields in `app-catalog/catalog.json` or `neode-ui/public/catalog.json` when the same value belongs in the manifest. Manifest-owned catalog fields currently include: - app title from `app.name`; - version from `app.version`; - description from `app.description`; - Docker image from `app.container.image`; - category from `app.category` or `app.metadata.category`; - tier from `app.metadata.tier`; - icon from `app.metadata.icon`; - repo URL from `app.metadata.repo`, `repoUrl`, or `source`. ### 1. Build and Push Your Image ```bash podman build -t docker.io/myorg/my-app:1.0.0 . podman push docker.io/myorg/my-app:1.0.0 ``` ### 2. Generate Catalogs ```bash python3 scripts/generate-app-catalog.py ``` ### 3. Verify Drift ```bash python3 scripts/check-app-catalog-drift.py --release --strict ``` Before release, the canonical catalog and UI public catalog should match: ```bash cmp -s app-catalog/catalog.json neode-ui/public/catalog.json ``` ## Testing Your App ### Local Testing ```bash # Run your container locally podman run -d --name my-app \ -p 8180:8080 \ --read-only \ --security-opt no-new-privileges \ --user 1000:1000 \ docker.io/myorg/my-app:1.0.0 # Verify it works curl http://localhost:8180/health # Check logs podman logs my-app ``` ### On an Archipelago Node 1. Install via the marketplace UI or RPC: ```bash curl -b cookies.txt -X POST http://192.168.1.228/rpc/v1 \ -d '{"method":"package.install","params":{"id":"my-app","dockerImage":"docker.io/myorg/my-app:1.0.0"}}' ``` 2. Verify the container is running: ```bash curl -b cookies.txt -X POST http://192.168.1.228/rpc/v1 \ -d '{"method":"container-list"}' ``` 3. Check the UI at `http://192.168.1.228/app/my-app/` ### Validate Manifest ```bash cargo test --manifest-path core/Cargo.toml -p archipelago-container python3 scripts/check-app-catalog-drift.py --release --strict ``` ## Updating Your App 1. Build and push the new version: `docker.io/myorg/my-app:1.1.0`. 2. Update `app.version` and `app.container.image` or `app.container.build.tag`. 3. Run catalog generation and drift checks. 4. Validate install/start/stop/restart/uninstall/reinstall behavior before shipping. The broader app update policy for `1.8-alpha` is still being finalized. Until that policy is locked, app manifests should be explicit and pinned so update detection compares concrete image/tag metadata rather than mutable tags. ## App Icon - Provide a URL to your app icon (PNG, WebP, or SVG) - Recommended size: 256x256 pixels - Square aspect ratio - If no icon URL, a generic placeholder is shown in the marketplace ## Release Validation Expectations Every supported app must satisfy the lifecycle contract: - install - launch - stop - start - restart - uninstall while preserving data - reinstall with preserved data - report truthful health/status - survive backend restart - survive host reboot For apps with special dependencies, launch must explain dependency wait states instead of showing a dead iframe. Examples include Bitcoin sync/IBD, Lightning wallet readiness, Nostr signer bridge injection, Tailscale login/auth, and app-specific setup screens. Runtime changes should be validated with focused tests first, then the release lifecycle harness on the validation host when host access is intentionally resumed.