# Docker Conflict. The container name is already in use in CI

> Docker Conflict. The container name is already in use in CI means a container from an earlier step or job still holds it. Learn where it survived.

Source: https://latchkey.dev/learn/docker/docker-container-name-already-in-use  
Updated: 2026-09-21

Docker Conflict. The container name is already in use means a container already holds the name you passed to `--name`, and names are unique per daemon whether or not the container holding one is running. The daemon is refusing to create a second container under a name it has already assigned, and it tells you the id of the container that has it.

## What this error means

A `docker run --name` step fails on the second run of a workflow and not the first, or fails in one job while an identical job elsewhere passes. The name in the message is quoted and carries a leading slash that you did not write, which is the daemon printing its own stored form of the name. The container blocking you is often stopped rather than running, so it does not appear in a plain `docker ps` and the name looks free when it is not.

```Reconstructed from nameConflictError in moby daemon/errors.go at v28.5.2, not a recorded run
docker: Error response from daemon: Conflict. The container name "/postgres-test" is already in use by container "9c4a1f6e2b7d". You have to remove (or rename) that container to be able to reuse that name.
```

## Common causes

### A container from an earlier job is still registered

The dominant cause on any runner that serves more than one job. The previous job failed, or was cancelled, before whatever would have removed the container ran, and the daemon has held the name ever since. The container is usually stopped, which is why it does not show up in the first place people look.

### An earlier step in the same job started it

A setup step starts a database with a fixed name, a later step tries to start it again because it cannot tell whether the first one ran, and the second attempt collides with the first. This is the version that also happens on hosted runners, and it is the only one that does.

### Two matrix legs share a daemon and a hardcoded name

Matrix legs that land on the same self-hosted machine run against the same daemon, so a name that is fixed in the workflow file is fixed for all of them. In our experience this presents as intermittent, because it depends on which legs are scheduled together.

### An action you call picks the name for you

Some actions start service containers under names of their own choosing. When one of those is left behind, the conflict is between two runs of the action rather than anything in your workflow, and the only fix available to you is to remove the container before the action runs.

## How to fix it

### Remove the blocking container, using the id in the message

1. Look for stopped containers too, with `docker ps -a`, not just `docker ps`.
2. Remove by the id the error printed, which is unambiguous even if names repeat.
3. Then start the container again, and keep the removal in the job so the next run is not manual.

```Terminal
docker ps -a --filter name=postgres-test --format '{{.ID}} {{.Names}} {{.Status}}'
docker rm -f postgres-test
```

### Make the name unique per run

Suffix the name with the run id, and the job index when a matrix is involved, so no two runs and no two legs can ever ask for the same one. This is the fix that keeps working when the workflow grows, because it does not depend on cleanup having succeeded.

```Terminal
NAME="pg-${GITHUB_RUN_ID}-${GITHUB_JOB}"
docker run -d --name "$NAME" postgres:17
echo "NAME=$NAME" >> "$GITHUB_ENV"
```

### Stop naming containers that nothing addresses by name

If the only reason for `--name` is that an example had one, drop it and keep the container id instead. The daemon will generate a unique name, nothing can collide, and the id is a better handle for scripts anyway because it is guaranteed unique.

```Terminal
CID=$(docker run -d postgres:17)
docker logs "$CID"
docker rm -f "$CID"
```

### Clean up in a step that runs even when the job fails

Use `if: always()` so teardown happens after a failed test run as well as a passing one. This is the difference between one bad job and every subsequent job on that machine failing the same way until somebody logs in.

```.github/workflows/ci.yml
- name: Tear down
  if: always()
  run: docker rm -f postgres-test || true
```

## How to prevent it

- Prefer generated names and container ids over fixed names in CI.
- When a stable name is required, make it unique per run and per matrix leg.
- Put teardown in an `if: always()` step, not at the end of the happy path.
- On self-hosted runners, prune stopped containers on a schedule, not by hand.

## Why the name has a slash in front of it

The leading slash is the first thing people notice and the last thing anyone explains. The daemon stores container names in a namespace whose paths begin at the root, so the name you passed as `postgres-test` is held as `/postgres-test`, and the message prints the stored form. The quotes around it are not yours either: the message is built in moby `daemon/errors.go`, where `nameConflictError` renders both the name and the blocking container id as quoted Go strings.

That is useful rather than trivia, because it means you can trust the message completely. The name in the quotes is exactly what the daemon has recorded, so a mismatch between it and what you think you passed is itself the bug, and the id in the second pair of quotes is a container you can inspect right now.

| Why the name is still taken | Where the container holding it is |
| --- | --- |
| A previous job on the same machine did not clean up | Stopped, on a reused self-hosted runner. Invisible to plain `docker ps` |
| An earlier step in this job started it and moved on | Running, in the same workflow. Visible in `docker ps` |
| The workflow was rerun after failing mid-way | Stopped, from the failed attempt, because cleanup never ran |
| Two matrix legs share one daemon and one fixed name | Running, started by the sibling leg moments earlier |

> The blocking container does not have to be running. `docker ps` shows running containers only, so check with `docker ps -a` before deciding the name is free.

## Hosted runners hide this and self-hosted runners do not

On a GitHub-hosted runner every job gets a fresh virtual machine, so the daemon has no history and a fixed name cannot collide across jobs. That is why a workflow can use hardcoded names for months and then break the week it moves to self-hosted runners, where the same daemon serves job after job and remembers everything that was not removed.

This also explains the reports that involve container jobs and actions that start services. When an action runs `docker run --name` with a name it chose, and a previous run of that action left the container behind, the collision is between two runs of somebody else's code and there is nothing in your workflow to change. Removing the leftover before the action runs is the only lever you have.

```.github/workflows/ci.yml
- name: Clear anything a previous job left behind
  run: docker rm -f postgres-test 2>/dev/null || true

- name: Start it fresh
  run: docker run -d --rm --name postgres-test postgres:17
```

## Unique names beat cleanup, and cleanup beats hope

There are two durable answers and they suit different jobs. If nothing needs to address the container by name, do not give it one: let the daemon generate a name and capture the id, and a collision becomes unreachable rather than handled. If something does need a stable name, such as another container resolving it over a network alias, make the name unique per run by suffixing it with the run id.

Then add the cleanup anyway, with an always-run step, because the case that bites you is the job that failed before its own teardown. `--rm` covers a container that exits on its own and does not cover a job that was cancelled while the container was still up.

```.github/workflows/ci.yml
- name: Start with a name nothing else can hold
  run: |
    docker run -d --name "pg-${{ github.run_id }}-${{ strategy.job-index }}" postgres:17

- name: Always clean up
  if: always()
  run: docker rm -f "pg-${{ github.run_id }}-${{ strategy.job-index }}" || true
```

## What the runner does about it

No repair, and no recorded run. The reason here is that the failure is made of state that a hosted runner does not have: it needs a container left over from a previous job on the same daemon, and our reproduction harness starts each job on a clean machine. To record it we would have to keep a machine alive across two jobs specifically so the first could litter for the second, which reproduces self-hosted runner reuse rather than anything about our runners.

The useful consequence is worth stating the other way around. If your workflow hits this on Latchkey runners, it is not leftover state from yesterday, because there is none; it is an earlier step in the same job that started the container and did not stop it. That narrows the search to the workflow file in front of you.

## FAQ

### Why does the container name in the error have a slash in front of it?

Because that is how the daemon stores it. Names live in a namespace rooted at a slash, so `postgres-test` is recorded as `/postgres-test`, and the conflict message prints the stored form inside quotes. You do not need to include the slash when you pass the name.

### Why does docker ps show nothing when the name is taken?

Because `docker ps` lists running containers only, and a stopped container keeps its name until it is removed. Use `docker ps -a` to see it. This is the single most common reason people conclude the error is wrong about the name being in use.

### Why did this start happening only after we moved to self-hosted runners?

Because hosted runners give each job a fresh machine with an empty daemon, so a fixed name cannot survive from one job to the next. A self-hosted runner reuses the same daemon, which remembers every container that was not removed, so the same workflow suddenly collides with its own history.

### Does --rm prevent this on its own?

Only partly. It removes the container when it exits normally, which covers the ordinary path and not the one that causes the trouble: a job cancelled or failed while the container is still running leaves it behind with the name held. Pair `--rm` with an `if: always()` cleanup step.

## References

- [moby v28.5.2: nameConflictError, the source of this message, daemon/errors.go](https://github.com/moby/moby/blob/v28.5.2/daemon/errors.go)
- [supercharge/mongodb-github-action#24: the conflict reported from a GitHub Actions job](https://github.com/supercharge/mongodb-github-action/issues/24)
- [Docker docs: docker run, the --name and --rm options](https://docs.docker.com/reference/cli/docker/container/run/)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
