Files
ermnvldmr.com/services/docs/content/en/apex/guides/service-composition.md
T

4.1 KiB
Raw Blame History

title, description, weight
title description weight
Add a service composition Wire a new project into a node: anchor IP, networks, tiers, and lint. 23

Context: a project joins a node as one directory under compositions/ containing a hand-authored compose file. The framework's contract for that file is small: a static anchor IP on the direct network, external network references by key, and tier-relative data paths.

{{% steps %}}

Allocate the anchor address

Pick the next free address at .20 or above in the node's /24 and record it in compositions/ADDRESSING.md. Addresses .10–.19 are reserved for the core composition (see Concept: Fleet addressing scheme).

Author the compose file

name: <project>
services:
  <anchor>:
    image: ...
    container_name: <project>-<anchor>
    user: "${APEX_UID?error}:${APEX_GID?error}"
    group_add:
      - "${APEX_SHARED_GID?error}"
    volumes:
      - ./@tier1/<anchor>/config:/config
      - ./@tier2/<anchor>/data:/data
    networks:
      direct:
        ipv4_address: 198.18.16.20
      enclave: {}
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.<project>.rule=Host(`...`)"
      - "traefik.http.services.<project>.loadbalancer.server.port=8080"
      # backup intent (resticontainer, discovered from restic.* labels)
      - "restic.enable=true"
      - "restic.backup.paths=/config"
    restart: unless-stopped

  <sidecar>:
    image: ...
    network_mode: "service:<anchor>"

networks:
  direct:
    external: true
  enclave:
    external: true

Conventions at work:

  • One anchor per project holds the static ipv4_address on direct; sidecars share its network namespace via network_mode: service:<anchor>.
  • Networks are referenced by key (direct, enclave, socket) with external: true — the core composition owns their definitions.
  • enclave membership plus traefik labels publishes the service through the edge proxy; the proxy discovers it via the socket proxy, never via published ports.
  • Data paths are tier-relative (./@tierN/...) and resolve through the symlinks created by tiers/link.
  • Backup is opt-in via restic.* labels. apex backup/run drives resticontainer, which discovers what to snapshot from compose labels: restic.enable=true marks the service, restic.backup.paths=<container-path> names the mounts to back up (resticontainer resolves them to host paths). A service with no restic.* labels is simply not backed up. Back up config only where the data is regenerable.
  • The ${APEX_UID...} trio comes from the project .env maintained by tiers/useradd.

Provision storage and identity

apex tiers/link <tier1> <tier2> <tier3>
apex tiers/useradd
apex tiers/chown

Re-running the trio is idempotent; it picks up the new project, creates its tier directories and .env, provisions its noroot-<project> user, and applies the ownership matrix.

Lint

apex utils/lint-docker-compose

Validates every anchor IP (well-formed, unique, inside the node subnet) and runs the compose linter over the repository. The pre-commit hook enforces the same lint on staged compose files.

Deploy

apex compose up

The core composition is already up; the new project starts in its alphabetical slot. Verify with docker ps and, if the service is proxied, through its route on the edge.

{{% /steps %}}

Notes

  • Service-internal secrets (for example a database password) live in the project's own .env in tier-1 storage, alongside the auto-injected UID block.
  • Never define direct/enclave/socket inside a project file — a second definition would race the core composition for network ownership.

See also: