Ecology
DeployAlly organizes services into an ecosystem of six archetypes — a closed enum describing the nature of each container. The archetype determines defaults for network, healthcheck, scope, and exposure. On top of archetypes, two mechanisms describe relationships between services: needs (consuming an asset) and provides (offering functions to consumers).
The Six Archetypes
Every template declares one — and only one — of these values in identity.archetype.
identity:
archetype: application # | asset | static | worker | multi_component_saas | network_appliance1. `application`
Single app with an HTTP route. The default case for CMSs, dashboards, web tools, and backends.
| Aspect | Default |
|---|---|
| Network | public (reachable via Traefik) |
| Healthcheck | external_http |
| Scope | shared (multiple instances possible on the same host) |
| Routes | one HTTP route on the first declared internal port |
Example: Memos, WordPress, n8n, Vaultwarden.
identity:
slug: memos
archetype: application
routes:
- { id: web, kind: http, port: 5230, host: ${input.WEB_HOSTNAME} }2. `asset`
Shared infrastructure service — databases, caches, queues. Singleton per host: a single MariaDB serves N instances of WordPress, n8n, Mautic. Assets have dual networking: a dedicated network (mariadb) where consumers attach, and the public network so admin tools (Adminer, dbgate) can reach them too.
| Aspect | Default |
|---|---|
| Network | [<slug>, public] (dedicated + public) |
| Healthcheck | external_tcp on the declared port |
| Scope | singleton |
| Routes | empty (no HTTP ingress) |
Assets expose functions via provides.functions so consumer apps can provision isolated resources at deploy time.
Example: MariaDB, MySQL, PostgreSQL, Redis.
identity:
slug: mariadb
archetype: asset
scope: singleton
routes: []
provides:
exports:
- { from: container.name, to: host }
- { from: secrets.MARIADB_ROOT_PASSWORD, to: admin_credentials }
functions:
create_user_and_db:
params:
db_name: { type: identifier, required: true }
user: { type: identifier, required: true }
password: { type: secret, required: true }
command:
kind: docker_exec
target: ${self.container.name}
argv: [mariadb, -uroot, -p${self.admin_credentials}, -e,
"CREATE DATABASE IF NOT EXISTS `${db_name}`; …"]3. `static`
Static site served by nginx or caddy. Stateless, no needs, no secrets. The simplest archetype.
| Aspect | Default |
|---|---|
| Network | public |
| Healthcheck | external_http on / |
| Scope | shared |
| Routes | one HTTP route on / |
Example: landing pages, static docs, Jekyll/Hugo sites.
4. `worker`
Headless process — no HTTP port, no route. Runs in the background processing queues, jobs, or cron tasks. Since there's no port to probe, the healthcheck observes process state.
| Aspect | Default |
|---|---|
| Network | public (to reach external APIs) |
| Healthcheck | stable_state — 30s settle, container must remain running |
| Scope | shared |
| Routes | empty |
Workers require lifecycle.post_deploy.assert_stable_state by default — deploy only succeeds if the process does not crash within 30s.
Example: Postal workers, n8n queue processors, cron containers.
5. `multi_component_saas`
App with multiple containers composing a single service — typically a web + one or more workers + auxiliary dependencies. Each component becomes a separate container, but the set is deployed/decommissioned as a unit.
| Aspect | Default |
|---|---|
| Network | public (+ extra networks per needs) |
| Healthcheck | per component |
| Scope | shared |
| Routes | reference the component via component: <id> |
identity:
archetype: multi_component_saas
components:
- id: web
primary: true
image: postal/postal:3.3.4
command: [postal, web-server]
- id: worker
image: postal/postal:3.3.4
command: [postal, worker]
routes:
- id: web
kind: http
component: web
port: 5000
host: ${input.WEB_HOSTNAME}Example: Postal (web + worker + SMTP server), Chatwoot (web + sidekiq).
6. `network_appliance`
Network proxy or ingress. Runs in network_mode: host (needs to see all ports and netfilter), is singleton, and usually has no exposed HTTP route — it is the route.
| Aspect | Default |
|---|---|
| Network | host (no network isolation) |
| Healthcheck | exit (CLI tools) or external_tcp |
| Scope | singleton |
| Routes | raw TCP/UDP when applicable |
Example: Traefik (ingress for the whole DeployAlly stack), SOCKS proxies, VPN gateways.
Relating Archetypes: `needs` and `provides`
Applications consume assets via needs. The block is declarative — it describes what the app needs, with no hardcoded credentials or hosts.
# wordpress.yaml
needs:
database:
class: relational
options: [mariadb, mysql]
per_instance:
database: "wp_${instance.uid}"
user: "wp_${instance.uid}"
password: ${secrets.WP_DB_PASSWORD}
on_provision: create_user_and_db
on_decommission: drop_user_and_db
exports:
- { from: container.name, to: WORDPRESS_DB_HOST }On WordPress deploy:
- The asset dispatcher looks for a running asset of
class: relationalon the host (labelccs.systems/asset_class=relational). - Once found, it fires the function declared in
on_provision(here, MariaDB'screate_user_and_db), passingdb_name=wp_<uid>,user=wp_<uid>,password=<secret>. - The asset executes the function via
docker exec(creates DB + user + grants). - Asset exports (
container.name → WORDPRESS_DB_HOST) become env vars on WordPress. - WordPress comes up and connects to the isolated
wp_<uid>database.
On decommission, the reverse cycle: drop_user_and_db is called, removing the DB and user. The shared MariaDB stays up.
Cascade auto-provision
If the asset doesn't exist on the host, the --provision-missing-assets flag instructs the cascade to create it automatically — first option in needs.options, variant marked default: true, same profile as the consumer. There's retry with backoff (3→15s, 5 attempts) waiting for the asset's cold-start.
deployally deploy --species wordpress --instance-uid wp-blog-001 \
--input WEB_HOSTNAME=blog.example.com \
--provision-missing-assets \
--applyNetworks
DeployAlly creates Docker networks automatically based on the archetype:
| Archetype | Networks |
|---|---|
application |
public + dedicated networks of assets in needs |
asset |
<slug> (dedicated) + public |
static |
public |
worker |
public + dedicated networks of assets in needs |
multi_component_saas |
public + dedicated networks |
network_appliance |
host |
Internal service discovery: containers find each other by container name. A WordPress connects to mariadb-shared:3306 via internal DNS on the mariadb network.
Complete Example
Typical stack: Traefik + MariaDB + Redis + WordPress + Adminer.
┌─────────────────────────────────┐
│ Traefik (network_appliance) │ host network, singleton
│ ingress + TLS │
└────────────────┬────────────────┘
│ public
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│WordPress │ │ Adminer │ │ … apps │
│ (app) │ │ (app) │ │ │
└────┬─────┘ └────┬─────┘ └──────────┘
│ │
│ mariadb │ mariadb
▼ ▼
┌─────────────────┐ ┌────────────┐
│ MariaDB (asset) │ │ Redis │
│ singleton │ │ (asset) │
└─────────────────┘ └────────────┘Deploy order implied by the archetype:
network_appliance(Traefik) — singleton, must be up first.asset(MariaDB, Redis) — singletons that provide functions.application(WordPress, Adminer) — consume assets vianeeds.
Next Steps
- Templates — How each block (image, routes, storage, etc.) varies by archetype.
- Definitions and Instances — How
per_instanceprovisions isolated resources. - Taxonomy — How kingdom/family/species organize the catalog.