My FeedDiscussionsHeadless CMS
Sign in
Log inSign up
Learn more about Hashnode Headless CMSHashnode Headless CMS
Collaborate seamlessly with Hashnode Headless CMS for Enterprise.
Upgrade ✨Learn more
Docker Tutorial for Beginners

Docker Tutorial for Beginners

Liron Navon's photo
Liron Navon
·Jan 30, 2019

Docker is a tool used to create and run containers, self-sufficient units that can have their own operating system, tools, and libraries, and run your code in an isolated environment. That means you don't have to worry about the libraries and the operating system architecture which can be different than your own development environment. Docker solves one of the most common excuses developers use when the production code fails: "But, it works on my machine".

it runs on my machine

In this Docker tutorial, I'll cover all the basics and will demonstrate how all Docker beginners can containerize Node.js and Go applications. Even if you aren't familiar with these languages it should be easy for you to follow this tutorial and use any other language.

What is Docker?

Docker is an engine for creating containers. You can think of it as an operating system that can be mounted inside another operating system, and managed by the Docker engine.

Before we start working with Docker we need to understand 3 concepts:

Container - A standardized unit of software, a container will typically run an operating system, any libraries that we may need, and our application.

Image - An image holds the layers that when mounted on the Docker engine, can operate as a container.

Dockerfile - A Dockerfile is a blueprint for creating a Docker image, it can inherit from another container, define what software to install, what files to transfer from our computer to the image, and what commands to run.

Prerequisites

1. Installing Docker

  • OSX and Windows Pro users: The easiest way is to use Docker desktop which you can download here. All you need to do is download the version for your operating system and follow the installer instructions.

  • Windows Home users: Please follow this video guide:

  • Ubuntu users: Please follow this video guide:

For Linux users, you will probably need to set a user group so you won't have to use sudo every time:

# add a group named docker if none exists
sudo groupadd docker

# add the current user to the docker group we just created
sudo gpasswd -a $USER docker

# reset the docker group in the current session, or just exit and reopen the terminal
newgrp docker

If you installed Docker correctly, you should be able to check the version like so:

docker -v

# output: Docker version 18.09.1, build 4c52b90

2. Registering on Docker Hub

We will cover publishing containers to Docker Hub, and for that we will have to register as a user here.

Once registered, we need to set the credentials in our local Docker, and here are three different ways to do so.

# the verbose way
docker login --password super-secret-password --username my-username

# the shorter way
docker login -p super-secret-password -u my-username

# or with stdin, by reading a file (the "safer" way for production)
cat ~/my_password.txt | docker login --username my-username --password-stdin

Getting started

Once installed, you should have the docker command available. Open your terminal/cmd and write the following command to check if Docker is installed: docker -v, and the output should look like this: Docker version 18.09.1, build 4c52b90.

Once you have Docker installed, we can start by running our very first image! We can use the "Hello world" image for this. You just need to run:

docker run -it hello-world

And you should see a message that starts with "Hello from Docker!". Now, let's analyze what happened here:

  1. We called docker run <image-id> to tell docker what image to run.
  2. We saw a message telling us Unable to find image 'hello-world:latest' locally. This is because we run it for the first time. The ":latest" part here is the image tag. We can tag an image with whatever we want (dev, v0.1.1, my-tag, etc...), but the default tag is latest.
  3. Since the image was not found locally, docker had to get it from somewhere, and we got it from Docker Hub. Docker Hub in concept is similar to GitHub, but instead of hosting repositories, it hosts images, and often those images are open sourced. But there are also private images.

The -it flag tells Docker to attach the container to the terminal, so CTRL+C should kill the container. If you want the container to keep running, drop this flag.

Let's build our own container!

For this Docker tutorial, we can choose between using Node.js or Go (just pick one). In reality you should be able to use pretty much any server technology you want (.net, Python, PHP, etc...).

You don't even need to install Go or Node.js to run the examples, since Docker will handle the dependencies for us.

First, let's create a directory for our little project and call it docker-example.

For Node.js

Create a file and call it server.js. In this file simply write this:

const http = require('http');
const port = 8000;

// we create a simple http server
http.createServer((request, response) => {
   // Send the HTTP header 
   // HTTP Status: 200 : OK
   // Content Type: text/plain
   response.writeHead(200, {'Content-Type': 'text/plain'});
   response.end('Docker is awesome!!!');
}).listen(port);

// Console will print the message
console.log(`Server running at http://127.0.0.1:${port}`);

For Go

Create a file and call it server.go, and in this file simply write this:

package main

import (
    "fmt"
    "net/http"
)

func main() {
       var port = 8000

       // this handler will recieve any incoming request     
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
          // send a response text
          w.Write([]byte("Docker is awesome!!!"))
    })

        // print a message to the console
    fmt.Printf("Server running at http://127.0.0.1:%d", port)
        // start accepting new requests  
        http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
}

When running any of these applications (node server.js, or go run server) you should be able to go to http://localhost:8000/ in the browser and see our message. You should also see a message in the terminal.

Let's write our first Dockerfile

The next step in this Docker tutorial is to write our first Dockerfile. The file name should be Dockerfile, with no extensions.

As you can see, a Dockerfile is just a series of commands to the docker engine that tells it what to do:

  • FROM: Start from a base image. This could be an os like Alpine or Ubuntu, or a known image that contains dependencies for us like databases, or language tools.
  • WORKDIR: set a working directory inside the container.
  • COPY: copy the files you need. For simplicity, I copy it all and build inside the container.
  • RUN: you can add multiple RUN commands, each of them is a simple command to be executed in the terminal, those will run when building the image.
  • EXPOSE: you can set ports to expose. I chose 8000 since I also use that port in the servers.
  • CMD: a command to run when starting the container. Sometimes you would see it as an array CMD ['npm', 'start'], and sometimes as a command CMD npm start - both are accepted.

There are a few more commands for Dockerfiles, but these are the most used ones.

Here are examples for a Dockerfile for Go and Node.js, pick the one you built the server with.

Dockerfile for Node.js

npm install is commented out, since we don't use any npm packages, but this is where you would call it.

# inherit from the Go official Image
FROM node:11.8

# set a workdir inside docker
WORKDIR /usr/src/app

# copy . (all in the current directory) to . (WORKDIR)
COPY . .

# run a command - this will run when building the image
# RUN npm install

# the port we wish to expose
EXPOSE 8000

# run a command when running the container
CMD node server.js

Dockerfile for Go

Here's what the Dockerfile for our Go application should look like:

# inherit from the Go official Image
FROM golang:1.8

# set a workdir inside docker
WORKDIR /go/src/server

# copy . (all in the current directory) to . (WORKDIR)
COPY . .

# run a command - this will run when building the image
RUN go build -o server

# the port we wish to expose
EXPOSE 8000

# run a command when running the container
CMD ./server

Let's build an image and run our container!

To build the image you simply need to go into the directory with the Dockerfile and run docker build .. You can replace the . with the path to the directory.

You will see an output that looks something like this:

Sending build context to Docker daemon  3.072kB
Step 1/6 : FROM golang:1.8
 ---> 0d283eb41a92
Step 2/6 : WORKDIR /go/src/server
 ---> Using cache
 ---> c18d9f48a8ee
Step 3/6 : COPY . .
 ---> Using cache
 ---> 41c3635ba598
Step 4/6 : RUN go build -o server
 ---> Using cache
 ---> 6ee0d2bafb00
Step 5/6 : EXPOSE 8000
 ---> Using cache
 ---> 5c16cd7ff831
Step 6/6 : CMD ./server
 ---> Using cache
 ---> 8675fc90ccdd
Successfully built 8675fc90ccdd

As you can see, Docker will specify the commands that it's running and ends up giving us an image id on the last line: 8675fc90ccdd. Just note it down somewhere since we will use it later.

We can now use this image to run the container:

docker run -it -p 8000:8000 8675fc90ccdd

The -p 8000:8000 part of the commands tells docker to redirect from localhost:8000 to the container-ip:8000.

Now you should be able to check out the browser and see the message from our server.

Pushing the image to Docker Hub

We first need to tag our image to give it a proper name. The name must be in this format <USER-NAME>/<IMAGE-NAME>. Here is an example with my username on Docker Hub (lironavon), and my image name (my-server)

# with tag latest
docker tag 294ebde018fe lironavon/my-server

# or with a custom tag
docker tag 8675fc90ccdd lironavon/my-server:v0.1

Now let's create an image on Docker Hub. You can click here to do so, and name it the same as we did when tagging. Here is an image:

Creating Docker image on Docker Hub

After you name the image, scroll down and click create.

Now we can push it to Docker Hub.

# with tag latest
docker push lironavon/my-server

# or with a custom tag
docker push lironavon/my-server:v0.1

More Docker commands

This wouldn't be a comprehensive Docker tutorial unless we take a deeper dive into other basic Docker CLI commands. So, let's check out some of the most useful ones:

# show a table of all the running containers
docker ps

# show a table of all the local images
docker images

# see more information about a container, you can use its ID or its name.
docker inspect 8675fc90ccdd

# similar to git pull, it pulls image updates
docker pull 8675fc90ccdd

# see logs from a container, useful for debugging
docker logs 8675fc90ccdd

# removes a running container
docker rm 8675fc90ccdd

# removes an image
docker rmi 8675fc90ccdd

# stop a running container
docker stop 8675fc90ccdd

Docker 'ps' command is very strong, it has two nice flags that allows it to mix with other commands easily:

  • docker ps -q : lists the running containers
  • docker ps -a -q : lists all the containers

Here are some common use cases for them:

# kill all running containers
docker kill $(docker ps -q)

# remove all running containers
docker rm $(docker ps -a -q)

# remove all images
docker rmi $(docker images -q)

Debugging a running application inside a container

One of the hardest things for me when I started to work with containers, was debugging, but after a while, I learned you could actually debug applications inside the container! It might take a while for the initial setup, but it's worth it. I will show how to do it in Node.js since it's the most straight forward, but I will link to a few other tutorials, and you should find one for your language of choice easily.

How to debug Node.js inside Docker container with Chrome

First, create a separate Dockerfile, and copy this:

FROM node:11.8
WORKDIR /usr/src/app
COPY . .

# expose two ports
# 9229 is the default node debugger port
EXPOSE 8000 9229

# run node in inspect mode
CMD node --inspect-brk=0.0.0.0 server.js

The --inspect-brk=0.0.0.0 flag tells node to run in debug mode, and the -brk part tells it to stop at the first line, the 0.0.0.0 part tells it to accept connections from any IP, since we are going to access it from the host IP and not from the container localhost.

We can now build the debug image, and then run it like this:

docker build -f debug.Dockerfile .

# make sure to use the image id we just built instead of "7b4bec4dc9b4"
docker run -it -p 8000:8000 -p 9229:9229 7b4bec4dc9b4

You should see a message like this in the terminal:

Debugger listening on ws://0.0.0.0:9229/88c7468f-...

Now you can open chrome, go to chrome://inspect and you should see a remote target named server.js. Clicking it will open a chrome debugger for us.

Debug other languages inside Docker

Most debuggers are capable of exposing a web API for debugging. Here is a list of some of the languages and how to debug them. You should be able to find documentation for other languages easily.

Debugging Go inside a container

Debugging Python inside a container

Debugging Elixir inside a container

What's next

Docker is a great tool, and it is massive. You should also take a look at other tools like Docker Swarm, Docker Compose or Kubernetes that increase the productivity and the ease of deployment of containers.

Summary

In this Docker tutorial for beginners, we went through the process of installing Docker, running our first container and dockerizing our first application. We've also demonstrated how to push your Docker images to Docker Hub. Let me know in the comments below how you liked this tutorial and please share it with whoever might find it useful.