# Local Deployment

> How DevOps composes the local environment using Docker so QA-Exec can run end-to-end tests AND the operator can manually trial features in a browser. Probes available host ports instead of hardcoding, discovers project .env/template/env_file requirements without exposing secrets, never edits the project's compose file directly, and produces a deploy report with QA test-environment fields and huma…

- **Type:** Skill
- **Install:** `agentstack add skill-evolplus-talos-local-deployment`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [evolplus](https://agentstack.voostack.com/s/evolplus)
- **Installs:** 0
- **Category:** [Cloud & Infrastructure](https://agentstack.voostack.com/c/cloud-infrastructure)
- **Latest version:** 0.1.0
- **License:** Apache-2.0
- **Upstream author:** [evolplus](https://github.com/evolplus)
- **Source:** https://github.com/evolplus/talos/tree/main/skills/local-deployment

## Install

```sh
agentstack add skill-evolplus-talos-local-deployment
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Local Deployment

## When to use

You are DevOps, dispatched against a task in status `ready-for-deploy`. Your job is to bring the local environment up — typically FE + BE + datastores composed via Docker — so two consumers can work against it:

1. **QA-Exec** runs the test runner against `base_url` / `api_base_url` per the kit's QA-Exec Run Contract.
2. **The operator** (PM, designer, eng lead) opens the running app in a browser to manually trial the feature before sign-off — confirming the feature looks and behaves right against intent, not just against test assertions.

The skill covers Docker-based composition, port discovery (probe-then-bind, never hardcode), and deploy-report population. For non-Docker stacks (rare in modern projects), consult `docs/architecture.md` for the project's documented run mode.

## Project-scoped container discipline (safety rule)

The kit dispatches DevOps inside an operator's environment that almost always has unrelated containers running — the operator's personal Postgres, another project's Redis, a sibling repo's docker-compose stack. A careless `docker compose down -v` would nuke them all. DevOps's blast radius is **scoped to the current project's containers only**.

### Project slug — single source of truth

Determine the project slug (the Compose project name) once at deploy start; thread it through every subsequent docker command. Lookup priority:

1. **`COMPOSE_PROJECT_NAME` environment variable** if set (most explicit; respects the operator's intent).
2. **SRS header's project name field** if Phase 0 read SRS successfully — sanitize: `lowercase(name).replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')`. Example: `"Stats Overflow"` → `stats-overflow`.
3. **Working-directory basename** as a fallback when neither of the above is available. Same sanitization.

Record the chosen slug at the top of the deploy report (`## Test Environment` block adds `project_slug: ` field) so QA-Exec and the operator can verify.

### Every command takes the slug

Apply the slug to every docker invocation:

```bash
slug="stats-overflow"  # from lookup above

# Compose: pass -p explicitly
docker compose -p "$slug" -f docker-compose.yml -f docker-compose.override.yml up -d --wait
docker compose -p "$slug" down                          # tear-down
docker compose -p "$slug" logs --tail=200 api           # inspection

# Plain docker ps: filter by Compose-project label
docker ps --filter "label=com.docker.compose.project=$slug"
docker ps --filter "name=^${slug}-"                     # if not using Compose labels

# Container operations: name MUST start with the slug
container_name="${slug}-api-1"
docker inspect "$container_name"
docker logs "$container_name"
```

### Out-of-scope reads — explicitly allowed (for conflict detection only)

Conflict detection legitimately requires reading what ELSE is running on the host. The following READ operations are permitted on out-of-scope containers / system state:

| Operation | Why allowed |
|---|---|
| `docker ps` (no filter) | Conflict detection — what ports are in use, what's running |
| `docker ps -a` | Same — including stopped containers |
| `docker inspect ` | Read-only inspection for conflict resolution |
| `docker logs ` | Read-only diagnostics if a port owner is identified |
| `docker port ` | Read which ports a container is publishing |
| `docker stats --no-stream` | Resource visibility |
| `docker version` / `docker info` | System info |
| `docker network ls` / `docker network inspect` | Network state visibility (read-only) |
| `docker volume ls` / `docker volume inspect` | Volume state visibility (read-only) |

These are HOW DevOps probes ports + detects conflicts. They never mutate state.

### Out-of-scope mutations — explicitly forbidden

DevOps MUST NOT run any of the following:

| Operation | Why forbidden |
|---|---|
| `docker stop ` where `` doesn't start with `` | Could stop the operator's personal services |
| `docker rm ` where `` doesn't start with `` | Same |
| `docker kill ` where `` doesn't start with `` | Same |
| `docker restart ` where `` doesn't start with `` | Same |
| `docker pause` / `unpause ` outside slug | Same |
| `docker compose -p ` ANY mutation | Operates on another project entirely |
| `docker compose down` without `-p ` | Defaults to cwd basename → might match wrong project if cwd is non-standard |
| `docker system prune` | Nukes ALL unused containers / networks / volumes across all projects |
| `docker volume prune` | Nukes unused volumes globally |
| `docker network prune` | Nukes unused networks globally |
| `docker container prune` | Nukes all stopped containers globally |
| `docker image prune` | Nukes unused images globally — affects other projects' rebuild times |
| `docker rm -f $(docker ps -q)` (and variants with `$(docker ps ...)`) | Force-removes ALL running containers |
| `docker stop $(docker ps -q)` | Stops ALL running containers |
| `docker volume rm ` outside slug | Could destroy operator's data |
| `docker network rm ` outside slug | Could break other projects' networking |
| `docker image rm ` | Affects every project that uses the image |

The `docker-scope-guard.cjs` hook enforces these at runtime — DevOps will be refused before the command runs.

### Conflict resolution — non-mutating recourse

When an out-of-scope container holds a port the kit project needs:

1. **Probe and report** — DevOps notes "port 5432 occupied by `mysite-postgres-1` (not in scope)" in the deploy report.
2. **Pick a different port** — port-probe algorithm finds a free one (see Step 4 above).
3. **Surface to the operator if no port is available** — write the conflict into `docs/open-issues.md` with category `local-port-exhaustion`, halt the dispatch, return NEEDS_CONTEXT asking the operator to manually free a port (their decision; they own those containers).

NEVER auto-resolve by stopping the other container.

## Inputs and outputs

- **Inputs:** the project's existing `docker-compose.yml` (or equivalent); project env templates and compose `env_file:` references; `docs/architecture.md` C2 Containers section (to know what services to expect); the task ID; SRS §3.4 Technical Constraints if it pins ports.
- **Outputs:** a running local environment (containers up + health-checked); `docs/deploy-reports/.md` with the standard Test Environment block + a new `## Human Trial URLs` section both QA and the operator consume.

## Procedure

### Step 1 — Verify Docker

Run two checks:

```bash
docker info > /dev/null 2>&1 || { echo "docker daemon not running"; exit 1; }
docker compose version > /dev/null 2>&1 || { echo "docker compose v2 not available"; exit 1; }
```

If either fails, halt with `NEEDS_CONTEXT`:

```
Status: NEEDS_CONTEXT
Reason: Docker prerequisite missing.
Question: docker daemon not running OR docker compose v2 not installed. The local-deployment skill requires both.
Suggested resolution: Start Docker Desktop / colima / podman; verify with `docker info` then `docker compose version`.
```

Do NOT attempt to install Docker yourself — that's an operator decision.

### Step 1.5 — Detect host architecture + compute target platform

Docker images are architecture-specific. The kit's most common failure mode here is silently shipping `amd64` images on Apple Silicon (arm64 Darwin) — they technically run via Rosetta emulation but at 30–60% performance penalty, occasional binary incompatibilities (e.g., native `node-gyp` modules), slow startup, and confusing error messages. Detect host arch BEFORE bringing the env up; match images deliberately.

```bash
host_os=$(uname -s)       # Darwin / Linux
host_arch=$(uname -m)     # x86_64 / arm64 / aarch64

# Normalize to Docker platform notation
case "$host_arch" in
  x86_64|amd64)         target_platform="linux/amd64" ;;
  arm64|aarch64)        target_platform="linux/arm64" ;;
  *)                    echo "Unknown arch: $host_arch"; exit 1 ;;
esac

echo "Host: $host_os $host_arch → target_platform=$target_platform"
```

**Host → target-platform mapping:**

| Host | uname -m | target_platform | Notes |
|---|---|---|---|
| Apple Silicon Mac (M1/M2/M3/M4) | `arm64` | `linux/arm64` | Native; preferred |
| Intel Mac | `x86_64` | `linux/amd64` | Native |
| Linux x86_64 (most servers / cloud VMs) | `x86_64` | `linux/amd64` | Native |
| Linux ARM64 (AWS Graviton, Ampere, RPi) | `aarch64` | `linux/arm64` | Native |
| Windows (WSL2) | per WSL distro | per arch | Same rules apply inside WSL |

**Inspect compose file for explicit platform pins.** Many projects pin services to `linux/amd64` for historical reasons (image vendor only published amd64, team standardized on Intel laptops, etc.). Walk the compose file's `services.*.platform` field:

```bash
# Pseudo: extract platform pins
yq '.services[] | select(.platform != null) | .platform' docker-compose.yml
```

For each pinned service, classify against the host:

| Compose platform | Host target | Action |
|---|---|---|
| Not pinned | any | Pass `--platform=$target_platform` to docker compose; Docker selects native image if available, falls back to multi-arch manifest |
| `linux/amd64` | `linux/amd64` host | Native; proceed |
| `linux/amd64` | `linux/arm64` host (Apple Silicon!) | **Emulation warning** — image runs via Rosetta/QEMU at ~50% performance. Flag in deploy report; consider asking SA for a multi-arch base image migration. |
| `linux/arm64` | `linux/arm64` host | Native; proceed |
| `linux/arm64` | `linux/amd64` host | **Emulation warning** (rare; usually means compose was authored on Apple Silicon and never re-tested on Intel CI) |
| Multi-arch image (no platform pin, image manifest lists both) | any | Docker picks native automatically; preferred |

**Apply the platform to every relevant command.** Pass `--platform=$target_platform` to:

- `docker compose -p  up -d --wait` → if compose-file `platform:` fields aren't explicit, this overrides; if they ARE explicit, this gets ignored per-service (compose-file wins).
- `docker build --platform=$target_platform -t  .` (and `buildx` invocations) — when building from local Dockerfiles.
- `docker run --platform=$target_platform ` — when running standalone containers.

**For projects shipping amd64-only images to mixed-arch teams:** the right answer is a multi-arch base image (build with `docker buildx build --platform=linux/amd64,linux/arm64`). Surface this as an open-issue with category `multi-arch-base-needed` and let SA pick up the migration.

### Step 2 — Discover the project's compose definition

Look for, in order:

1. `docker-compose.yml` at project root (most common).
2. `compose.yml` (Compose v2 convention).
3. `infra/docker-compose.yml`, `deploy/docker-compose.yml` (per kit's "project-owned reusable infra" pattern).
4. A multi-file split: `docker-compose.yml` + `docker-compose.local.yml` (project's `local` overlay).

If NONE exists, halt:

```
Status: NEEDS_CONTEXT
Reason: No compose file found at project root or under infra/ / deploy/.
Question: The kit cannot author a compose file inline (per the DevOps template's "project-owned reusable infra" rule). Either:
  [a] The project doesn't have local-deploy machinery yet — needs an architecture decision (escalate to SA / TL via open-issue with category `infra-decision-pending`).
  [b] The compose file lives elsewhere — name the path.
```

### Step 2.5 — Discover environment files and validate env readiness

Do this before port probing or `docker compose up`. Many local deploy failures are really env-loading failures: Compose was run from the wrong directory, the root `.env` was not loaded, a service-level `env_file:` was missing, or QA ran with defaults that differ from the operator's intended local setup.

Classify env files:

- **Operator-owned secret env files:** `.env`, `.env.local`, `.env.development`, `.env.test`, `.env.`, and any file named by compose `env_file:` unless it is an allowlisted template. Detect existence and usage, but never read or print values.
- **Safe templates:** `.env.example`, `.env.template`, `.env.sample`, including service-specific variants. These may be read to collect required key names and comments.

Procedure:

1. Inspect the selected compose files and project run docs for:
   - `env_file:` entries per service;
   - `${VAR}` / `${VAR:-default}` / `${VAR:?required}` interpolation placeholders;
   - documented local env-file order, for example `.env` then `.env.local`.
2. Read safe templates only. Extract key names, defaults, and comments; do not treat template placeholder values as deploy secrets.
3. Detect whether operator-owned env files exist and whether every compose-referenced `env_file:` path exists. Do not copy env files into the worktree and do not create missing files.
4. Build a `compose_env_args` list for every later Compose command:
   - run from the project root or pass `--project-directory ` so root `.env` participates in interpolation;
   - include explicit `--env-file ` only when the project docs/scripts/compose setup declare that file order;
   - preserve service-level `env_file:` entries in compose instead of copying them into the generated override.
5. Validate the selected compose/env files before port probing:

   ```bash
   # Use the same -p, -f, --project-directory, and --env-file arguments that the final deploy will use,
   # but do not include the generated override yet because Step 5 has not created it.
   docker compose --project-directory "$project_root" "${compose_env_args[@]}" -p "$slug" -f docker-compose.yml config --quiet
   ```

   Use `config --quiet`, not full `docker compose config`, because full config output can print resolved secret values.
6. If validation fails because a required env var or env file is missing, halt with `NEEDS_CONTEXT`. Ask the operator to create/update the project-local env file manually. Do not write `.env` yourself.
7. If privacy hooks block key-only inspection of operator-owned env files, record `key_status: not inspected (privacy guard)` in the deploy report. Do not bypass privacy just to count keys.

The deploy report must include an env summary with file names and statuses only:

```markdown
### Env File Awareness

| Item | Status | Notes |
|---|---|---|
| `.env` | present | operator-owned; values not read or printed |
| `.env.local` | absent | not declared by project docs |
| `api/.env` via compose `env_file` | present | service-level env file |
| `.env.example` | read | 18 documented keys |

- compose_config_quiet: pass
- missing_required_env: none
- secret_values_redacted: true
```

### Step 3 — Determine preferred port ranges

Consult, in priority order:

1. **SRS §3.4 Technical Constraints** if it names port preferences for the project.
2. **`.claude/skills/solution-defaults/references/defaults-table.md`** if it lists project port defaults.
3. **The compose file's published ports** (`ports:` lists; treat as preferences, not mandates — they may conflict on the host).
4. **Kit defaults** (table below) when none of the above apply.

| Service type | Preferred range | Container-internal default |
|---|---|---|
| Web FE (Next.js / Vite / CRA) | 3000–3010 | 3000 |
| Backend HTTP API | 4000–4010 OR 8080–8090 | varies |
| Admin / internal | 4100–4110 | varies |
| WebSocket gateway | 4200–4210 | varies |
| Postgres | 5432–5442 | 5432 |
| MySQL | 3306–3316 | 3306 |
| Redis | 6379–6389 | 6379 |
| Elasticsearch | 9200–9210 | 9200 |
| Kafka broker | 9092–9102 | 9092 |
| RabbitMQ | 5672–5682 | 5672 |
| MinIO / S3-compat | 9000–9010 + 9100–9110 (console) | 9000 / 9001 |

The preferred range gives predictability for the operator (no random ports each run) while leaving headroom when the defaults are occupied.

### Step 4 — Probe ports

For each service, walk its preferred range and find the first host port not in LISTEN state:

```bash
probe_port() {
  local start=$1 end=$2
  for port in $(seq "$start" "$end"); do
    if ! lsof -iTCP:"$port" -sTCP:LISTEN -t > /dev/null 2>&1; then
      echo "$port"; return 0
    fi
  done
  retur

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [evolplus](https://github.com/evolplus)
- **Source:** [evolplus/talos](https://github.com/evolplus/talos)
- **License:** Apache-2.0

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-evolplus-talos-local-deployment
- Seller: https://agentstack.voostack.com/s/evolplus
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
