Writing Your First Dockerfile in Docker

Writing Your First Dockerfile in Docker

A beginner-friendly, ground-up walkthrough of what a Dockerfile actually is, why it exists, and how each instruction inside one contributes to building a working container image — explained for someone who has never written one before.

Every container that has ever run, from a tiny script to a massive production web service, started life as a set of plain-text instructions describing how to build it. That set of instructions is called a Dockerfile, and writing your first one is the moment Docker stops being something you only run and starts being something you actually build with. It can feel intimidating at first — a small text file with a strange, capitalised vocabulary of its own — but underneath, a Dockerfile is really just a recipe: a precise, repeatable list of steps that always produces the exact same result, no matter who runs it or where. This guide walks through what a Dockerfile is, how Docker reads and executes it, and how to think about writing your very first one, one concept at a time.

ACore Concepts

Before writing a single instruction, it helps to understand exactly what problem a Dockerfile is solving.

A Dockerfile is a plain text file containing a series of instructions that tell Docker how to assemble a container image. A container image, in turn, is a self-contained, ready-to-run package holding everything an application needs: the application’s own code, the specific version of any runtime it depends on, any libraries it uses, and even the exact configuration it expects to find. When you eventually run that image, Docker uses it as the blueprint for creating a running container, the live, isolated environment discussed in earlier guides on container networking and on docker exec.

The relationship between these three things, a Dockerfile, an image, and a container, is one of the first things worth fixing firmly in mind, because beginners often blur them together. The Dockerfile is the recipe you write. The image is the sealed, finished dish that recipe produces once it is built. The container is a single serving of that dish, currently being eaten — a live, running instance created from the image. You write one Dockerfile, build one image from it, and from that same image you can start as many running containers as you like, each one starting from an identical copy of exactly what the Dockerfile described.

Everyday Analogy

Think of a Dockerfile as a recipe card for baking a specific cake. The recipe itself is not a cake — it is a set of written steps. Following that recipe once produces an actual cake, which is like building an image. And every time a slice of that cake is served to someone, that is like starting a new running container from the same image — each slice identical, each one traceable back to the exact same recipe, no matter who baked it or in which kitchen.

The single biggest reason Dockerfiles matter is repeatability. Before this kind of approach became common, “it works on my machine but not on yours” was an extremely common and extremely frustrating problem, usually caused by two computers having subtly different versions of some underlying tool installed. A Dockerfile eliminates this entire category of problem by writing the setup steps down precisely, once, so that building an image from it produces the exact same result whether it happens on a developer’s laptop, a teammate’s machine, or a server in a data centre on the other side of the world.

i
Key Idea

A Dockerfile does not run your application directly. It describes how to build an image, which is later used to start containers. Writing a Dockerfile is a one-time authoring step; building an image from it, and running containers from that image, are separate steps that happen afterward, often repeated many times.

It is worth pausing on why this problem needed solving at all. Before an approach like this became standard, setting up a new machine to run a piece of software usually meant following a written list of manual steps, or worse, relying on someone’s memory of “the way we usually do it.” Every one of those manual steps was an opportunity for a tiny difference to creep in — a slightly different version of some tool, a step performed in a different order, a step quietly skipped by accident. None of these differences would necessarily be obvious right away, but any one of them could eventually cause an application to behave subtly differently on one machine than on another, for reasons that were maddening to track down after the fact.

A Dockerfile turns that entire fragile, manual process into something Docker itself carries out mechanically, exactly the same way every single time. Nothing is left to memory, nothing depends on who happens to be setting things up that day, and nothing depends on what was already installed on the machine beforehand. This is also why Dockerfiles are almost always stored and tracked alongside an application’s own source code — the setup instructions are treated as being just as important, and just as worth preserving carefully, as the application logic itself.

BArchitecture & Components

A Dockerfile is built from a small vocabulary of instructions, each one responsible for one specific kind of step in assembling an image.

Every instruction in a Dockerfile begins with an all-capitals keyword, followed by whatever that instruction needs to do its job. You do not need to memorise dozens of these to write a useful first Dockerfile — a small handful cover the overwhelming majority of everyday needs, and understanding what each one is responsible for is the real foundation everything else builds on.

Starting Point

FROM

Chooses the base image everything else will be built on top of, such as a particular operating system or a particular programming language runtime already packaged and ready to use.

Copying Files

COPY

Copies files or folders from your own project, on your machine, into the image being built, so your application’s code actually ends up inside it.

Build-Time Setup

RUN

Executes a command while the image is being built, such as installing a dependency, and permanently bakes the result of that command into the image.

Start-Up Behaviour

CMD

Specifies the default command that should run automatically whenever a container is started from this image, unless something else is explicitly requested instead.

A few additional instructions round out what a beginner-level Dockerfile typically needs. An instruction for setting the working directory establishes which folder, inside the image, subsequent instructions like copying files or running commands should treat as their starting point, in much the same way that opening a specific folder on your own computer before running a command changes where that command actually operates. An instruction for declaring which network port the application expects to be reached on documents that expectation directly inside the image, making it clear to anyone reading the Dockerfile, or anyone later configuring how the container is exposed, exactly which port matters. An instruction for setting environment variables lets you bake in configuration values, such as a mode flag or a default setting, that the application can read once it is running.

graph TD
  A["FROM
Pick a base image"] --> B["WORKDIR
Set the working folder"] B --> C["COPY
Bring in your project files"] C --> D["RUN
Install dependencies"] D --> E["EXPOSE
Document the port"] E --> F["CMD
Define the startup command"]

Fig 1 — A typical, beginner-level ordering of Dockerfile instructions, from choosing a starting point through to defining how the container starts.

Why the Order of Instructions Matters

Docker reads a Dockerfile from top to bottom and carries out each instruction in the order it appears. This is not just a stylistic preference — it directly affects both correctness, since later instructions often depend on something an earlier instruction already set up, and performance, a topic covered in more depth in the Internal Working section below.

Two more instructions are worth knowing about even in a first Dockerfile, because they come up often once you look at real-world examples. One lets you define named build-time arguments, values that can be supplied from outside the Dockerfile at the moment an image is built, useful for small variations between builds without needing to maintain several nearly-identical Dockerfiles. Another lets you switch which user account subsequent instructions, and eventually the running container itself, operate as, which matters for security, since running an application as a highly privileged account inside a container when it does not need that level of access is a habit worth avoiding from the very first Dockerfile you write.

None of these instructions need to be memorised all at once. A genuinely useful first Dockerfile can be written using only four or five of them, and the rest can be learned gradually, exactly when a real, concrete need for them shows up in a project. Trying to learn every available instruction before writing anything at all is, for most beginners, a much slower path than simply writing a small, working Dockerfile first and expanding it as new needs arise.

CInternal Working

Understanding what actually happens when an image is built from a Dockerfile explains several behaviours that otherwise seem mysterious.

When Docker builds an image from a Dockerfile, it does not treat the whole file as one single, indivisible operation. Instead, it processes the instructions one at a time, and after carrying out each one, it saves the result as a distinct, numbered layer. A layer is essentially a snapshot of the filesystem at that exact point in the build, capturing only what changed since the previous layer, not the entire filesystem all over again. The finished image is really just an ordered stack of these layers, placed on top of one another, with the very top layer representing the final, fully assembled result.

This layered approach is the reason Docker builds can be remarkably fast after the very first time. Docker keeps a cache of every layer it has already built, and before recreating a layer from scratch, it checks whether the instruction that would produce it, along with everything that instruction depends on, is identical to something already cached. If nothing has changed, Docker reuses the existing cached layer instantly instead of redoing the work. This is exactly why the instruction that changes most often in a typical project, usually the step that copies in your actual application code, is conventionally placed near the end of a Dockerfile, and why steps that rarely change, such as installing a fixed set of dependencies, are placed earlier: it lets Docker reuse the slow, rarely-changing steps from cache and only redo the fast, frequently-changing step of bringing in your latest code.

1

Docker Reads the Dockerfile

Every instruction is parsed in the order it appears, from top to bottom.

2

Each Instruction Becomes a Layer

Docker checks its cache first; if the instruction and its inputs are unchanged, the existing layer is reused instead of rebuilt.

3

Layers Stack Into a Final Image

Once every instruction has been processed, the resulting stack of layers is sealed together as one finished, ready-to-use image.

4

Image Is Ready to Run

Docker can now start any number of containers from this image, each one beginning from an identical copy of that same layered result.

!
Worth Noting

Because each layer only captures what changed, a Dockerfile that installs a large dependency early and copies a small amount of frequently-changing code later will typically rebuild much faster on every subsequent change than a Dockerfile written the other way around, since the expensive early layer keeps being reused from cache.

It is also worth understanding what the base image, chosen right at the start of a Dockerfile, actually contributes. A base image is itself the result of someone else’s Dockerfile, already built and published for reuse. Choosing a base image that already contains a particular operating system, or already contains a particular language runtime pre-installed, means your own Dockerfile does not need to rebuild any of that groundwork from raw components — it simply starts from that already-assembled foundation and adds only what is specific to your own application on top of it.

This same layering idea can be extended deliberately, through an approach commonly called a multi-stage build, where a Dockerfile actually describes more than one starting point, one after another. An earlier stage might use a heavier base image containing every tool needed to prepare or compile an application, while a later, final stage starts fresh from a much leaner base image and copies across only the finished result from the earlier stage, leaving all of the heavier preparation tools behind entirely. The final image ends up containing only what is genuinely needed to run the application, even though building it required a much larger set of tools along the way. This is not something a first Dockerfile typically needs, but understanding that the concept exists helps make sense of more advanced Dockerfiles encountered later.

DData Flow & Lifecycle

Following one project from an empty folder to a running container shows how every piece fits together in practice.

Imagine you have written a small application on your own computer, along with a Dockerfile describing how to package it. The build process begins the moment you tell Docker to build an image using that Dockerfile, pointing it at the folder containing both your project files and the Dockerfile itself.

sequenceDiagram
  participant You as You
  participant Docker as Docker Build Engine
  participant Files as Your Project Files
  participant Image as Finished Image
  You->>Docker: Build using this Dockerfile
  Docker->>Docker: Pull or reuse the base image (FROM)
  Docker->>Files: Copy application files in (COPY)
  Docker->>Docker: Install dependencies (RUN)
  Docker->>Docker: Record startup command (CMD)
  Docker->>Image: Seal all layers into one image
  Image-->>You: Ready to run as a container
        

Fig 2 — The build process turning a Dockerfile and a folder of project files into one finished, runnable image.

Docker starts by resolving the base image named in the first instruction, either reusing a copy it already has stored locally or fetching it if this is the first time that particular base image has been used on this machine. From there, it works through the rest of the Dockerfile in order: setting the working directory, copying in your project’s files, running any setup commands such as installing dependencies, documenting the expected network port, and finally recording the default startup command. Each of these steps produces its own layer, as described in the previous section, and once every instruction has been processed, Docker seals the complete stack of layers together as one finished image, ready to be used.

From that point forward, the Dockerfile itself has done its job. Starting a container from the finished image does not re-read the Dockerfile at all; it simply takes the already-built image and creates a new, live, running environment from it, exactly as described in earlier guides on how containers work. If you later change your application’s code and want an updated container, the cycle repeats: you rebuild the image from the same Dockerfile, Docker reuses whatever cached layers it still can, rebuilds only what actually changed, and produces a new, updated image ready to replace the old one.

Everyday Analogy

This is similar to a factory assembly line building a finished product from a written specification sheet. The specification sheet, the Dockerfile, is consulted once, at build time, to assemble the product correctly. Once the product, the image, rolls off the line, customers using that product, containers being started from it, never need to see the specification sheet again — they simply use the finished item exactly as it was assembled.

It is worth also tracing what happens to a built image once it needs to travel somewhere else, such as onto a server that will actually run it in production. An image, once built, can be given a memorable, versioned name and pushed to a shared registry, a storage location purpose-built for holding images so other machines can retrieve them. From there, any other machine that needs to run the application simply pulls that already-built image down and starts a container from it directly, without needing the original Dockerfile, the original project files, or any of the tools that were needed to build the image in the first place. This is what makes the image, rather than the Dockerfile itself, the actual unit that moves through the rest of an application’s journey from a developer’s machine into production.

EAdvantages, Disadvantages & Trade-offs

Writing your setup as a Dockerfile instead of relying on manual, one-off setup steps brings real benefits, but it also asks you to think differently about how you build and maintain an application.

Advantages

  • Produces the exact same result every time it is built, eliminating “it works on my machine” style inconsistencies between environments.
  • Documents the application’s entire setup process as plain, readable text, rather than as tribal knowledge someone has to remember or re-discover.
  • Speeds up rebuilds dramatically through layer caching once written thoughtfully, since unchanged steps are reused rather than repeated.
  • Makes an application portable across any machine capable of running Docker, without needing that machine’s setup to match exactly.

Disadvantages & Trade-offs

  • Adds a learning curve, since the small but specific vocabulary of instructions is unfamiliar the first time someone encounters it.
  • A poorly ordered Dockerfile can end up slower to rebuild than necessary, if frequently-changing steps are placed before rarely-changing ones.
  • Choosing an unnecessarily large or heavy base image can make the resulting image bigger and slower to distribute than it needs to be.
  • Every dependency and setup step has to be made explicit in the Dockerfile; nothing is inherited automatically from whatever happens to already be installed on the machine building it.
1
DOCKERFILE
PER APPLICATION IMAGE
4-6
INSTRUCTIONS TYPICALLY
COVER A FIRST DOCKERFILE
∞
CONTAINERS STARTABLE
FROM ONE BUILT IMAGE

FSecurity, Monitoring & Observability

What you choose to include, and exclude, in a Dockerfile has a direct and lasting effect on how secure and how easy to monitor the resulting image will be.

Every file and every installed dependency named in a Dockerfile becomes a permanent part of the resulting image, and therefore a permanent part of every container ever started from it. This means the Dockerfile is effectively the first and most important place to think about security: including only what the application genuinely needs, and nothing more, keeps the image smaller and reduces the number of things inside it that could ever be affected by a future vulnerability discovered in some unrelated, unnecessary piece of software.

!
Security Reminder

It is a common and serious mistake to copy an entire project folder into an image without excluding sensitive files first, such as local configuration files containing credentials or private keys used only for a developer’s own machine. Anything copied into the image becomes part of it permanently, visible to anyone who can inspect or run a container from that image.

Choosing a base image also carries a security dimension. A base image that is actively maintained and kept up to date receives security fixes over time, while an old, no-longer-maintained base image can quietly become the weakest link in an otherwise carefully built application, simply because nothing was updating the foundation it was built on. Many teams make it a habit to periodically rebuild their images against a freshly updated base image for exactly this reason, even when nothing about their own application code has changed.

On the observability side, a well-written Dockerfile makes an image easier to reason about later, which pays off directly during monitoring and debugging. Setting a clear working directory, documenting the expected port, and keeping the startup command simple and explicit all make it far easier for someone using tools like exec, described in an earlier guide, to inspect a running container and immediately understand what they are looking at, rather than having to reverse-engineer an unfamiliar, undocumented setup from scratch.

Many teams also scan every image, immediately after it is built, against a known list of software vulnerabilities, automatically flagging anything the Dockerfile pulled in that turns out to have a known weakness. Because this scan happens against the finished image, and the finished image is entirely determined by what the Dockerfile described, a disciplined, minimal Dockerfile directly translates into fewer findings for that scan to raise in the first place. Teams that write sprawling, over-inclusive Dockerfiles, by contrast, tend to spend far more ongoing effort reviewing and patching vulnerabilities that trace back to something the application never actually needed to begin with.

GDesign Patterns & Anti-patterns

A handful of recurring habits separate a Dockerfile that stays easy to maintain from one that quietly becomes a source of slow builds and confusing bugs.

Pattern

Order by Change Frequency

Placing rarely-changing steps, such as installing dependencies, before frequently-changing steps, such as copying application code, so Docker’s layer cache does the most work on your behalf.

Pattern

One Clear Purpose Per Image

Keeping each Dockerfile focused on building one specific application or service, rather than trying to bundle several unrelated purposes into a single, sprawling image.

Anti-pattern

Copying Everything Blindly

Copying an entire project folder into the image without excluding files that were never meant to leave the developer’s own machine, such as local secrets or temporary build artifacts.

Anti-pattern

Installing Things “Just in Case”

Adding extra tools or packages to an image on the assumption they might be useful someday, quietly growing the image’s size and its exposure to future vulnerabilities without any real, current need.

PATTERN NOTE · 001 Avoid
Anti-Pattern

Writing every setup step as one giant, undifferentiated block, with dependency installation, code copying, and configuration all mixed together in whatever order they were first thought of.

Why It Causes Problems

Every small change to the application’s code invalidates the layer cache from that point forward, forcing Docker to redo dependency installation and every other step that follows, even though none of those steps actually needed to change. Over time, this turns what should be a fast, incremental rebuild into a slow, full rebuild every single time.

Better Approach

Deliberately separate the Dockerfile into stages that change at different rates: base image and dependency installation first, since these rarely change, followed by copying in the application’s own code last, since this changes constantly during everyday development.

HBest Practices & Common Mistakes

A short set of habits makes the difference between a first Dockerfile that works by accident and one that is genuinely well built from the start.

Start From the Smallest Base Image That Fits

Choosing a lean, purpose-built base image over a large, general-purpose one keeps the final image smaller, faster to distribute, and generally easier to reason about.

Be Explicit About What Gets Copied In

Rather than copying an entire project folder wholesale, copy only what the application genuinely needs, and deliberately exclude anything sensitive or irrelevant to how the application actually runs.

Keep the Startup Command Simple

The instruction defining how a container starts should do one clear thing — launch the application — rather than trying to chain together several unrelated setup steps at startup time that would be better handled earlier, during the build itself.

Rebuild and Test Before Trusting an Image

Building the image and actually starting a container from it to confirm it behaves as expected is a small step that catches a surprising number of mistakes before they reach anyone else.

Common MistakeWhy It HappensFix
Copying the whole project folder inFeels simpler than picking files individuallyCopy only what the application needs, excluding sensitive or irrelevant files
Installing dependencies after copying codeWritten in the order it was first thought ofInstall dependencies before copying frequently-changing code
Choosing an unnecessarily large base imageIt was the first one that came to mindPick the smallest base image that genuinely covers the application’s needs
Never rebuilding from an updated base imageThe application code has not changed, so it seems unnecessaryPeriodically rebuild against a freshly updated base image regardless

It also helps, even in a first Dockerfile, to leave a short comment near the top explaining what the image is for and any non-obvious choice made along the way. A Dockerfile is read far more often than it is written, and a brief explanation saves a future reader, quite possibly you yourself months later, from having to reconstruct the reasoning behind a decision from scratch.

IReal-World & Industry Examples

Every organisation that runs containerized software at scale relies on the same underlying discipline described in this guide, just applied across many Dockerfiles rather than one.

Streaming Platforms

Companies like Netflix maintain a huge number of small, independent services, each with its own Dockerfile describing exactly how that specific service is built. Because every one of those Dockerfiles follows the same disciplined structure, base image first, dependencies next, application code last, new services can be onboarded quickly by following an established, predictable pattern rather than inventing a new approach each time.

E-Commerce Platforms

Retail platforms often maintain a small set of carefully chosen, well-maintained base images that every team across the company is expected to build on top of, rather than letting each team pick its own starting point independently. This keeps security patching consistent and predictable across hundreds of different Dockerfiles, since updating one shared base image quietly benefits every image built from it.

Financial Technology Platforms

Payments and banking platforms typically enforce strict review processes specifically for Dockerfiles, since every instruction inside one determines exactly what ends up inside a production image. A Dockerfile that copies in an unnecessary file, or installs an unnecessary tool, can introduce risk that has nothing to do with the application’s actual business logic, which is why these files are treated as seriously as the application code itself.

A Dockerfile is not just a build script — it is the single, precise record of exactly what ends up inside every container built from it.

Ride-hailing and logistics platforms, which typically maintain hundreds of small services built by many different teams, often go a step further and provide a small set of officially supported, pre-written Dockerfile templates that teams are encouraged to start from rather than writing one entirely from scratch. This keeps newly onboarded engineers from having to relearn every best practice covered in this guide independently, and keeps the overall fleet of images consistent enough that operational tooling built around one service’s Dockerfile pattern tends to work smoothly against dozens of others as well.

JFrequently Asked Questions

Q1Does a Dockerfile run my application, or only build it?
A Dockerfile only describes how to build an image. It does not run anything on its own. The application actually starts running only when a container is created from the finished image, using the startup command the Dockerfile recorded.
Q2Do I need a different Dockerfile for every container I start?
No. One Dockerfile produces one image, and that single image can be used to start as many running containers as you like, each one an independent, identical copy of what the Dockerfile described.
Q3What happens if I change my Dockerfile after already building an image from it?
Nothing happens to the image that was already built; it remains exactly as it was. To get an image reflecting your changes, you need to rebuild it, at which point Docker reuses whatever unaffected layers it can from cache and rebuilds only the parts that actually changed.
Q4Why does the order of instructions in a Dockerfile matter so much?
Because Docker builds layers in order and caches each one, placing frequently-changing steps early forces Docker to rebuild everything after them on every single change. Placing rarely-changing steps early instead lets Docker reuse those layers from cache far more often, making everyday rebuilds noticeably faster.
Q5Can a Dockerfile start from another team’s already-built image instead of starting completely from scratch?
Yes, and this is in fact the normal, expected way to write one. The very first instruction in a Dockerfile chooses an existing base image to build on top of, which is itself the finished result of someone else’s Dockerfile, saving you from having to recreate common groundwork yourself.
Q6Is it safe to copy my entire project folder into the image to keep things simple?
Not automatically. Copying an entire folder can accidentally include files never meant to leave your own machine, such as local secrets or temporary files. It is safer, and considered a best practice, to be explicit about exactly which files and folders genuinely need to be part of the image.
Q7Does the machine I build an image on need to match the machine that eventually runs it?
No, and this is one of the main points of writing a Dockerfile in the first place. Once an image is built, it carries everything the application needs bundled inside it, so any machine capable of running Docker can run a container from that image, regardless of what that machine happened to have installed beforehand.
Q8What happens to a Dockerfile after the image has been built and shared elsewhere?
The Dockerfile itself stays wherever it was written, usually alongside the application’s own source code, and is not needed again until someone wants to rebuild the image, for instance after a code change. The built image, once created, is entirely self-contained and can be shared, run, and reused without needing the Dockerfile present at all.

KSummary and Key Takeaways

Key Takeaways

  • A Dockerfile is a recipe, not the finished dish. It describes how to build an image; the image, once built, is what actually gets used to start containers.
  • A small vocabulary of instructions covers most first Dockerfiles. Choosing a base image, copying in files, installing dependencies, and defining a startup command are the core building blocks.
  • Every instruction becomes a cached layer. Ordering rarely-changing steps before frequently-changing ones keeps everyday rebuilds fast.
  • One image can start unlimited containers. Building happens once; running containers from that same built image can happen as many times as needed.
  • Everything copied or installed becomes permanent. Be deliberate about what goes into the image, both for security and for keeping it lean.
  • Order and clarity matter as much as correctness. A thoughtfully structured Dockerfile is faster to rebuild and far easier for someone else, or future you, to understand.
  • Writing your first Dockerfile is a foundational skill. Every more advanced Docker topic builds on the same core idea: describe the setup precisely once, and let Docker reproduce it reliably, everywhere, every time.