# Backend network capacity

The backend action prepares small networks before Compose starts a new project. It uses only
the running Docker daemon's existing IPv4 address-pool bases. It does not change daemon settings,
restart Docker, prune networks, or resize/reconnect existing projects.

New ordinary bridge networks normally receive a `/24`. An existing pool configured for a smaller
network keeps that smaller prefix, down to `/28`; unsupported sizes refuse. Subnets already allocated
to a network are excluded in full, even when few containers use them. An allocated `/16` cannot be
subdivided by this helper. All new networks retain separate Compose project/network ownership.
Creation also requests the selected subnet's first host address as its gateway and verifies that exact
readback. Moby28 inspection can omit an automatically assigned gateway when the subnet was explicit;
requesting both keeps the actual bridge gateway verifiable without accepting missing evidence.

## Supported scope

Only automatically named project bridge networks without authored IPAM, static service addresses,
custom driver options or IPv6 requirements are prepared. Authored labels and the `internal`/`attachable`
flags are preserved. External networks and custom names remain Compose's responsibility. The helper
never turns one of those networks into a different kind of network to recover capacity.

Existing networks retain their ID, subnet, configuration and connections. Missing or conflicting
project/network labels refuse adoption. Scoped ownership readback runs first; deployments that need
no new ordinary network do not open the allocation lock or require allocation-only host, routing or pool admission.
Every allocation acquires that lock and repeats ownership readback before selecting any subnet.
A tooling-created network carries its own configuration
fingerprint. A later change to its network contract refuses before Compose rather than silently
ignoring authored IPAM or replacing a network that may carry active workloads. An app requiring a
different network must implement that deliberate migration in its own deployment.

The helper intentionally does not manufacture `com.docker.compose.config-hash`. Compose 2 and Compose 5
accept a network with the correct project/network labels and no hash. A wrong hash can trigger
replacement. Its own fingerprint protects replay, including configuration changes. New-network
preparation admits Compose 2.x and 5.x; any other major version refuses with
`compose_version_unsupported` until it has been reviewed. Existing selected networks are preserved
under every Compose version because preservation never allocates.

Compose 5 review (2026-09-17, docker/compose tags v5.0.0, v5.3.1 and v5.5.1 against v2.40.3):
`resolveOrCreateNetwork` in `pkg/compose/create.go` at v5.0.0 and v5.3.1 returns the inspected
network when its `com.docker.compose.network` label matches and its config-hash label is empty or
equal (`hash == "" || hash == expected`), the same branch as v2.40.3; the remaining differences are
Docker client types, IPAM parsing and event emission. v5.5.1 moved reuse into `reconcileNetworks`
in `pkg/compose/reconcile.go`, whose observed state is scoped by the project label and which leaves a
network with no recorded hash untouched (`observed.ConfigHash == ""`). docker/compose carries no v3 or
v4 tags, and Compose 1 (`docker-compose`) stays unreviewed.

## Address-space proof

Custom effective pools are read from `docker info`. When that API returns no custom pools, only exact
reviewed Docker release versions may use the recorded Moby built-in pool facts. Source paths, release
tags and source hashes are retained in `backend-action/network_pool_facts.json`. Unknown versions
retain a diagnostic `pool_provenance_unknown`; an empty or failed read never selects guessed ranges.
The implementation uses explicit selected subnets across these supported versions. It does not depend
on Docker 29's size-only subnet syntax or introduce its downgrade compatibility restriction.

Preparation requires a local rootful Linux daemon in the host network namespace. It reads every Docker
network, host IPv4 addresses, peers, gateways, resolver addresses, all route tables and the supported
policy-rule chain. Standard Linux and the exact supported Tailscale rules are admitted; other routing
policies refuse. Network IDs, host exclusions and effective engine settings are rechecked for churn.
The inputs and resolved Compose environment stay private. Diagnostics publish selected metadata only.

One protected host lock covers selection, creation and readback across all applications using this
released helper. Builds and application startup do not hold it. Another Docker client can ignore this
lock: a newly observed competing Docker allocation restarts inventory within a three-attempt bound. An uncertain
create reply is read back before any retry. Existing networks are never a rollback target. A created
network remains available for the same project's retry if a later deployment stage fails.
Post-create host overlaps must belong to the exact created bridge's expected connected/local routes
or addresses; a foreign route, resolver or host prefix inside the new subnet still refuses.

Existing pool membership bounds the allocation policy; route snapshots do not prove that an arbitrary
private destination behind a default gateway is unused. The helper never expands into another RFC1918
range to conceal exhaustion. If all permitted space is already allocated or excluded, it reports
`existing_pool_capacity_exhausted` and leaves those networks intact.

## Read-only diagnosis

Use the app's installed Backend Deploy workflow with `operation: network-diagnostics`. It uses the
existing deployment identity but skips source sync, environment installation and deployment. Reviewed
package modules travel over SSH stdin and execute in memory; no helper file is installed on the host.

The `backend-network-diagnostics-<job>-<attempt>` artifact contains `result.json` with schema
`gowalk-cicd/backend-network-diagnostics.v2`. It records:

- Docker, Compose and Python versions with a UTC timestamp;
- effective pool bases/sizes, their provenance and any typed pool refusal;
- inventory completeness, network count and excluded IPv4 prefixes;
- only the selected project's network IDs, names, Compose keys, subnets and endpoint counts;
- a `validation` for each selected network, separating observed IPv4 overlap from verified pool membership.

Selected-network validation binds an unambiguous automatic Compose name to its project/key labels.
It supports local ordinary IPv4 bridges with one RFC1918 subnet, an explicit usable gateway and no
custom options. Other shapes report `unverified`; labels alone never exempt a host route. The exact
network ID determines the bridge device, and only its expected kernel connected/local/broadcast and
address records count as self records. Connected and local routes and addresses must actually appear.
Foreign Docker prefixes, foreign host routes, resolver addresses and unexpected records on the selected
bridge remain conflicts even when their prefix is identical to a self record. The public receipt retains
only overlapping prefixes, fixed host-record kinds and device relationships, never foreign project names
or raw host inventory. Each conflict list is limited to 256 entries; a truncated list retains `conflict`.

`validation.overlap` is `clear`, `conflict` or `unverified`. `pool_membership` independently reports
`inside_verified_pools`, `outside_verified_pools` or `unverified`; it does not measure spare capacity.
Top-level `ok:true` means the inventory read completed, so consumers must inspect each validation.
An outside-pool network can have a clear observed overlap check while still lacking an address-policy
reservation. This receipt does not authorize that range or certify destinations hidden behind a default
route. Establish any deliberate host-local reservation through the owning infrastructure policy and
recheck the live network before asserting permanent suitability. Provider metadata can establish a
host's identity and attached private subnet; it does not delegate another range to Docker.

For a DigitalOcean deployment, select `diagnostic-provider: digitalocean` on that same workflow.
The default `none` performs no provider read. The optional `host_identity` reads only six fixed leaves
from `169.254.169.254:80` on the already verified deployment host: Droplet ID, region, public interface 0
IPv4 address, and private interface 0 IPv4 address/netmask/gateway. It never requests an index, bulk
metadata, user data, tokens or credentials. The reader allows no proxy, redirect, retry, authentication
or destination override; it bounds each response to 128 body bytes and 8 KiB/16 header fields, with
two seconds per request and ten seconds overall.

Positive binding requires the returned public address to equal the configured literal IPv4 deployment
host and a kernel local address. The returned private address and derived subnet must match the same
unique non-bridge kernel interface. A usable private gateway is retained as a metadata fact; its absence
from kernel gateway records does not invalidate host identity or claim that the gateway is being used.
`host_identity.status` separates `verified`, `mismatch`, `unavailable` and `unverified`; failure of this
optional read never rewrites the independently observed network-overlap result. Interface slot 0 is
not a VPC/peering inventory. Current supported Tailscale kernel routes remain part of the overlap check;
no Tailscale preferences, state files, account data or control-plane route claims are fetched.

These facts can support a deliberate current host-local reservation by the authorized infrastructure
owner, recorded against the exact host, existing network ID, subnet, gateway and diagnostic revision.
Such a decision must state its host-local routing scope and require revalidation before future routing
or advertisement changes. It does not invent a historical provider delegation, expand daemon pools,
modify a network or turn a route snapshot into global address-space proof.

A routing refusal can retain safe engine/pool metadata with `complete:false` and `ok:false`. Such
partial evidence never admits allocation. A successful diagnostic is a readback, not a deployment,
capacity reservation, proof of application health or proof that another project cannot reach the app.
Raw container data, labels, environment, daemon configuration and command errors are not exported.
Policy-rule refusals use `host_rules_unsupported_<reason>` to distinguish count, fields, priority,
source, table, structure, fwmark, fwmask, action and duplicate mismatches. These fixed classifications
retain the strict supported-rule comparison and publish no rejected keys or values.
The exact decimal-string spellings of supported table IDs are normalized because
[iproute2 prints rule tables as JSON strings](https://github.com/iproute2/iproute2/blob/v6.8.0/ip/iprule.c#L370).
This does not admit additional routing tables, aliases, priorities or forwarding policies.

The deployment emits its own `gowalk-cicd/backend-networks.v1` receipt. The existing
`backend_deploy_failed` phase remains `compose_up` on a preparation refusal. Normal public health,
ingress authentication and exact build-SHA verification still determine backend deployment success.

## Verification

Focused tests exercise project isolation, ownership, incomplete observations, source-fact admission,
capacity exhaustion, competing allocations, lost replies and interrupted lock holders under Python 3.8.
The package's CI-only Docker proof keeps six new projects alive concurrently, verifies each API fixture can
reach its own HTTP service, checks cross-project HTTP refusal and replays Compose without changing
the network/container IDs. It preserves all pre-existing networks and removes only its exact test
projects, including a network created before a failed Compose startup. This proves fixture connectivity
and network isolation; it does not exercise PostgreSQL or database authentication.
The ordinary local test command does not contact a Docker daemon.

Adopt a published package through the existing installer and commit the generated actions, workflow,
configuration and lock changes. A copied helper or a local patch is not a released deployment path.
