brandLogo

Tech Notes.

By Danish

Lets create an image of our own

Now that we have our setup docker setup ready we are good to start creating an image of our own.

The code for our basic node app is available in node-code directory.

We'll start with a simple node app single-app that'll have only one endpoint to greet.

The endpoint is http://*/greet/:name

Writing our dockerfile

dockerfile is the blueprint to create an image. It contains a set of instructions to tell docker about how to process our app. This includes :

  • Base image: Provides the basic environment that our job needs to run, like in our case- node.
  • Instructions: Like copying files and installing dependency
  • Entry point: The command to start app

Some common instructions we need to pass

  • FROM: Specifies the base image to use for the build
  • RUN: Executes commands inside the container
  • COPY: Copies files from the host to the container
  • ENV: Sets environment variables
  • CMD: Specifies the default command to run when the container starts

We have our dockerfile for this example here

# Use the official Node.js image (version 20)
FROM node:20

# Set the working directory in the container
WORKDIR /app/single-app

# Copy only the package.json files to install dependencies
COPY package*.json /app/single-app

# Install dependencies
RUN npm install

# Copy the entire app code from node-apps to the working directory
COPY . /app/node-code/single-app

# Environment variable to pass port number,default 3000
ENV PORT=3000

# Expose the port that your app will run on
EXPOSE $PORT

# Command to run the application
CMD ["node", "/app/node-code/single-app/index.js"]

Notice that all paths are relative to the dockerfile location.

This is the file responsible to create our images, which we'll later use to create our containers.

Let's create our first image

Now that we have our dockerfile ready, we have told docker about the entire infrastructure required by our app to run, we can now create our image.

Get inside the ubuntu terminal and navigate to the folder where the dockerfile resides. we can do that adding /mnt/ to path and not using : with drive.

For example in my case it's cd /mnt/d/docker-demo/node-code/single-app

Use the command:

docker build -t <image_name>:<tag> <path to directory containing docker file>

-t flag allows us to add tag explicitly to the image, otherwise "latest" will be used by default

I'll use docker build -t single-app:1.0 .

This will create the image for us. We can check with docker images and our image should be in the list.

Creating container from image

Now that we have our image handy, we are good to start with our container. This will be the actual functional unit where our app will run.

In the location let's start our container

docker run  --name <container_name> -p <host_port>:<container_port> -e PORT=<container_port> <image_name>:<image_tag>

Here is the break down

  • --name <container_name>: This is optional argument. It gives our containers name, so we can identify them easily
  • -p <host_port>:<container_port>:
    • host_port: the port on our system on whose requests will be forwarded to container's port
    • container-port: the port inside container in which our app will run
  • -e PORT=<container_port>: This will be an environment variable for our application, remember we added the port number to be picked from environment in our node app.
  • <image_name>: Name of the image we want to build
  • <image_tag>: The version of image we want to target

I run docker run --name single-app -p 3200:3100 -e PORT=3100 single-app:1.0

This starts our app container and keeps the logs attached. A -d flag can be attached to run container in detached mode and terminal won't be blocked.

Don't worry if your terminal is attached to the container, closing the terminal will not stop the container.

Now let's begin now. Try to access http://localhost:3200/greet/danish

Congrats 🥳🥳 our first docker app ready.

Basic Container Commands

  • docker ps : This will display a list of all the currently. Our single-app should be available here.
  • docker logs <container-name> : This will print the entire log of the container. In our case it'll be docker logs single-app.
    • if we want to see the logs in real-time we can attach flag -f. docker logs -f single-app and this will get us logs like we get on our ide/terminal
  • docker stop <container-name>: This will stop the container. Imagine you run a bakery shop and close at 11. You can simply shutdown the service to take order and display on UI, not accepting live orders.
    • docker stop single-app. Now let's verify if it has actually stopped. Run docker ps and our single-app is no where to be found.
  • docker ps -a: This displays the list of all containers available on our machine, wether it's running or not. Run this command and and we'll have our single-app in the list with STATUS=Exited
  • docker start <container-name>: Next day you're back on the shop and start accepting orders. You can start the service back up.
    • docker start single-app: This will start our app again.
      • We can check the updated status with docker ps. single-app should have STATUS=Up.
      • docker logs single-app: The logs will show server startup logs
  • Now let's make some changes to our app and see how we can update our containers
/**
 *
  * @param {import('express').Request} req
  * @param {import('express').Response} res
  */
actionGreet = (req, res) => {
  const name = req.params.name;
  logger.info(`Request received to greet ${name}`);
  // used in v1
  // res.send(`Hello ${name}, Welcome to your single-app container!`);
  // used in v2
  res.send(`Hello ${name}, Welcome to the single-app container!`);
};
  • We'll first have to get the rid of the existing containers. And then add our new changes back in place, like we did earlier.
  • docker rmi <image_name>:<tag>: It removes the image from our local. For us docker rmi single-app:1.0 but we'll get an error

    Error response from daemon: conflict: unable to remove repository reference "single-app:1.0" (must force) - container <container_id> is using its referenced image <image_id>

    • We always need to remove the container prior to removing the image.
  • docker rm <container-name>: It deletes the container.
    • let's assume you have made some changes to the app and you need to get rid of the pervious ones.
    • docker rm single-app is the way to go for us. But when we execute this, we'll get an error.

      Error response from daemon: cannot remove container "/single-app": container is running: stop the container before removing or force remove

    • We always need to close the container prior to removing it.
    • docker stop single-app Now we can get rid of the container and thereafter the image.
  • Here is the exact flow to get rid of things
    • Stop the containers -> Remove the containers -> Remove the image
      • docker stop <container-name>
      • docker rm <container-name>
      • docker rmi <image-name>
  • Now that we have made the changes, technically its a new version of our app. Since, its not a major change, let's consider it v1.1
  • Like our code is versioned by commits in git, images are versioned by tags in docker
  • Let's create the new version of app but with updated version i.e tag
    docker build -t single-app:1.1 .
  • Once image is built, let's start the container
    docker run --name single-app -p 3200:3100 -e PORT=3100 single-app:1.1
  • Our app is now up and running, let's hit URL and see.

With this we have successfully started a new version of the app in the docker

Push the image to repo

Now that we have the functional app ready, let's share it with the team and make it available for the QA to test.
Consider it as pushing our commits to remote, so that it can be available to everyone.

  • We have already completed the setup for our DockerHub
  • Let's go to https://hub.docker.com/repositories/our_username
  • Create a repo of our own by adding name and description.
  • I have created with the same name docker-demo.
  • You can check that out here.
  • Let's tag the local image to the repo
    docker tag <image-name-local>:<image-tag-on-local> <dockerhub-username>/<repo>:<image-tag-on-remote>
    • I'll keep the name same, so for me its docker tag single-app:1.1 devdanishjaved/docker-demo:single-app-1.1
    • Now we can use docker images to check the available images and we should have a new image with our repo name but with the same related image id.
    • Let's push this to the repo with docker push <dockerhub-username>/<repo>:<image-tag>
    • For me it's docker push devdanishjaved/docker-demo:single-app-1.1
🥳🎉 With this, we have successfully shared the image with our team 🥳🎉

Pull an image from the repo

Now let's image, the company was extremely pleased with our work and they have now given us a huge bonus and a paid vacation (Don't get carried away, i said imagine, its just our imagination 😂😂).
While we were on vacation, our teammate updated the app. Now we need the updated image. Similar to how we take a pull for the codebase.

docker pull <dockerhub-username>/<repo>:<image-tag>

Here we are to pull the v1.2 image, so for me its docker pull devdanishjaved/docker-demo:single-app-1.2.
Upon successful pull, the logs would end something like this

Digest: sha256:<hash_of_image's_content>
Status: Downloaded newer image for devdanishjaved/docker-demo:single-app-1.2
docker.io/devdanishjaved/docker-demo:single-app-1.2

Use docker images to check the image is available

Let's run the container and test.
I'll use the same command used previously to run the container just with the updated tag name.
docker run --name single-app-1.2 -p 3200:3100 -e PORT=3100 devdanishjaved/docker-demo:sing le-app-1.2 But got an error

docker: Error response from daemon: driver failed programming external connectivity on endpoint single-app-1.2 (a85b68c89fcea9db5044f3cb9737e89c730a105def35272a605686c8d31449f3): Bind for 0.0.0.0:3200 failed: port is already allocated.

It is because the VM's port 3100 is already occupied by an older version of the app. Although we got an error, but that was port forwarding step, so our container is ready. We can use docker ps to check. Status would be Created, not Up.
Let's remove the app and recrate it with updated port. Since the container never started we can remove it in one go with docker rm <container_id>

Now let's create the new container with docker run --name container_name_of_our_choice -p VM's_Port:Host_Port PORT=VM's_Port <dockerhub-username>/<repo>:<tag>

For me its docker run --name single-app-1.2 -p 3300:3300 -e PORT=3300 devdanishjaved/docker-demo:single-app-1.2. We'll get our server stat-up logs and be able access the new changes.

With this we are good to wrap up this section with basic understanding.