Dockerfiles and dockhold.json
What these files are, whether you need them, and exactly what to put in them.
To put your app online, Dockhold turns your code into an image: a self-contained package with your code and everything it needs to run. This page explains the two files that control how that happens, starting from scratch. If you have never written either one, you are in the right place.
Do I need a Dockerfile?
It depends on your plan.
- Free accounts: yes. Your repository needs a
Dockerfileat its root. If there is none, the deploy stops immediately and tells you so, before anything is built. - With compute added ($5/month or more): no. Dockhold reads your project, works out the language and framework, and builds it for you. You can still add a
Dockerfileto take over whenever you want.
Either way, a Dockerfile you commit always wins. Dockhold never overrides one, on any plan. Writing one is also a good idea once your app is real, because it pins exactly how your app is built instead of leaving it to automatic detection.
What is a Dockerfile?
A Dockerfile is a plain text file, with no extension, that lists the steps to package your app. Think of it as the instructions you would give a new teammate setting your project up on a fresh laptop: start from this language, copy these files, install the dependencies, run this command to start it.
It is a widely used standard, not something specific to us. ADockerfile that works elsewhere works here.
Every line is a step. The common ones:
FROM: the starting point, usually a language runtime likenode:22-alpineorpython:3.12-slim.WORKDIR: the folder inside the image to work in.COPY: copy files from your repo into the image.RUN: run a command while building, such as installing dependencies.EXPOSE: the port your app listens on.CMD: the command that starts your app.
Where does it go?
In the root of your repository, next to yourpackage.json or requirements.txt. The file is named exactly Dockerfile: capital D, no .txt, no extension at all.
your-repo/
├── Dockerfile ← here
├── package.json
├── src/
└── README.mdCommit it and push. That push is your deploy.
Write your first one
Copy the one that matches your project, save it as Dockerfile in your repo root, and adjust the port if your app uses a different one.
Node.js
FROM node:22-alpine
WORKDIR /app
# Install dependencies first so this step is cached
# and only re-runs when your dependencies change.
COPY package*.json ./
RUN npm ci
# Now copy the rest of your code.
COPY . .
# If your project has a build step, keep this line.
# If it does not, delete it.
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]Python
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "main.py"]Go
FROM golang:1.24-alpine AS build
WORKDIR /src
COPY . .
RUN go build -o /app/server .
# Start from a clean, tiny image and copy only the
# built binary in. This keeps the final image small.
FROM alpine:3.20
COPY --from=build /app/server /server
EXPOSE 8080
CMD ["/server"]Listen on the right port. Whatever port your app listens on, tell Dockhold the same number when you deploy. Bind to0.0.0.0, not localhost. An app listening only onlocalhost inside its own image cannot be reached from outside, so it will look like it started but never respond.
Keep the image small. Free accounts have a 300 MB limit on the finished image. Start from a -alpine or -slimbase, and add a .dockerignore file listing things that should never be copied in:
node_modules
.git
.env
dist
__pycache__What is dockhold.json?
dockhold.json is an optional file that tells Dockhold how to build your app. Most projects never need it. Add one only when the default is wrong for you.
Like the Dockerfile, it goes in the root of your repository, and it must be valid JSON.
Pointing at a Dockerfile somewhere else
This is what almost everyone uses it for. By default Dockhold looks for aDockerfile in the repo root. If yours lives in a subfolder, or has a different name, say so:
{
"build": {
"dockerfile": "docker/Dockerfile.production"
}
}The path is relative to the repo root. So a repo laid out like this:
your-repo/
├── dockhold.json
├── docker/
│ └── Dockerfile.production ← the path above points here
└── src/The path has to exist. If dockhold.json points at a file that is not there, the deploy stops and tells you which path it could not find. It does not quietly fall back to something else, because silently building the wrong thing is worse than a clear failure.
Which one does Dockhold use?
Every build follows the same order and takes the first match:
- A
Dockerfilein your repo root. Used as-is. - The path in
dockhold.jsonunderbuild.dockerfile. - Automatic detection. Dockhold reads your project and builds it for you. Available once you have added compute.
So a Dockerfile in the root wins over dockhold.json. If you are using dockhold.json to point somewhere else, make sure there is no Dockerfile sitting in the root as well.
Check it before you push
If you have Docker installed locally, you can build the same image on your own machine first. It is the fastest way to find a mistake:
docker build -t my-app .
docker run -p 3000:3000 my-appOpen http://localhost:3000. If it works there, it will work here. No Docker locally? Push and watch the build log in your dashboard instead. It shows each step as it runs and stops at the line that failed.
Common problems
- "No Dockerfile found in your repository." Either add one (copy a template above), oradd compute and Dockhold will detect your stack and build it for you.
- The file is named wrong. It must be
Dockerfile, notdockerfile,Dockerfile.txt, orDockerFile. Some editors add.txtwithout showing it. - It is not committed. Check that the file is actually pushed to GitHub, and that a
.gitignoreis not excluding it. - The build works locally but not here. Usually something was copied in that should not have been. Add a
.dockerignoreso localnode_modulesand.envfiles never reach the build. - The image is too large. Free accounts cap the finished image at 300 MB. Use an
-alpineor-slimbase image, and for compiled languages copy only the built binary into a clean final image, as in the Go example above.
Next: how deploys work, or set upenvironment variables and secrets.