What a Dockerfile is and what it does
A Dockerfile is a text file that contains instructions for Docker to build an image — a blueprint that Docker then uses to create and run containers. You write the instructions in plain language, Docker reads them in order from top to bottom, and each instruction creates a layer in the final image. When someone runs that image, Docker spins up a container with everything inside it: the operating system, your application code, libraries, configuration files, and whatever else you specified.
The file itself is just text. You create it in your project folder, name it Dockerfile (no extension), and Docker knows how to read it. The real work happens when you run the docker build command — that's when Docker executes each line and produces the image you can then share, run, or push to a registry.
Key Takeaways
- A Dockerfile is a text file named Dockerfile that lists instructions Docker uses to build an image, one layer at a time.
- Every Dockerfile starts with a FROM instruction that specifies the base image — usually a Linux distribution or a pre-built image with a language runtime already installed.
- Common instructions include RUN to execute commands, COPY to add files from your computer into the image, WORKDIR to set the working directory, and CMD to specify what runs when the container starts.
- You build the image by running docker build -t imagename . in the folder where your Dockerfile lives, and Docker creates a reusable image you can run or share.
- Keeping instructions organized and using a .dockerignore file to exclude unnecessary files makes images smaller and builds faster.
The basic structure: FROM, RUN, COPY, and CMD
Every Dockerfile begins with FROM, which tells Docker what base image to start with. This is usually something like FROM ubuntu:22.04 for a Linux base, or FROM python:3.11 if you want Python already installed. The base image is the foundation — everything else you add builds on top of it.
After FROM, you typically use RUN to execute commands inside the image. For example, RUN apt-get update && apt-get install -y curl updates the package manager and installs curl. Each RUN instruction creates a new layer, so combining multiple commands with && keeps the image smaller than running them separately.
COPY brings files from your computer into the image. COPY . /app copies everything from your current folder into the /app folder inside the image. WORKDIR /app sets that folder as the working directory for all commands that follow, so you don't have to type the full path every time.
CMD specifies what command runs when someone starts a container from your image. CMD ["python", "app.py"] means Docker will run python app.py by default. If the container is started with a different command, that overrides CMD, but CMD provides the sensible default.
A complete example: containerizing a Python application
Here is a working Dockerfile for a simple Python application:
FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["python", "app.py"]
This Dockerfile starts with Python 3.11 (the -slim variant is smaller), sets the working directory to /app, copies your requirements.txt file in, installs the dependencies, copies the rest of your code, and then runs python app.py when the container starts. The order matters: dependencies are installed before the code is copied, so if you change your code but not your dependencies, Docker can reuse the dependency layer instead of reinstalling everything.
To build this image, save the Dockerfile in your project folder alongside your Python files, then run docker build -t my-python-app . in that folder. Docker reads the Dockerfile, executes each instruction, and creates an image named my-python-app. You can then run it with docker run my-python-app, and the container will start and execute your Python application.
Choosing a base image that fits your needs
The base image you choose in the FROM line determines what's already installed and how large the final image will be. ubuntu:22.04 is a full Linux distribution with many tools, but it's large. python:3.11-slim includes Python but strips out unnecessary packages, making it smaller. python:3.11-alpine is even smaller because it's based on Alpine Linux, though some packages may not install as smoothly on Alpine.
For most applications, start with a language-specific image like python:3.11, node:18, or golang:1.20 rather than a bare Linux distribution. These images come with the runtime and package manager already set up, so you don't waste time installing them yourself. If you need something very specific, you can always start with a smaller base and add what you need with RUN commands.
Check Docker Hub for the official images in your language. The documentation there lists available tags (versions), explains what's included in each variant, and shows example Dockerfiles. Using an official image means security updates are maintained by the language maintainers, not by you.
Reducing image size and build time
Docker images can grow quickly if you're not careful. Every RUN instruction creates a layer, and even if you delete files in a later layer, the earlier layers still take up space. Combine multiple RUN commands into one using &&: instead of three separate RUN lines to update, install, and clean up, write RUN apt-get update && apt-get install -y curl && apt-get clean as a single instruction.
Create a .dockerignore file in your project folder to tell Docker which files to skip when copying. List one pattern per line, just like .gitignore:
.git .gitignore node_modules __pycache__ .env *.log
This prevents Docker from copying version control files, dependency folders, cache, and logs into the image, which shrinks the image and speeds up the build. It also keeps secrets out of the image if you accidentally list them in .env.
Order your instructions so that things that change frequently come last. If your application code changes every time you build but your dependencies rarely change, copy and install dependencies first, then copy your code. Docker caches layers, so it can skip the dependency installation on the next build and only rebuild the code layer.
Building and testing your image locally
Once your Dockerfile is written, build it by running docker build -t imagename . in the folder containing the Dockerfile. The -t flag tags the image with a name; the dot at the end tells Docker to look for the Dockerfile in the current directory. Docker will print output as it executes each instruction, showing you which layer is being built and whether any errors occur.
If the build fails, Docker stops and shows you the error. Common issues include a missing file (check your COPY paths), a command that doesn't exist in the base image (verify the base image has what you need), or a syntax error in the Dockerfile (check spacing and capitalization). Fix the Dockerfile and run the build command again.
Once the build succeeds, test the image by running docker run imagename. This starts a container from your image and runs the CMD instruction. If your application starts correctly and behaves as expected, the Dockerfile is working. If something goes wrong, you can run docker run -it imagename /bin/bash to start a shell inside the container and explore what's actually in there.
Common instructions beyond the basics
ENV sets environment variables inside the image. ENV DATABASE_URL=postgres://localhost makes that variable available to your application. EXPOSE documents which ports your application listens on — EXPOSE 8080 tells anyone reading the Dockerfile that the app uses port 8080, though it doesn't actually open the port (you do that with docker run -p).
USER specifies which user runs the commands. By default, everything runs as root, which is a security risk. RUN useradd -m appuser creates a user, and USER appuser switches to it, so your application runs with limited permissions. HEALTHCHECK tells Docker how to test whether your application is still running — HEALTHCHECK CMD curl localhost:8080 makes Docker periodically check if the app is responding.
ENTRYPOINT is similar to CMD but harder to override. Use ENTRYPOINT when you want to ensure a certain command always runs, and CMD for default arguments. For example, ENTRYPOINT ["python"] and CMD ["app.py"] means docker run myimage runs python app.py, but docker run myimage other.py runs python other.py instead.
Frequently Asked Questions
Do I have to name the file exactly "Dockerfile"?
Yes, Docker looks for a file named Dockerfile by default. If you want to use a different name, you can specify it with the -f flag: docker build -f Dockerfile.prod -t imagename . This is useful if you have multiple Dockerfiles for different purposes, but for most projects, stick with the standard name.
What's the difference between COPY and ADD?
COPY copies files from your computer into the image. ADD does the same thing but also handles URLs and automatically extracts tar archives. For most cases, use COPY because it's simpler and more predictable. Use ADD only when you specifically need to fetch a file from the internet or extract an archive.
Can I run a Dockerfile without Docker installed?
No, you need Docker installed and running to build and run Dockerfiles. Docker Desktop is available for Windows and Mac; on Linux, install the Docker Engine from your package manager. Once installed, the docker command becomes available in your terminal.
How do I push my image to Docker Hub so others can use it?
First, create an account on Docker Hub. Tag your image with your username: docker tag my-python-app username/my-python-app. Log in with docker login, then push with docker push username/my-python-app. Anyone can then pull and run your image with docker run username/my-python-app.
Why is my image so large?
Large images usually come from including unnecessary files, not combining RUN commands, or using a base image that's bigger than needed. Check what's in the image with docker run -it imagename /bin/bash, use a .dockerignore file to exclude files you don't need, combine RUN commands with &&, and consider switching to a smaller base image like -slim or -alpine variants.