4.1 KiB
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_addressondirect; sidecars share its network namespace vianetwork_mode: service:<anchor>. - Networks are referenced by key (
direct,enclave,socket) withexternal: true— the core composition owns their definitions. enclavemembership 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 bytiers/link. - Backup is opt-in via
restic.*labels.apex backup/rundrives resticontainer, which discovers what to snapshot from compose labels:restic.enable=truemarks the service,restic.backup.paths=<container-path>names the mounts to back up (resticontainer resolves them to host paths). A service with norestic.*labels is simply not backed up. Back up config only where the data is regenerable. - The
${APEX_UID...}trio comes from the project.envmaintained bytiers/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
.envin tier-1 storage, alongside the auto-injected UID block. - Never define
direct/enclave/socketinside a project file — a second definition would race the core composition for network ownership.
See also: