Docker Conflict. The container name is already in use in CI
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.
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.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 |
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
- Look for stopped containers too, with
docker ps -a, not justdocker ps. - Remove by the id the error printed, which is unambiguous even if names repeat.
- Then start the container again, and keep the removal in the job so the next run is not manual.
docker ps -a --filter name=postgres-test --format '{{.ID}} {{.Names}} {{.Status}}'
docker rm -f postgres-testMake 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.
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.
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.
- name: Tear down
if: always()
run: docker rm -f postgres-test || trueHosted 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.
- 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:17Unique 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.
- 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 }}" || trueWhat 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.
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.
Frequently asked questions
Why does the container name in the error have a slash in front of it?
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?
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?
Does --rm prevent this on its own?
--rm with an if: always() cleanup step.