Dependency Tree Visualization
Extract, containerize, and visualize service dependency graphs for local dev. Detect cycles, enforce startup order, and gate drift against the staging topology.
"The stack is broken" is rarely true; what is true is that one service depends on another that never came up, came up out of order, or formed a circular wait. Making that graph explicit turns a vague outage into a precise missing edge. This guide extracts the dependency graph from your manifests and Compose file, renders it into a shareable artifact, and gates it against the staging topology so local environments stop diverging silently. It supports the wider effort on onboarding architecture and friction mapping, and the two most common downstream tasks — mapping microservice dependencies for local dev and detecting circular dependencies in local builds — have dedicated walkthroughs.
Prerequisites
- Docker Engine 24+ with the Compose v2 plugin.
jqandnpxon the host (or run them throughdocker run --rm node:20-alpine).- A
docker-compose.ymland at least one lockfile (package-lock.json,go.sum,poetry.lock).
Dependency Graph Extraction & Lockfile Parsing
Turn raw manifests into a machine-readable adjacency list so downstream steps have a deterministic baseline.
- Extract the service-to-dependency edges straight from Compose:
#!/usr/bin/env bash set -euo pipefail docker compose config --format json \ | jq '[.services | to_entries[] | {source: .key, targets: (.value.depends_on // {} | keys)}]' \ > normalized_deps.json jq 'length' normalized_deps.json - Validate the structure and look for cycles:
#!/usr/bin/env bash set -euo pipefail jq -e 'type == "array"' normalized_deps.json >/dev/null && echo "valid graph" npx madge --circular --json src/ > cycles.json jq 'length == 0' cycles.json && echo "no import cycles" - Drift check — alert when the graph changes versus the committed baseline:
#!/usr/bin/env bash set -euo pipefail BASELINE_HASH=$(cat .cache/baseline.sha256) CURRENT_HASH=$(sha256sum normalized_deps.json | awk '{print $1}') if [ "$BASELINE_HASH" != "$CURRENT_HASH" ]; then echo "Dependency graph changed since baseline; review before commit." >&2 fi
WSL2: run extraction on the Linux filesystem (
~/project);/mnt/cI/O makes lockfile parsing crawl. Apple Silicon (ARM64): if hostjq/npxconflict, run them indocker run --rm -v "$(pwd):/work" -w /work node:20-alpine. macOS (Docker Desktop): under heavy parsing, VirtioFS can throw transientEBUSY; rerun on a named volume rather than a bind mount.
Containerized Topology & Startup Ordering
Map the adjacency list to a Compose manifest with health-gated ordering so dependent services never start against a cold database.
- Declare healthchecks and
depends_onconditions:# docker-compose.yml services: api: image: "${REGISTRY}/api:latest" depends_on: db: condition: service_healthy cache: condition: service_healthy networks: - dev-mesh environment: DB_HOST: db CACHE_HOST: cache db: image: postgres:16-alpine healthcheck: test: ["CMD-SHELL", "pg_isready -U app"] interval: 5s timeout: 3s retries: 5 networks: - dev-mesh cache: image: redis:7-alpine healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 5 networks: - dev-mesh networks: dev-mesh: driver: bridge - Verify the topology and that names resolve on the network:
#!/usr/bin/env bash set -euo pipefail EXPECTED_DEPS=2 ACTUAL_DEPS=$(docker compose config | grep -c 'condition: service_healthy') [ "$ACTUAL_DEPS" -eq "$EXPECTED_DEPS" ] && echo "topology matches" || { echo "missing healthcheck conditions" >&2; exit 1; } docker compose exec api getent hosts db docker compose exec api getent hosts cache
When names do not resolve, follow resolving DNS resolution failures between local containers.
macOS (Docker Desktop): default 2-core/4GB limits make healthchecks time out under concurrent boot; raise resources in Settings. WSL2:
host.docker.internalbehaves differently; addextra_hostsif a service needs a host-loopback callback.
Automated Seed Data & State Injection
Inject deterministic fixtures during initialization so every workstation and CI runner boots identical state.
- Run a fixture seed from the dev container's lifecycle hook:
// .devcontainer/devcontainer.json { "name": "dev-container-workspace", "image": "mcr.microsoft.com/devcontainers/base:ubuntu", "features": { "ghcr.io/devcontainers/features/docker-in-docker:2": {} }, "postCreateCommand": "./scripts/seed-db.sh --mode=local --fixtures=baseline" } - Verify row counts against the fixture manifest and fail on divergence:
#!/usr/bin/env bash set -euo pipefail EXPECTED_ROWS=$(jq '.core_tables' .cache/fixture_manifest.json) ACTUAL_ROWS=$(docker compose exec -T db psql -t -A -U app -d app_db -c "SELECT COUNT(*) FROM core_tables;") if [ "$EXPECTED_ROWS" -ne "$ACTUAL_ROWS" ]; then echo "Row count delta: expected $EXPECTED_ROWS, got $ACTUAL_ROWS." >&2 exit 1 fi
Seed runs frequently stall on a port collision; reconcile that against common local failure points first.
WSL2: ensure
seed-db.shhaschmod +xand LF endings, orpostCreateCommandfails withbad interpreter. Apple Silicon (ARM64): client binaries in init containers must match the host arch or pinplatform: linux/amd64.
Visualization Pipeline
Render the normalized graph into a version-controlled artifact engineers can actually read.
- Generate a static graph from the adjacency list:
# Makefile .PHONY: visualize visualize: npx @antv/g6-cli render \ --input=normalized_deps.json \ --output=docs/dep-graph.html \ --theme=dark \ --layout=force @echo "Rendered docs/dep-graph.html" - Verify the artifact exists and node counts match the source:
#!/usr/bin/env bash set -euo pipefail test -f docs/dep-graph.html || { echo "render failed" >&2; exit 1; } SRC_NODES=$(jq 'length' normalized_deps.json) ARTIFACT_NODES=$(grep -o 'data-node-id' docs/dep-graph.html | wc -l | tr -d ' ') [ "$SRC_NODES" -eq "$ARTIFACT_NODES" ] && echo "node parity verified" || echo "node mismatch" >&2
Apple Silicon (ARM64):
@antv/g6-clineeds native canvas bindings; installlibcairo2-devandpkg-configbeforenpm install. WSL2: serve the HTML from inside the Linux filesystem (python3 -m http.server 8080) rather than a network drive.
Drift Detection & Parity Gates
Catch local topology diverging from staging before a pull request merges.
- Compare local and baseline graphs in CI:
# .github/workflows/parity-check.yml name: Dependency Parity on: pull_request: schedule: - cron: "0 2 * * 1" jobs: diff: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Extract local topology run: ./scripts/extract-graph.sh --output=local_deps.json - name: Compare against staging baseline run: | ./scripts/compare-graph.sh \ --baseline=staging_deps.json \ --target=local_deps.json \ --threshold=0.05 \ --report=parity_report.md - uses: actions/upload-artifact@v4 with: name: parity-compliance path: parity_report.md - Fail the gate when divergence exceeds the threshold:
#!/usr/bin/env bash set -euo pipefail DIVERGENCE=$(grep -oP 'divergence: \K[0-9.]+' parity_report.md) if awk "BEGIN { exit !($DIVERGENCE > 0.05) }"; then echo "Local topology diverges from staging by >5%." >&2 exit 1 fi echo "Parity validated against staging baseline."
CI runners vs local: GitHub Actions runs
x86_64; keepcompare-graph.sharchitecture-agnostic and avoid hardcoded/usr/local/binpaths. Corporate proxy: setNO_PROXY=127.0.0.1,localhost,.localso local mesh syncs are not intercepted.
Rollback / Recovery
If a topology change wedges the stack, tear it down and prune dangling artifacts before retrying:
#!/usr/bin/env bash
set -euo pipefail
docker compose down --remove-orphans --volumes
docker network prune -f
git checkout -- docker-compose.yml normalized_deps.json