How to containerize your web app- a beginner-friendly tutorial for Dockerfile
Tags: docker, devops, containerapps, webdev
Published: 2024-06-18 18:07:18

Welcome to part 2 of the series `Docker for Dummies` in this blog we are going to create an image of a small web app and learn about what each step does. Without any further ado let's get started. For this blog, I'm going to use a simple vue-app quiz app that'll let you guess the name of the book based on the first line. If you want to follow along with the setup you can find the GitHub link to the web app [here](https://github.com/SwikritiT/Guessthebook-blog) or you can use your app or you can create a simple `hello-world` app in node or framework/language of your choosing.

## Let's create the image
> Note: this tutorial follows ubuntu commands but they should be similar for other OS's as well

Let's clone the app and go inside of it

```bash
git clone https://github.com/SwikritiT/Guessthebook-blog
cd Guessthebook-blog

```

We are going to start by creating a file called `Dockerfile` in the root of our repository.

```bash
touch Dockerfile
```

### Dockerfile

A Dockerfile is a script that contains all the steps necessary to build an image. Dockerfile starts with something called `base image`. A `base image` is a pre-configured environment that our images build upon. The base image can be an OS like Linux, alpine, or some application stack. Choosing an appropriate base image is crucial for the overall size and productivity of the image of our application. We will talk more about how to select the appropriate base image in the later part of this series so for now let's keep in mind that a lighter base-image will create a lighter app image(this comes with its limitations which we will talk about in more detail in the later part of this series). So, for this app, we are going to use `node:alpine` images as a base, you can select your needed version of a base image from an image registry like [Docker Hub](https://hub.docker.com/_/node).

> Note: If you're not building the image of the node application, you need to select the image appropriate for your stack

```dockerfile
# Start with a base image
FROM node:alpine
```

> `FROM`: Specifies the base image to use. Every Dockerfile starts with this instruction.

The next step is to set the `WORKDIR` i.e. working directory for our container where commands will be executed and files will be stored by default. Normally, for web apps, workdir is set to `/usr/src/app` but you can customize it as per your need.

```dockerfile
# Set the working directory
WORKDIR /usr/src/app
```

Now let's copy the necessary config files to our container, in our case, package.json and lockfile. For this, we will use `COPY` command

```dockerfile
# Copy package.json and package-lock.json to the working directory
COPY package*.json ./
```

> `COPY or ADD`: Copies files from your local filesystem into the container.

The next step is to install dependencies just like we do in our host machine and for this `RUN` command can be used

```dockerfile
# Install the application dependencies
RUN npm install
```

> `RUN`: Executes commands in the container. These commands are typically used to install software packages.

Now we copy the rest of the files to our container's working directory

```dockerfile
# Copy the current directory contents into the container at /usr/src/app
COPY . .
```

The above command will copy every file and folder that is in your app to the container's working directory. This includes the build file, files created by IDE, or other miscellaneous files/folders that might not be necessary for building the image. So, it is a good practice to either copy only necessary files or create a `.dockerignore` file with the list of files and folders that you don't want to be copied inside the container. The syntax of the `.dockerignore` file is similar to that of `.gitignore`. Learn more [here](https://docs.docker.com/build/building/context/#dockerignore-files)

The next step is to build our application

```dockerfile
# Build the application
RUN npm run build
```

The next step is to `Expose` the ports on which a containerized application listens for network connections

```dockerfile
# Make port 3000 available to the world outside the container
EXPOSE 3000
```

> ` EXPOSE` is a way to document which ports the application running inside the container will use. It does not map the port to the host machine’s ports. It simply indicates which ports are intended to be accessible.

We've come to the final stage where we will run the application. There are two commands that we can use to do this `CMD` and `ENTRYPOINT` for this part we will be using `CMD` and we'll talk about `ENTRYPOINT` in later parts.

Since we will be running the application in production env we will use the `preview` command for this. Later we'll also create a dockerized dev env.

```dockerfile
# Define the command to run the app
CMD ["npm","run","preview"]
```

> `CMD`: Provides the command that will be run when a container is started from the image. Only one CMD instruction can be present in a Dockerfile.

Now let's look at the whole `Dockerfile`

```Dockerfile
# Start with a base image
FROM node:alpine

# Set the working directory
WORKDIR /usr/src/app

# Copy package.json and package-lock.json to the working directory
COPY package*.json ./

# Install the application dependencies
RUN npm install

# Copy the current directory contents into the container at /usr/src/app
COPY . .

# Build the application
RUN npm run build

# Make port 3000 available to the world outside the container
EXPOSE 3000

# Define the command to run the app
CMD ["npm","run","preview"]
```

In Docker, each line in your Dockerfile creates a new layer in the final image, like adding ingredients to a sandwich. These layers stack on top of each other, with each layer representing a change or addition, such as copying files or installing software. Docker saves these layers, and if you rebuild your image and some layers haven’t changed, Docker reuses them, speeding up the build process and reducing redundancy. So, in the above dockerfile, we have 8 layers.

## Build the image

Now that we've created the image it's time to build it and get it running. We can run the following command in the terminal from the root of our repository to build a docker image

```bash
$ docker build . -t guessthebook:v1

[+] Building 1.1s (11/11) FINISHED                               docker:default
 => [internal] load build definition from Dockerfile                       0.0s
 => => transferring dockerfile: 191B                                       0.0s
 => [internal] load metadata for docker.io/library/node:20-alpine          0.9s
 => [internal] load .dockerignore                                          0.0s
 => => transferring context: 129B                                          0.0s
 => [1/6] FROM docker.io/library/node:20-alpine@sha256:66c7d989b6dabba6b4  0.0s
 => [internal] load build context                                          0.0s
 => => transferring context: 1.41kB                                        0.0s
 => CACHED [2/6] WORKDIR /usr/src/app                                      0.0s
 => CACHED [3/6] COPY package*.json ./                                     0.0s
 => CACHED [4/6] RUN npm install                                           0.0s
 => CACHED [5/6] COPY . .                                                  0.0s
 => CACHED [6/6] RUN npm run build                                         0.0s
 => exporting to image                                                     0.0s
 => => exporting layers                                                    0.0s
 => => writing image sha256:08f32d7f583b7e65a844accafff8fc19930849204c5ae  0.0s
 => => naming to docker.io/library/guessthebook:v1                         0.0s

```

You will get the output like such with the information of each layer being built. The command `docker build . -t guessthebook:v1` reads the Dockerfile in the current directory, builds a Docker image according to its instructions, and tags this image as `guessthebook` with version `v1`.

Now if you run the following command in your terminal, you should see the relevant info about the image that we just created

```bash
docker images

REPOSITORY                                   TAG            IMAGE ID       CREATED         SIZE
guessthebook                                 v1             08f32d7f583b   5 minutes ago   275MB
```

## Running a Docker Container

Once you have built the image, you can run it with the `docker run` command:

```bash
docker run -p 3000:3000 guessthebook:v1
```

This command runs a container from the `guessthebook:v1` image, mapping port `3000` of the container to port `3000` on the host machine. We use `-p` for port mapping.

We can improvise the above command to use the option `-d` to run it in detached mode

```bash
 docker run  -d -p 3000:3000 guessthebook:v1
```

After running the docker container you can visit `http://localhost:3000` to ensure everything works fine. If everything is setup and working correctly you should be greeted with this screen


![app homepage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iggehash0bf3euqpyxt3.png)

You can now take a rest and play the quiz to see how many you get right.

## Stop container

You can stop the container with the following command

```bash
docker stop <container_name or id> # can run `docker ps` to get the name and id
```

## Publish the image to the docker hub

If we want to take this a step further we can publish the image to the docker hub. For that create an account on [Docker Hub](https://hub.docker.com/signup) if you haven't already. Next, you can create a new repository.


![docker hub create repo page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l9l3ctp3zg0qxnl46ckw.png)

1. Login to your docker hub through docker CLI

```bash
docker login
```

2. Tag your local image to match the repo image

```bash
# docker tag local-image:tagname new-repo:tagname
docker tag guessthebook:v1 <your-docker-username>/guessthebook
```

3. Push the image

```bash
# docker push new-repo:tagname
docker push <your-docker-username>/guessthebook
```

Now, you can go and check if your docker hub has the image that you just pushed.

To test the image you can now run the image by pulling directly from `Docker Hub`

```bash
# let's remove the locally tagged image first
docker rmi <your-docker-username>/guessthebook

# run the container
docker run -p 3000:3000 <your-docker-username>/guessthebook
```

That's it for this blog. Hope you enjoyed this one and learned something new as well! See you in the next part of this series. If you have any queries or suggestions please comment them below!