DEV Community

Malleswari
Malleswari

Posted on

Docker for Beginners: What Actually Happens When You Dockerize an Application?

A few days ago, I started learning Docker.

Like many beginners, I started with the commands:

docker build
docker run
docker ps
docker images
Enter fullscreen mode Exit fullscreen mode

But after seeing a few commands, I realized something.

I could memorize them.

But I didn't really understand what was happening behind them.

What exactly is a Docker image?

What is a container?

Why do we need a Dockerfile?

What does docker run actually do?

And why does -p 3000:3000 suddenly make my application accessible?

So instead of learning Docker as a list of commands, I decided to follow one simple application and understand what happens at each step.

And that's the story I want to share here.


It Works on My Machine

Imagine you're working on a Node.js application.

You clone the project, install the dependencies, and start it:

npm install
npm start
Enter fullscreen mode Exit fullscreen mode

Everything works.

Your application needs:

Node.js
npm
Dependencies
Application code
Configuration
Enter fullscreen mode Exit fullscreen mode

Then someone else clones the same project.

They run:

npm install
npm start
Enter fullscreen mode Exit fullscreen mode

And suddenly...

It doesn't work.

Maybe they have a different Node.js version.

Maybe a dependency behaves differently.

Maybe some configuration is missing.

The code is the same.

But the environment is different.

That's where Docker starts becoming interesting.

Instead of asking every developer or server to manually recreate the environment, we can describe how our application should be packaged.


So, What Does Docker Actually Give Us?

At first, Docker looked like a collection of commands to me.

But the basic idea is actually simple:

Application
    +
Dependencies
    +
Required environment
        ↓
    Docker Image
        ↓
    Container
        ↓
Running Application
Enter fullscreen mode Exit fullscreen mode

And this introduced three words I needed to understand:

Dockerfile. Image. Container.

Let's build our way through them.


First: How Do We Tell Docker What We Need?

Our application needs Node.js.

It needs dependencies.

It needs our source code.

It needs to know what command to run.

How do we describe all of that?

We create a file called:

Dockerfile
Enter fullscreen mode Exit fullscreen mode

Think of the Dockerfile as a set of instructions for building our application's environment.

For our simple Node.js application:

FROM node:22

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000

CMD ["npm", "start"]
Enter fullscreen mode Exit fullscreen mode

At first, this looked like a lot of new syntax.

But once I understood what each line was doing, it became much easier.


Reading Our First Dockerfile

Let's go through it.

Start with Node.js

FROM node:22
Enter fullscreen mode Exit fullscreen mode

Our application needs Node.js.

Instead of installing Node.js manually, we start from an existing Node.js image.

So we're saying:

Give me an environment that already has Node.js 22.


Create a working directory

WORKDIR /app
Enter fullscreen mode Exit fullscreen mode

This sets /app as the working directory inside the image.

Our application will live there.


Copy the package files

COPY package*.json ./
Enter fullscreen mode Exit fullscreen mode

We copy our package files into the image.


Install dependencies

RUN npm install
Enter fullscreen mode Exit fullscreen mode

Docker runs npm install while building the image.

That's important:

docker build
      ↓
RUN npm install
Enter fullscreen mode Exit fullscreen mode

RUN happens during the image build.


Copy our application

COPY . .
Enter fullscreen mode Exit fullscreen mode

Now we copy the rest of our source code into the image.


Tell Docker about the application port

EXPOSE 3000
Enter fullscreen mode Exit fullscreen mode

Our Node.js application listens on port 3000.

This tells Docker that the application uses that port inside the container.

It does not yet make the application accessible from our computer.

We'll get to that.


Tell Docker how to start the application

CMD ["npm", "start"]
Enter fullscreen mode Exit fullscreen mode

When the container starts, Docker will run:

npm start
Enter fullscreen mode Exit fullscreen mode

So the application can start.


We Have Instructions. But We Don't Have an Application Running Yet.

This was one of the first things that confused me.

We have a Dockerfile.

But a Dockerfile isn't our running application.

It's just instructions.

So we need to use those instructions to create something.

That's where the Docker image comes in.


Building the Image

We run:

docker build -t my-app .
Enter fullscreen mode Exit fullscreen mode

Docker reads the Dockerfile and builds an image.

The flow is:

                Dockerfile
                    │
                    │ docker build
                    ↓
                Docker Image
Enter fullscreen mode Exit fullscreen mode

The -t my-app gives our image a name.

And that final .?

It tells Docker:

Use the current directory as the build context.

This small . is easy to overlook when you're starting.

Now we can check our images:

docker images
Enter fullscreen mode Exit fullscreen mode

We should see our:

my-app
Enter fullscreen mode Exit fullscreen mode

At this point, we have an image.

But our application still isn't running.

Why?

Because an image isn't a running application.


From Image to Container

Now we finally use:

docker run my-app
Enter fullscreen mode Exit fullscreen mode

Docker uses our image to create a container and starts the application.

The flow is:

Dockerfile
    ↓
docker build
    ↓
Image
    ↓
docker run
    ↓
Container
    ↓
npm start
    ↓
Application
Enter fullscreen mode Exit fullscreen mode

This was the mental model that made Docker much easier for me:

Dockerfile → Image → Container

A useful analogy is:

Dockerfile = Recipe
Image      = Prepared package / blueprint
Container  = Running instance
Enter fullscreen mode Exit fullscreen mode

And one image can create multiple containers:

              my-app Image
              /     |     \
             ↓      ↓      ↓
        Container Container Container
Enter fullscreen mode Exit fullscreen mode

The image is the reusable package.

The containers are the instances created from it.


The Application Is Running... So Why Can't I Open It?

Now comes another small surprise.

Our Node.js application is running inside the container.

It's listening on:

3000
Enter fullscreen mode Exit fullscreen mode

But our browser is outside the container.

So if I simply run:

docker run my-app
Enter fullscreen mode Exit fullscreen mode

I can't necessarily access it through:

localhost:3000
Enter fullscreen mode Exit fullscreen mode

We need to connect the port on our computer to the port inside the container.

That's what this does:

docker run -p 3000:3000 my-app
Enter fullscreen mode Exit fullscreen mode

The format is:

-p HOST_PORT:CONTAINER_PORT
Enter fullscreen mode Exit fullscreen mode

So:

-p 3000:3000
Enter fullscreen mode Exit fullscreen mode

means:

Your Computer
localhost:3000
       │
       ↓
Container
port 3000
       │
       ↓
Node.js Application
Enter fullscreen mode Exit fullscreen mode

Now:

http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

should show:

Hello from Docker!
Enter fullscreen mode Exit fullscreen mode

That was one of those Docker commands that makes much more sense once you understand why it exists.


But What About EXPOSE 3000?

At first, I also wondered:

If we already wrote EXPOSE 3000, why do we need -p 3000:3000?

They're different.

EXPOSE 3000
Enter fullscreen mode Exit fullscreen mode

means:

The application uses port 3000 inside the container.

Whereas:

-p 3000:3000
Enter fullscreen mode Exit fullscreen mode

means:

Connect port 3000 on my computer to port 3000 inside the container.

So:

EXPOSE
   ↓
Application uses port 3000

-p
   ↓
Connect host port → container port
Enter fullscreen mode Exit fullscreen mode

That distinction is important when you're starting with Docker.


What Actually Happens When We Run docker run?

When we run:

docker run -p 3000:3000 my-app
Enter fullscreen mode Exit fullscreen mode

Docker is doing more than simply starting our Node.js application.

Conceptually:

Docker Image
     ↓
Create Container
     ↓
Set up container filesystem
     ↓
Set up networking
     ↓
Map port 3000
     ↓
Start container process
     ↓
npm start
     ↓
Application runs
Enter fullscreen mode Exit fullscreen mode

We don't need to understand every internal Docker component yet.

For now, understanding this flow is enough.


Then the Container Stops...

Now imagine the application crashes.

Or we stop the container.

We can check running containers:

docker ps
Enter fullscreen mode Exit fullscreen mode

But what if the container already stopped?

Use:

docker ps -a
Enter fullscreen mode Exit fullscreen mode

And then:

docker logs <container-id>
Enter fullscreen mode Exit fullscreen mode

This became one of the first useful debugging habits for me:

Something isn't working
        ↓
docker ps -a
        ↓
Find the container
        ↓
docker logs
        ↓
Understand what happened
Enter fullscreen mode Exit fullscreen mode

Maybe the application crashed.

Maybe a dependency is missing.

Maybe an environment variable isn't configured.

Docker doesn't automatically fix application problems.

It gives us a consistent environment to run the application.


One More Thing I Learned: Docker Images Have Layers

Look at these lines again:

COPY package*.json ./

RUN npm install

COPY . .
Enter fullscreen mode Exit fullscreen mode

Why do we copy the package files separately?

One reason is Docker's image layering and build cache.

Docker builds images in layers.

If we change our source code but don't change package.json, Docker may be able to reuse the layer where the dependencies were installed.

Conceptually:

Package files
      ↓
Install dependencies
      ↓
Application source
Enter fullscreen mode Exit fullscreen mode

So the next build doesn't necessarily have to redo everything from scratch.

This becomes increasingly useful as applications become larger.


And There Is a Small File Called .dockerignore

Our project might contain:

node_modules
.git
.env
coverage
Enter fullscreen mode Exit fullscreen mode

We don't necessarily want all of those sent as part of the Docker build context.

So we can create:

.dockerignore
Enter fullscreen mode Exit fullscreen mode

and add:

node_modules
.git
.env
coverage
Enter fullscreen mode Exit fullscreen mode

It's a small thing, but it's useful to know early.


So What Have We Actually Learned?

Let's stop for a moment.

We started with:

"It works on my machine."

Then we needed a consistent way to describe our application's environment.

That led us to:

Dockerfile
Enter fullscreen mode Exit fullscreen mode

We used the Dockerfile to create:

Image
Enter fullscreen mode Exit fullscreen mode

We used the image to create:

Container
Enter fullscreen mode Exit fullscreen mode

The container runs our application.

Then we needed our browser to reach the application.

That introduced:

Port Mapping
Enter fullscreen mode Exit fullscreen mode

Then we needed to understand what happened when something went wrong.

That introduced:

docker ps
docker ps -a
docker logs
Enter fullscreen mode Exit fullscreen mode

And we learned that images are built in layers and can use caching.

So our journey currently looks like:

"It works on my machine."
          ↓
Need a consistent environment
          ↓
      Dockerfile
          ↓
      docker build
          ↓
        Image
          ↓
      docker run
          ↓
      Container
          ↓
    Port Mapping
          ↓
Running Application
Enter fullscreen mode Exit fullscreen mode

And this is already enough to understand the core Docker workflow.


But Our Application Will Eventually Grow

Right now, we're running a simple Node.js application.

Real applications are rarely this simple.

Eventually, our application might need:

Backend
Database
Cache
Other Services
Enter fullscreen mode Exit fullscreen mode

And that's where Docker introduces more concepts.

For example:

Our application needs persistent data

We'll need to understand Docker Volumes.

Multiple containers need to communicate

We'll need to understand Docker Networking.

We have multiple containers to manage

We'll need to understand Docker Compose.

We want to build smaller production images

We'll need to understand Multi-stage Builds.

We want automated deployments

We'll need to understand Docker + CI/CD.

But I don't think we need to learn all of these at once.

For me, the important first step was understanding:

How does my application become an image, and how does that image become a running container?


Where Does the Image Go When We Want to Share It?

Our image currently exists on our computer.

But imagine we want to deploy the application somewhere else.

We don't want to manually rebuild the image on every machine.

Instead, we can store the image in a container registry.

The flow becomes:

My Computer
     ↓
Docker Image
     ↓
   push
     ↓
Container Registry
     ↓
    pull
     ↓
Another Environment
     ↓
Docker Image
     ↓
Container
Enter fullscreen mode Exit fullscreen mode

For example, Docker Hub is a container registry.

We can download an existing image using:

docker pull nginx
Enter fullscreen mode Exit fullscreen mode

And then run it:

docker run nginx
Enter fullscreen mode Exit fullscreen mode

So images can be:

Built locally
     OR
Pulled from a registry
Enter fullscreen mode Exit fullscreen mode

Either way, we eventually use the image to create a container.


And This Is Where Docker Connects to Deployment

Now the whole idea starts to make sense.

We have:

Application
    ↓
Dockerfile
    ↓
Docker Image
Enter fullscreen mode Exit fullscreen mode

That image can be stored in a registry:

Docker Image
    ↓
Container Registry
Enter fullscreen mode Exit fullscreen mode

And a server or cloud platform can use that image:

Container Registry
        ↓
   Docker Image
        ↓
     Container
        ↓
    Application
Enter fullscreen mode Exit fullscreen mode

So a simplified real-world workflow looks like:

Developer
    ↓
Code
    ↓
Dockerfile
    ↓
Docker Image
    ↓
Container Registry
    ↓
Production Environment
    ↓
Container
    ↓
Application
Enter fullscreen mode Exit fullscreen mode

This is one of the reasons Docker is so useful in modern development and deployment workflows.


The Mental Model I Finally Have

After going through this example, Docker doesn't feel like a list of random commands anymore.

I can now think about it as a journey:

                 Application
                      │
                      ↓
                 Dockerfile
                      │
                 docker build
                      ↓
                 Docker Image
                      │
                  docker run
                      ↓
                Docker Container
                      │
                      ↓
                 Application
Enter fullscreen mode Exit fullscreen mode

And when I need to share it:

Docker Image
     ↓
docker push
     ↓
Container Registry
     ↓
docker pull
     ↓
Docker Image
     ↓
Container
Enter fullscreen mode Exit fullscreen mode

That's the foundation.


What I'm Learning Next

I'm intentionally stopping here.

I haven't learned every Docker concept yet, and I don't think beginners need to learn everything in one sitting.

My next topics are:

Docker Volumes
       ↓
Docker Networking
       ↓
Docker Compose
       ↓
Multi-stage Builds
       ↓
Docker Security
       ↓
CI/CD with Docker
       ↓
Cloud Deployment
Enter fullscreen mode Exit fullscreen mode

Each of those solves a different problem that appears as our application becomes more complex.

And that's probably the biggest thing I learned from starting Docker:

Don't just memorize the command. Understand why the command exists.

Once the reason is clear, the command becomes much easier to remember.


Final Takeaway

If you're just starting Docker, don't worry about memorizing everything.

Start with these three relationships:

Dockerfile
    ↓
   Image
    ↓
Container
Enter fullscreen mode Exit fullscreen mode

And understand what happens around them:

Dockerfile
    ↓
docker build
    ↓
Image
    ↓
docker run
    ↓
Container
    ↓
Port Mapping
    ↓
Running Application
Enter fullscreen mode Exit fullscreen mode

That's where I would start.

Docker becomes much easier when you stop looking at it as a collection of commands and start seeing it as a journey your application goes through.

Top comments (0)