Working Effectively with Docker Images

Stephen Afam-Osemene

Docker is the world’s leading software container platform.

Containers are a way to package software in a format that can run isolated on a shared operating system. Unlike VMs, containers do not bundle a full operating system - only libraries and settings required to make the software work are needed. This makes for efficient, lightweight, self-contained systems and guarantees that software will always run the same, regardless of where it’s deployed

Docker is a wonderful tool for many things. A few of them are;

  • As a version control system for your entire app's operating system by storing each configuration as a central build.
  • To distribute/collaborate our app's operating system with our team.
  • To run your code on your laptop in the same environment as you have on your server.

We are going to explore Docker images. Why we should use them, and how to go about it.

To illustrate this, we will create a Docker image for AdonisJs applications.

To follow this tutorial you will need Docker installed on your machine. You can find detailed installation instructions here.

When to build a Docker image

Docker images are used to configure and distribute application states. Think of it as a template with which to create the container.

With a Docker image, we can quickly spin up containers with the same configuration. We can then share these images with our team, so we will all be running containers which all have the same configuration.

There are several ways to create Docker images, but the best way is to create a Dockerfile and use the docker build command.

Writing the Dockerfile

The Dockerfile is a file that tells Docker how to build an image. There are various instructions we give to let Docker know what to do to configure our image. The full documentation on the Dockerfile can be found here.

Example Dockerfile

Following the AdonisJs installation instructions, we will ask Docker to do the following.

  1. Build from the node:8 image since AdonisJs requires at least Node 8.0
  2. Install the cli tool.
  3. When a container starts, we need to create a new application in the current directory if none exists.
  4. Serve the AdonisJs application.

Let us create a folder called adonisjs for our code.

mkdir adonisjs

With that in mind, we can have our Dockerfile like this.

FROM node:8

LABEL maintainer="Stephen Afam-Osemene <stephenafamo@gmail.com>"

# ------------------------------------------
# install the adonis CLI
# ------------------------------------------
RUN npm i -g @adonisjs/cli

# ------------------------------------------
# change the working directory
# ------------------------------------------
WORKDIR /var/www

# ------------------------------------------
# copy our initialization file and set permissions
# ------------------------------------------
COPY init.sh /init.sh
RUN chmod 755 /init.sh

CMD ["/init.sh"]
  • The FROM instruction tells Docker to build from the node:8 image
  • We copy the init.sh file into our image and it will run when the container is first created. We will use it to install and serve AdonisJs

Our init.sh will be like this.

#!/bin/bash

# check if workinig directory is empty
# Install adonisjs in the working directory if empty
if [[ -z "$(ls -A $PWD 2>/dev/null)" ]]; then
    adonis new . $adonisFlags
    # change host and port to work properly inside a Docker container
    sed -i -e "s/HOST=.*/HOST=0.0.0.0/g" .env
    sed -i -e "s/PORT=.*/PORT=80/g" .env
fi

# check for the .env file
if [[ -z "$(ls -A $PWD | grep .env)" ]]; then
    echo "no .env file found."
    exit 1
fi

# source the .env file so we can use the variables
source .env

# serve production build if the NODE_ENV is set to production
# serve with --dev flag if not in production
if [[ "$NODE_ENV" == "production" ]]; then
    adonis serve
else
    adonis serve --dev 
fi

The code for this Docker image can be found here To further understand how to write a Dockerfile, please take some time to read through the full documentation.

After creating a Dockerfile, we can then build our image.

Building our image from a Dockerfile

The docker build command creates a new Docker image from a Dockerfile in the specified directory.

docker build /path/to/directory

If the Dockerfile is present in the current directory, we can simply do

docker build .

To specify the name of the file, we use the -f or --file flag

docker build -f CustomDockerfile .

Naming our Image

When a new image is built, it is given an ID but no name. If you run docker images you will see the REPOSITORY and TAG as <none> To change that we pass a string to the -t or --tag flag.

docker build -t my-adonis-js .

Specifying the TAG

If we run docker images we will see our image with the REPOSITORY set as my-adonis-js and the TAG set to latest.

However, this means that each time we build our image, it will overwrite the old one. It does this because by default docker build uses the TAG latest. To prevent this, we should explicitly define the TAG.

NOTE:There are other reasons to avoid using the default latest tag. Some of them are highlighted in this article.

To specify the TAG of our image, in the string passed to the docker build command, we pass a colon : and then the TAG.

docker build -t my-adonis-js:1.0.0 .

NOTE:The parameter passed to the--tag flag is actually the repository and the tag.

Distributing our Image

There are several ways to distribute our images.

  • With a tar archive
  • Using Docker Hub
  • Other methods

With a tar archive

Creating the tar archive

To create a tar archive of our repository, we use the docker save command. We specify the file to write to using the -o or --output flag.

docker save -o my-adonis-js.tar my-adonis-js

When run like this, it will save all tags of the my-adonis-js image. To save only one tag we should specify it.

docker save -o my-adonis-js.tar my-adonis-js:1.0.0

If we want to save more than one image in the same file, we can pass more images to the command

docker save -o my-adonis-js.tar my-adonis-js:1.0.0 my-adonis-js:2.0.0

NOTE: The images passed do not have to be tags of the same image. For example, we can do;

docker save -o backup-images.tar my-adonis-js:1.0.0 ubuntu:16.04 node:8

Loading a tar archive

After creating and sharing our tar archive, we have to then load them into Docker. To do this, we use the docker load command. To specify the input file, we use the -i or --input flag .

docker load -i my-adonis-js.tar

This will load all the images we have saved into the tar archive. The images will now show up when we run docker images.

Using Docker Hub

Docker Hub is a public repository for Docker images. There are thousands of Docker images available and anyone can signup and use it to host their own Docker image.

Docker Hub

First of all, if you don't have an account yet, you will need to sign up at hub.docker.com.

Next, we log in on our terminal using the command docker login. We will be prompted to input our username and password.

docker login

To push to Docker Hub, we have to change the tag of our image to include our Docker Hub username. For that, we use the docker tag command.

docker tag my-adonis-js:1.0.0 username/my-adonis-js:1.0.0 

NOTE: This will NOT delete the previous image. If we need to do that we run

docker rmi my-adonis-js:1.0.0

Then we push the image to Docker Hub.

docker push username/my-adonis-js:1.0.0

By default, all repositories on Docker Hub are public. But you can also have private repositories.

Anyone with access to the image on Docker Hub can get it by running

docker pull username/my-adonis-js:1.0.0

Other methods

There are several other ways of sharing your Docker image.

Running a Docker image

Once we have our image on the machine, we can then run it using

docker run --name=adonis username/my-adonis-js:1.0.0

The above command will spin up a new container based on our image and run it. If we do not pass the --name parameter, Docker will pick a random name for our container.

Many images require some extra parameters to be passed to the run command, so take some time to read through the documentation of an image before you use it.

Also, take some time to read the full documentation of the docker run command.

We can see our running containers using the docker ps command. To see ALL containers, we add the -a flag - docker ps -a.

Now, we should be able to see our AdonisJs container up and running! Our team members can also pull the image and run the same application, and if we ever change it, we can just create a new tag for it.

One more thing though. If we actually want to see our AdonisJs website we need to make some changes to our docker run command.

First, the Adonis server is started inside the container on port 80. We have to map a port on our host machine to port 80 on the container to be able to visit the server. Let's use port 12345.

docker run --name=adonis -p 12345:80 username/my-adonis-js:1.0.0

Now we can visit out AdonisJs site on http://0.0.0.0:12345.

AdonisJs

Second, we don't just want the base AdonisJs site, we want to be able to edit the files and develop the site. To do this we have to mount a directory onto the working directory of the container. That's the way we configured it :-).

docker run --name=adonis -p 12345:80 -v $PWD/src:/var/www username/my-adonis-js:1.0.0

Now once the container is up, we can view the files in the src folder and edit them. Note that this will not change the image itself. To do that, we can use docker commit. However, it is more maintainable to use a Dockerfile.

Conclusion

Docker is a very powerful tool, and images are at its very heart. Understanding how to use images are key to making effective use of Docker.

We have been able to build, distribute and run a Docker image. If you have any questions, suggestions, comments, kindly leave them below.

There is so much more to do with Docker. I hope to write more about it. Let me know if there is anything you'd like me to write about.