DevOps

Fix Could not find a required file: index.html in Docker

Prove whether a volume is hiding public/index.html, fix the mount path or the COPY order, and know why docker run shows the file while Compose does not.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

TL;DR#

react-scripts start and react-scripts build check for public/index.html and src/index.js before anything else, and print this error when the first is missing. In Docker the file is usually not missing from the image: a bind mount in docker-compose.yml has replaced the image's public/ folder with a host folder that is empty, or that Docker had to create because the path did not exist. Fix the host path of the mount (or remove the mount), or, when the error comes from npm run build in a Dockerfile, make sure COPY . . runs before the build.

The question on Stack Overflow shows the confusing part. Compose prints the error, yet index.html is plainly there when the asker opens a shell in the image:

text
web_1     | Could not find a required file.
web_1     |   Name: index.html
web_1     |   Searched in: /usr/src/app/web_client/public

Why the file vanishes only under Compose#

The check is a few lines in react-dev-utils. scripts/start.js in react-scripts calls checkRequiredFiles([paths.appHtml, paths.appIndexJs]) on line 50 of its start script. Here appHtml resolves to public/index.html under the working directory. If the file cannot be accessed, the function prints the three lines above and react-scripts exits. It does not look anywhere else.

The asker's Compose service mounts two host folders into the container:

yaml
volumes:
  - ./web_client/src:/usr/src/app/web_client/src
  - ./web_client/public:/usr/src/app/web_client/public

A bind mount does not merge with what the image has at that path. It replaces it, as the Docker bind mounts documentation explains for mounting over a non-empty directory. Whatever COPY . . put into /usr/src/app/web_client/public stays in the image but is hidden while the mount is in place. If the host side of the mount is empty, the container sees an empty public/.

The asker's own test explains the paradox. They ran docker run -t -i vocabularyapp_web /bin/bash, which uses no volumes, so it showed the image's files. Compose adds the mounts, and the mounts hide them.

This was reproduced on 4 October with Docker Desktop on Windows 11, a node:22-alpine image, and the same checkRequiredFiles call react-scripts makes, from react-dev-utils 12.0.1. The image was built with COPY . ., and without a mount the check passed:

text
index.html
required files found

Mounting an empty host folder over public/ was the only change, and the check failed with the reader's exact message:

text
Could not find a required file.
  Name: index.html
  Searched in: /usr/src/app/web_client/public
exit=1

A host path that does not exist gives the same result. With the short -v syntax, Docker created the missing folder on the host, empty, and mounted it. After that run, the folder existed on the test machine. A typo in the left side of a volumes: line therefore fails in the same way as an empty folder, with no warning.

The asker's own report names the setup: Windows 10 with the Docker Quickstart Terminal. Whatever the exact cause on that specific machine, the check below finds it without needing to guess: compare what the image contains against what the running container actually sees.

Fix#

1. Prove that a mount is hiding the file#

Compare the image without mounts and the running Compose container:

bash
docker image ls
docker compose run --rm --no-deps web ls -la /usr/src/app/web_client/public
docker run --rm --entrypoint ls vocabulary-app-web -la /usr/src/app/web_client/public

Replace vocabulary-app-web with the image name docker image ls shows for the service. The asker's thread used the older docker-compose, which named it vocabularyapp_web. The second command applies the service's volumes and the third does not. If only the third lists index.html, a mount is the cause. To see exactly what is mounted where:

bash
docker inspect --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}' "$(docker compose ps -aq web)"

-a matters: react-scripts exits after the failed check, so the container is stopped and docker compose ps lists only running containers by default. Each Source is the host folder Docker actually used. Check that it is the folder you meant, and that it holds index.html.

2. Point the mount at the right folder, or remove it#

Relative paths in docker-compose.yml resolve from the directory that holds the Compose file. In the asker's layout, docker-compose.yml sits next to web_client/, so ./web_client/public is correct only when Compose is run with that file and the Docker daemon can see that folder. On Docker Desktop, keep the project on a drive that Docker Desktop shares. In WSL 2, keep it inside the Linux file system and run Compose from there.

When you do not need live editing of public/, remove that line. In the reproduction, mounting the real public/ folder instead of the empty one was the only change, and the check passed:

text
index.html
required files found

For development with live reload, mounting src/ is usually enough. Files in public/ rarely change while you work.

3. For production builds: COPY everything before npm run build#

The top answer (score 11) fixes a different variant: an image whose Dockerfile runs npm run build before the project files are copied in, so public/ does not exist yet in that stage. The order that works copies package.json, installs, copies the rest, then builds:

dockerfile
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html

Create React App writes the production output to build/. Serve that folder, not public/. The top answer copies /app/public into nginx, which serves the unbuilt template instead of the compiled app. Do not bind-mount the project into this image at run time either. The image already contains everything it needs.

Add node_modules to .dockerignore so COPY . . does not overwrite the modules installed in the image with the host's. The node-sass binding error shows what goes wrong when a host node_modules lands in a container.

Verify the fix#

bash
docker compose up -d --build web
docker compose exec web ls /usr/src/app/web_client/public
docker compose logs web

The ls should list index.html (and favicon.ico, manifest.json in a default project), and the logs should show the file check passing and "Starting the development server". For the production image, docker run --rm -p 8080:80 <image> followed by a visit to localhost:8080 should load the app rather than a raw template.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

When the error names src/index.js instead#

The same check guards src/index.js, so Name: index.js with Searched in: …/src is the same problem on the other mount. The asker's file mounts ./web_client/src too, so a wrong path there hides the entry file in exactly the same way. The commands in step 1 work for it unchanged.

Once the files are found, an old Create React App project on a newer Node image often stops at the next step with error:0308010C:digital envelope routines::unsupported. That is webpack 4's MD4 hash meeting OpenSSL 3, and the 0308010C fix covers the upgrade and the temporary workaround. If the image then fails because a base image's entrypoint swallows your npm start, see overriding ENTRYPOINT from a base image.

The rule that resolves the paradox is short: docker run shows you the image, and Compose shows you the image plus every volume on top of it. When a file is in one and not the other, look at the volumes first.

Further reading: Fix: React hot reload doesn't work in a Docker container.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote