Docker

My foray into the world of containerization

Posted by Rodney Hartzell on 2020-11-07
Estimated Reading Time 10 Minutes
Words 1.8k In Total
Viewed Times

Learning Docker

The following blog post is just a collection of fairly random efforts that I put in when first encountering docker, and then later diving in with serious development effort.
So I apologize in advance for the “Stream of consciousness” method of writing here.

First step: install docker on MacOS. Open up a terminal zsh, bash or whatever. Then type:

1
brew cask install docker

If you need help installing brew see install brew on MacOs.

Next step: fire up this docker tutorial in a self contained container.

1
docker run -dp 80:80 docker/getting-started

The tutorial is now running here

What is a container?

Now that you’ve run a container, what is a container? Simply put, a container is simply another process on your machine that has been isolated from all other processes on the host machine. That isolation leverages kernel namespaces and cgroups, features that have been in Linux for a long time. Docker has worked to make these capabilities approachable and easy to use.

What is a container image?

When running a container, it uses an isolated filesystem. This custom filesystem is provided by a container image. Since the image contains the container’s filesystem, it must contain everything needed to run an application - all dependencies, configuration, scripts, binaries, etc. The image also contains other configuration for the container, such as environment variables, a default command to run, and other metadata.

We’ll dive deeper into images later on, covering topics such as layering, best practices, and more.

Building the App’s Container Image

This section is regarding a piece in the getting started tutorial where we install a simple todo app in a container image and run it. In order to build the application, we need to use a Dockerfile. A Dockerfile is simply a text-based script of instructions that is used to create a container image. If you’ve created Dockerfiles before, you might see a few flaws in the Dockerfile below. But, don’t worry! We’ll go over them.

Create a file named Dockerfile in the same folder as the file package.json with the following contents.

1
2
3
4
5
FROM node:12-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]

Please check that the file Dockerfile has no file extension like .txt. Some editors may append this file extension automatically and this would result in an error in the next step.

If you haven’t already done so, open a terminal and go to the app directory with the Dockerfile. Now build the container image using the docker build command.

docker build -t getting-started .

This command used the Dockerfile to build a new container image. You might have noticed that a lot of “layers” were downloaded. This is because we instructed the builder that we wanted to start from the node:12-alpine image. But, since we didn’t have that on our machine, that image needed to be downloaded.

After the image was downloaded, we copied in our application and used yarn to install our application’s dependencies. The CMD directive specifies the default command to run when starting a container from this image.

Finally, the -t flag tags our image. Think of this simply as a human-readable name for the final image. Since we named the image getting-started, we can refer to that image when we run a container.

The . at the end of the docker build command tells that Docker should look for the Dockerfile in the current directory.

Starting an App Container

Now that we have an image, let’s run the application! To do so, we will use the docker run command (remember that from earlier?).

Start your container using the docker run command and specify the name of the image we just created:

docker run -dp 3000:3000 getting-started

Remember the -d and -p flags? We’re running the new container in “detached” mode (in the background) and creating a mapping between the host’s port 3000 to the container’s port 3000. Without the port mapping, we wouldn’t be able to access the application.

After a few seconds, open your web browser to http://localhost:3000. You should see our app!

Empty Todo List

Go ahead and add an item or two and see that it works as you expect. You can mark items as complete and remove items. Your frontend is successfully storing items in the backend! Pretty quick and easy, huh?

At this point, you should have a running todo list manager with a few items, all built by you! Now, let’s make a few changes and learn about managing our containers.

If you take a quick look at the Docker Dashboard, you should see your two containers running now (this tutorial and your freshly launched app container)!

Docker Dashboard with tutorial and app containers running

Tons of interesting and useful Docker commands

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
docker build -t myProject-repository-api .
docker build -f myProject-repository-api/Dockerfile -t myProject-repository-api .
docker run -dp 5006:5006 myProject-repository-api
docker run -dp 5006:80 myProject-repository-api
docker ps
docker stop 795a2e4818cc
docker exec -it 314383f31507 mysql -p
docker stop 314383f31507
docker-compose up -d
docker network ls
docker network inspect --format "{{ json .Containers }}" bridge
docker network inspect
docker network inspect app_default
docker network inspect bridge
docker network inspect host

Take look at the layers in a Docker image

1
docker history myProject-repository-api

It should produce output like this

1
2
3
4
5
6
7
8
9
10
11
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
ba79b4f8b258 17 minutes ago /bin/sh -c #(nop) ENTRYPOINT ["dotnet" "myProject… 0B
ef6cb768d92a 17 minutes ago /bin/sh -c #(nop) EXPOSE 1521 0B
ec8590a7460f 17 minutes ago /bin/sh -c #(nop) COPY dir:90ece92f4f478ccb6… 8.61MB
875d7f707e91 14 hours ago /bin/sh -c #(nop) WORKDIR /app 0B
89fbff0a51f0 7 months ago /bin/sh -c aspnetcore_version=3.1.4 && w… 17.8MB
<missing> 7 months ago /bin/sh -c dotnet_version=3.1.4 && wget … 77.3MB
<missing> 7 months ago /bin/sh -c #(nop) ENV ASPNETCORE_URLS=http:… 0B
<missing> 7 months ago /bin/sh -c apk add --no-cache ca-certifi… 4.08MB
<missing> 7 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 7 months ago /bin/sh -c #(nop) ADD file:66a440394c2442570… 5.58MB

Attach and Detach

Attach using this command

1
docker attach <container name or sha>

Detach without shutting down the container using
ctrl p + ctrl q

A Dockerfile with dotnet core on a linux Alpine image

I attempted to layer the myProject-repository-api onto this dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ARG VERSION=3.1-alpine3.10
FROM mcr.microsoft.com/dotnet/core/sdk:$VERSION AS build-env
WORKDIR /app

COPY . ./
RUN dotnet publish myProject-repository-api.csproj -c Release -o myProject-repository-api/out

FROM mcr.microsoft.com/dotnet/core/aspnet:$VERSION
RUN adduser \
--disabled-password \
--home /app \
--gecos '' app \
&& chown -R app /app
USER app
WORKDIR /app

COPY --from=build-env /app/myProject-repository-api/out .
ENV DOTNET_RUNNING_IN_CONTAINER=true \
ASPNETCORE_URLS=http://+:5006
EXPOSE 5006
ENTRYPOINT ["dotnet", "myProject-repository-api.dll"]

It didn’t quite work. What I ended up using was this:

1
2
3
4
5
6
7
8
9
10
11
12
13
ARG VERSION=3.1-alpine3.10

FROM mcr.microsoft.com/dotnet/core/sdk:$VERSION AS build-env
WORKDIR /app

COPY . ./
RUN dotnet publish myProject-repository-api/myProject-repository-api.csproj -c Release /p:EnvironmentName=qa -o myProject-repository-api/out

FROM mcr.microsoft.com/dotnet/core/aspnet:$VERSION
WORKDIR /app
COPY --from=build-env /app/myProject-repository-api/out .
EXPOSE 1521
ENTRYPOINT ["dotnet", "myProject-repository-api.dll"]

But, the problem I still have with this Dockerfile is that the /p:EnvironmentName=qa switch in the publish command, doesn’t work. I had to manually attach to
the running container and swap out the appsettings.json file with the appsettings.qa.json file. But I at least was able to open the Oracle port 1521 and
access the myProject database.

Wait a minute I have the anwer!!!

1
ENTRYPOINT ["dotnet", "myProject-repository-api.dll", "--environment=qa"]

And there you have it. Learning as I go with this stuff.

Docker Compose

This will run the todo app as two containers. One with the app and the other for the mySql database.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
version: "3.7"
services:
app:
image: node:12-alpine
command: sh -c "yarn install && yarn run dev"
ports:
- 3000:3000
working_dir: /app
volumes:
- ./:/app
environment:
MYSQL_HOST: mysql
MYSQL_USER: root
MYSQL_PASSWORD: secret
MYSQL_DB: todos

mysql:
image: mysql:5.7
volumes:
- todo-mysql-data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: todos

volumes:
todo-mysql-data:

Then execute docker-compose as follows:

1
docker-compose up -d

After docker-compose spins it up you can exec into the mySql container and run mySql commands. It’s pretty cool IMHO.

1
2
3
4
5
6
7
cd /Users/rhartzell/Downloads/app
docker-compose up -d
docker ps
app docker exec -it 7f559ce83587 mysql -p
MYSQL> show databases;
MYSQL> use todos;
MYSQL> select * from todo_items;

Running myProject’s API services in Docker

The following is a quick explanation of how to get both the web-api and the repository-api for myProject, up and running in a docker container.

Currently the Docker files for both APIs are built and will execute with --environment=qa context. If you want to run these APIs in a different environment
context, simply update the last line of the Docker file.

1
ENTRYPOINT ["dotnet", "myProject-web-api.dll", "--environment=qa"]

Anyway. Back to the instructions.

Start by pulling latest for myProject

1
2
3
cd myProject
git checkout master
git pull

Then Simply run the docker build commands to build latest

1
2
docker build -t myProject-web-api -f DockerfileWebApi .
docker build -t myProject-repository-api -f DockerfileRepoApi .

Then run them.

The following commands will ensure that both web and repo apis will run in their respective sockets.

1
2
docker run --name repoapi -dp 5006:80 myProject-repository-api --network myProject_api_network --net=host
docker run --name webapi --link repoapi -dp 6100:80 myProject-web-api --network myProject_api_network --net=host

To make sure the images are running

Notice how the docker ps command shows which ports are open in the container and which port the container default port is listening on. Oracle uses port 1521.

1
2
3
4
5
➜  myProject git:(master) docker ps

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2dbe83d28208 myProject-repository-api "dotnet myProject-reposito…" 4 seconds ago Up 2 seconds 1521/tcp, 0.0.0.0:5006->80/tcp bold_lehmann
c4f7c8405751 myProject-web-api "dotnet myProject-web-api.…" 3 minutes ago Up 3 minutes 1521/tcp, 0.0.0.0:6100->80/tcp bold_roentgen

Now both APIs are running.

Use a browser to see the Swagger UI for the APIs


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them. Thank you !