A Dockerfile is a plain-text recipe that tells Docker how to assemble an image — the self-contained snapshot your container will run from. Every line is an instruction, read top to bottom, and the result is reproducible: anyone who builds the image gets the same environment. Containerizing an app also means the app no longer depends on whatever happens to be installed on the host machine — your laptop, your colleague's laptop, and the server all get exactly the same runtime.
This guide takes a tiny Python app and turns it into an image, building and running it along the way.
The project
You have a small web app. The pieces that matter are a dependency manifest and the source:
myapp/
├── app.py
└── requirements.txt
The manifest lists what runtime packages the app needs. The source is a short file, for example a small Flask app in app.py. Alongside them you'll create one more file, named exactly Dockerfile — capital D, no extension (watch out: some editors quietly append .txt, which breaks the build).
Write the Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd --create-home appuser
USER appuser
EXPOSE 5000
CMD ["python", "app.py"]
Walk through the instructions:
FROMpicks the base image your image is layered on top of.python:3.12-slimgives you Python minus the bulk, keeping the final image smaller. Every Dockerfile starts with aFROM.WORKDIRsets the working directory for every instruction after it, creating it if it doesn't exist. Commands and the runtime now start in/app.COPY requirements.txt .copies just the dependency manifest into the image.RUN pip install --no-cache-dir -r requirements.txtexecutes a command at build time, installing the dependencies, so they're baked into the image rather than installed each time a container starts.COPY . .copies the rest of your source code in.RUN useradd --create-home appuser+USER appusercreate a non-root user and switch to it for everything that follows. The base image runs as root by default, and you don't want your app running as root.EXPOSE 5000declares the port the app listens on. It's documentation for humans and tooling — it does not publish the port by itself.CMD ["python", "app.py"]is the command that runs every time a container starts from this image.
The ordering does a real job: Docker builds in layers and caches each one. Because you copy requirements.txt and install dependencies before copying the source, editing app.py rebuilds only the last layer — Docker reuses the cached dependency install. If you copied source first, any code change would reinstall everything on every build.
Build the image
From inside myapp/, run:
docker build -t myapp:v1 .
The . is the build context — it tells Docker the current directory holds the Dockerfile and the files to copy. It is required: docker build -t myapp without the dot fails.
Docker sends the whole directory to the daemon as the build context, so trim what shouldn't travel. Add a .dockerignore file in the project root:
.git
.venv
__pycache__
This keeps source-control history, virtual environments, and Python caches out of the image.
Run the container
docker run -d -p 5000:5000 --name myapp myapp:v1
-druns it detached, in the background.-p 5000:5000maps port 5000 on your machine to port 5000 inside the container — this is where the port actually gets published.--name myappgives the container a friendly name.
Check it and see it running:
docker ps
curl http://localhost:5000
Clean up when you're done with docker stop myapp and docker rm myapp.
What's next
You have a working image; the natural next step is sharing it. Push the tagged image to a registry and run the same docker run on a server — that's the whole promise of containers, an environment that travels. After that, look at docker compose to define the app plus its database and networking in one file.