Fun with Docker

Background

It has been long since the Docker buzzword has been around. On how it is different from Virtual Machines. On creating/downloading images and running them as containers. But I haven't got any opportunity to get my hands dirty with Docker. I gave it a try once, but had to stop it midway because of other priorities.

Recently while travelling in train, I heard a group of guys discussing about Docker. I realized that people use it commonly and I felt bad I don't know it yet. High time to learn it.

Learning Docker

Docker Image is like the blueprint of a context. The context can be an OS, build environment, development environment, runtime environment or a combination of the above. We can also bundle our code or data, together with the above contexts. Prebuilt images are available in the docker hub. Additionally we can create our own images and push it to the docker hub, which can be used by others.

Once we have the image, we need to run the image. A running image is referred to as a container. Docker needs to be installed in the machine to pull the image and run it. These actions can be done through the docker command-line Terminal(Docker QuickStart Terminal) or a GUI tool(Kitematic). I personally prefer the Terminal.

Initially I tried out downloading the hello world version of Nginx and ran it. Trying it out felt really nice in the sense that Nginx which usually runs on Linux was running in my Windows machine. Yaay!

Trying Docker with ASP.Net Core

Next step was to run an already existing application in Docker. As always I went ahead with my website, which was recently migrated to ASP.Net Core technology.

There are mainly 2 images for ASP.Net Core available from Microsoft:
  1. microsoft/aspnetcore: this is an image serving as the runtime for ASP.Net Core, which means we need to already have the binaries ready, which then can be run, using this image.
  2. microsoft/aspnetcore-build: this image helps building the ASP.Net Core project to produce the binaries.

Take 1: Running a Prebuilt Application using ASP.Net Core Runtime Image

First, I started with the simplest task. Build the binaries using Visual Studio and then run it using the ASP.Net Core runtime image. 

The project was published to the following path: "./bin/Debug/netcoreapp2.0/publish". This path is relative to the project root.

There needs to be a file named "Dockerfile" at the root of your application. This file will contain the commands to create the image. This is how the docker file looked like:

FROM microsoft/aspnetcore
COPY ./bin/Debug/netcoreapp2.0/publish .
ENTRYPOINT ["dotnet", "CodingSoldier.dll"]

Base image is "microsoft/aspnetcore". Now I need to create the image from the published output and "microsoft/aspnetcore" image into a single image "codingsoldierwebapp" and then run that image.
I start the "Docker QuickStart Terminal" and then run the following commands:

$ docker build -t codingsoldierwebapp .
$ docker run -d -p 8080:2350 codingsoldierwebapp

Here -p is the port mapping. Container running on port 2350 is mapped to port 8080 of the Docker machine. Now that the container is running, we can access the site, by hitting the URL:
http:{DockerMachine IP}:8080


Take 2: Building and Running a Website from the Source Code

In "Take 1", we already had the binaries built of the source code. But here we will need to first build the binaries and then run it against the Runtime image.
This is how the Docker file looked in this case:

FROM microsoft/aspnetcore-build:2.0 AS build-env
WORKDIR /app

# Copy csproj and restore as distinct layers
COPY *.csproj ./
RUN dotnet restore

# Copy everything else and build
COPY . ./
RUN dotnet publish -c Release -o out

# Build runtime image
FROM microsoft/aspnetcore:2.0
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "CodingSoldier.dll"]

Now create and run the image, based on the above commands:

$ docker build -t codingsoldierwebapp .
$ docker run -d -p 8080:2350 --name myapp codingsoldierwebapp

Website can be accessed through this URL:  http:{DockerMachine IP}:8080

Take 3: Website + Nginx in the same container

Leveling up more. Here we will create an image which will run the website on Kestrel server as usual, but use Nginx as the Revers Proxy before Kestrel.

Image consists of:

  1. The binaries of the website(built from the website source code, using the image "microsoft/aspnetcore-build").
  2. ASP.Net Core runtime image "microsoft/aspnetcore". The binaries are configured to run against this image.
  3. Nginx image, which will be ran and used as Reverse Proxy in front of Kestrel(default web server for ASP.Net Core)

Kestrel is configured to listen on Port 2350. Nginx listens on port 80, and forwards the requests to port 2350 listened by Kestrel.
When we run the image, we need to specify the port on the host, to which the outside world can connect to. In the example below, port 8080 is mapped to port 80 on Nginx. And port 8080 is accessible to everyone outside of the host.

user -> 8080(exposed port) -> 80(Nginx) -> 2350(Kestrel)

DockerFile content:

#aspnet core build image
FROM microsoft/aspnetcore-build:2.0 AS build-env
WORKDIR /app
# Copy csproj and restore as distinct layers
COPY *.csproj ./
RUN dotnet restore
# Copy everything else and build
COPY . ./
RUN dotnet publish -c Release -o out

#aspnet core runtime image
FROM microsoft/aspnetcore:2.0
WORKDIR /app

#nginx install
RUN apt-get update
RUN apt-get install -y nginx
#copy startup.sh with correct permissions
COPY ./startup.sh .
RUN chmod 755 /app/startup.sh
#copy nginx.conf
RUN rm /etc/nginx/nginx.conf
COPY nginx.conf /etc/nginx
#expose 2350 - kestrel will listen on this port
#expose 80 - nginx will listen on this port.
#Both ports are internal to the container. 
EXPOSE 2350 80

#copying output of "build-env" from "/app/out" to "."
COPY --from=build-env /app/out .
CMD ["sh", "/app/startup.sh"]

nginx.conf content:

worker_processes 4;

events { worker_connections 1024; }

http {
sendfile on;

upstream app_servers {
server 127.0.0.1:2350;
}

server {
listen 80;
location / {
proxy_pass http://app_servers;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
}
}

startup.sh content:

#!/bin/bash
service nginx start
dotnet /app/CodingSoldier.dll


Now as usual we need to run the commands to first build the image and then run that image:
$ docker build -t codingsoldier-with-nginx .
$ docker run -p 8080:80 codingsoldier-with-nginx

Website can be accessed through this URL:  http:{DockerMachine IP}:8080

Publishing the image to Docker Hub

Now that we have the image in our local, we have the option of publishing the image to docker hub. First we need to create an account in Docker Hub and then login using that account into the QuickStart Terminal. Once we login, we can publish the image to Docker Hub.

$ export DOCKER_ID_USER="yourDockerId"
$ docker login
$ docker tag codingsoldier-with-nginx $DOCKER_ID_USER/codingsoldier-with-nginx
$ docker push $DOCKER_ID_USER/codingsoldier-with-nginx

I pushed this particular image to Docker Hub and is available there for everyone to download:
https://hub.docker.com/r/bnyjohns/codingsoldier-with-nginx/

Important point to note:
If you are trying this out in Windows environment, beware of CRLF issues in startup.sh and nginx.conf files. It is better to create those files using Vim in Linux, instead of copy pasting the content from this web page. The script file may break because of encoding issues between Linux and Windows environments.


Take 4 (Last Take): Website + Nginx in seperate containers.

Leveling up again. It is similar to take 3, except that we have the Webserver and Nginx running in seperate containers and communication happens between those containers.

Here we will need to use "Docker compose" commmand where we compose different containers, from different docker files specified in the docker compose file.
In my example, "Dockerfile" and "NginxDockerfile" are the 2 docker files. And "docker-compose.yml" is the docker compose file.

docker-compose.yml content:

version: '3'

services:
  website:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
    expose:
      - "2350"
  reverseproxy:
    build:
      context: .
      dockerfile: NginxDockerfile
    links:
      - website
    ports:
      - "8080:80"


DockerFile content:

#aspnet core build image
FROM microsoft/aspnetcore-build:2.0 AS build-env
WORKDIR /app
#Copy csproj and restore as distinct layers
COPY *.csproj ./
RUN dotnet restore

#Copy everything else and build
COPY . ./
RUN dotnet publish -c Release -o out

#aspnet core runtime image
FROM microsoft/aspnetcore:2.0
WORKDIR /app

EXPOSE 2350

#copying output of "build-env" from "/app/out" to "."
COPY --from=build-env /app/out .
CMD ["dotnet", "/app/CodingSoldier.dll"]

NginxDockerFile content:

FROM nginx
COPY nginx.conf /etc/nginx/nginx.conf

Nginx.conf content:

worker_processes 4;

events { worker_connections 1024; }

http {
sendfile on;

upstream app_servers {
server website:2350;
}

server {
listen 80;
location / {
proxy_pass http://app_servers;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
}

}

Now the commands to start the containers using docker compose:

$ docker-compose build
$ docker-compose up

Logs that you would see after running the "docker-compose" command:
$ docker-compose up
Starting codingsoldier_website_1 ...
Starting codingsoldier_website_1 ... done
Starting codingsoldier_reverseproxy_1 ...
Starting codingsoldier_reverseproxy_1 ... done

Attaching to codingsoldier_website_1, codingsoldier_reverseproxy_1

Website can be accessed through this URL:  http:{DockerMachine IP}:8080

Conclusion

It was challenging trying out docker, running into various issues(especially the encoding issue that was breaking the Linux scripts). But I got through finally and  the learning was huge. Learned lot more stuff on Linux and understood the advantages of Docker. It is a great tool to have under your belt. As I mentioned in the article, I even published an image to Docker Hub. All together it was a wonderful experience learning Docker.

References

  1. https://docs.docker.com/engine/examples/dotnetcore
  2. https://www.sep.com/sep-blog/2017/02/20/hosting-asp-net-core-docker/





Comments

Popular posts from this blog

My First Full Marathon

My Second Full Marathon

ASP.Net Core saves me $4 a month!