12 KiB
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/<app-id>/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/<app-id>/manifest.yml. The root key is app; runtime, catalog, and integration fields live below that key.
Template Manifest
# 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/<secret_file> |
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 |
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.
Security Requirements
These are enforced by the marketplace/catalog pipeline and the node. Non-compliant apps are flagged.
Mandatory
- No
:latesttag — Pin a specific version:myapp:1.0.0 - Read-only root filesystem —
security.readonly_root: true(use volumes for writable data) - No privilege escalation —
security.no_new_privileges: true - Minimal capabilities — Drop all caps, only add required ones
- No host network unless explicitly approved — keep
security.network_policyisolated 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:<name>orns:<path> - 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
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:
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:
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:
# 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.categoryorapp.metadata.category; - tier from
app.metadata.tier; - icon from
app.metadata.icon; - repo URL from
app.metadata.repo,repoUrl, orsource.
1. Build and Push Your Image
podman build -t docker.io/myorg/my-app:1.0.0 .
podman push docker.io/myorg/my-app:1.0.0
2. Generate Catalogs
python3 scripts/generate-app-catalog.py
3. Verify Drift
python3 scripts/check-app-catalog-drift.py --release --strict
Before release, the canonical catalog and UI public catalog should match:
cmp -s app-catalog/catalog.json neode-ui/public/catalog.json
Testing Your App
Local Testing
# 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
- Install via the marketplace UI or RPC:
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"}}' - Verify the container is running:
curl -b cookies.txt -X POST http://192.168.1.228/rpc/v1 \ -d '{"method":"container-list"}' - Check the UI at
http://192.168.1.228/app/my-app/
Validate Manifest
cargo test --manifest-path core/Cargo.toml -p archipelago-container
python3 scripts/check-app-catalog-drift.py --release --strict
Updating Your App
- Build and push the new version:
docker.io/myorg/my-app:1.1.0. - Update
app.versionandapp.container.imageorapp.container.build.tag. - Run catalog generation and drift checks.
- 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.