Your AI Wrote a Dockerfile — Check These Nine Things Before You Push
Ask Claude, Cursor, or ChatGPT for a Dockerfile and you'll get one that is syntactically fine, follows the shape of a thousand tutorials, and fails in a handful of predictable ways when it meets a real platform. That's not the model being careless — it's writing for a generic Docker host it can't see, without knowing where your build runs, how your port gets exposed, or how your start command is applied. Three of the common problems stop the build outright, three let it deploy and then quietly not work, and the rest just cost you time and money. Here's the whole list, in the order you'll hit it.
Three that stop the build
1. The file has to be called Dockerfile, at the root. The build looks for exactly that name in the root of what you push — not Dockerfile.prod, not docker/Dockerfile, not backend/Dockerfile. AI tools like tidy directory structures and will happily put it in a subfolder, and a monorepo layout makes this near-certain.
If your app lives in a subdirectory, either push that subdirectory as its own repo, or keep a root Dockerfile that copies from the subfolder.
2. ARG values aren't supplied at build time. Generated Dockerfiles often open with something like:
ARG NODE_VERSION
FROM node:${NODE_VERSION}
There's no mechanism to pass build arguments here, so an ARG without a default resolves to empty and the build fails on a nonsense base image. Give every ARG a default value, or hardcode it:
ARG NODE_VERSION=22
FROM node:${NODE_VERSION}-slim
The same applies to ARGs used later for feature flags — bake the value in, or move the decision to a runtime environment variable.
3. The build has to fit in a small container. Builds run with roughly 0.75 GB of RAM and half a vCPU — less than your app gets. A Dockerfile that runs a Vite/webpack bundle, compiles native extensions, or invokes Gradle can exceed that, and the failure is usually a build that stops mid-step without a clear error rather than an explicit out-of-memory message.
Note that multi-stage builds don't help with this — they shrink the final image, but the compile still happens. More on the build limit, including capping Node's heap and moving heavy builds to CI.
Three that deploy fine and then don't work
4. EXPOSE doesn't publish anything. This is the single most common misunderstanding, and models reinforce it because every tutorial includes the line. EXPOSE 3000 is documentation — metadata on the image. It does not make your app reachable.
Your app becomes reachable when you add a port binding in the panel, choosing the container port and HTTPS. Leave EXPOSE in if you like; just don't expect it to do anything.
5. The start command binds to localhost, or runs a dev server. Two failures with one cause — the model wrote what works on a laptop:
# both of these will deploy and then be unreachable or fragile
CMD ["npm", "run", "dev"]
CMD ["flask", "run"]
flask run and manage.py runserver default to 127.0.0.1, which is unreachable from outside the container even with a correct port binding. npm run dev starts a development server with hot reload, file watchers, and debug output you don't want in production.
Use a production server bound to all interfaces:
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
Because there's no health check probing your app, a container listening on the wrong interface deploys "successfully" and simply never answers.
6. ENTRYPOINT and your run command stack up. Worth knowing precisely, because the symptom is baffling: the run command you set on the app replaces the image's CMD, not its ENTRYPOINT. If the generated Dockerfile ends with
ENTRYPOINT ["python"]
CMD ["app.py"]
and you also type python app.py as the app's run command, the container executes python python app.py. It fails instantly with an error that looks like your code is broken.
Pick one. Either put the full command in the Dockerfile and leave the app's run command empty, or use a plain CMD and set the command in the panel.
Two that are quietly wrong
7. Secrets baked into the image. If you told the model about your API key, there's a decent chance it wrote it in:
ENV OPENAI_API_KEY=sk-...
ENV values are stored in image layers and readable by anyone who can pull the image, without running it. Secrets belong to the deployment — set them as environment variables on the app instead. If one has already been committed or built in, rotate it rather than editing it out.
8. HEALTHCHECK does nothing useful here. Models add it because it looks professional. Docker will run the check and can mark your container unhealthy, but nothing on this platform acts on that status — restarts are triggered by your process exiting, not by a failed probe. It's harmless, and it's also not the monitoring you think you have. If you want liveness detection, build it yourself.
Two that just cost you
9. A full base image and no .dockerignore. Models default to FROM python:3.12 or FROM node:22 — the full images, hundreds of megabytes larger than the -slim variants for no benefit in most apps. And they rarely generate a .dockerignore, so COPY . . sweeps in .git, node_modules, .venv, and — dangerously — a local .env.
Add one before your first build:
.git
.env
node_modules
__pycache__
.venv
While you're there, check the layer order. Models frequently write COPY . . before installing dependencies, which busts the cache on every code change and makes every deploy slower than it needs to be. Dependencies first, source second. Why image size matters here — it's deploy time, not storage.
A baseline that works
Nothing clever, just the corrections applied:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
Root-level Dockerfile, no ARG, dependencies cached before source, binds all interfaces, production server, no secrets, no ENTRYPOINT to collide with a run command. Push it with git push and the platform builds it — which also means it's built on the right architecture, unlike an image built on an Apple Silicon Mac.
FAQ
Why isn't my Dockerfile being found?
It must be named exactly Dockerfile and sit at the root of what you push. Subdirectory paths and suffixed names aren't detected.
Why does my build fail on a ${VARIABLE} in FROM?
Build arguments aren't passed, so an ARG with no default is empty. Give it a default or hardcode the value.
Does EXPOSE make my app public?
No. It's image metadata. Add a port binding on the app to get a URL.
Why does my container fail immediately with a weird command error?
Probably ENTRYPOINT in the image plus a run command on the app. They combine. Use one or the other.
Should I keep the HEALTHCHECK my AI added?
It's harmless but inert — nothing here reacts to health status. Add a watchdog in your app if you want liveness detection.
Do I even need a Dockerfile?
Not always. Uploading a zip lets the build agent work out the runtime for you. A Dockerfile gives you control and is required for git-push deploys.
Wrapping up
An LLM writes a good generic Dockerfile and a mediocre specific one, because the specifics — where the build runs, how ports are published, how the start command is applied — aren't in the training data for your platform. The fixes are all small, and eight of the nine are things you change once and never think about again. Run through the list before your first push and you'll skip the confusing half-hour where a successful build produces an app that won't answer.