/ DOCKER , CSHARP , SQL

Docker compose introduction - Dotnet core app composed with MySQL Database

Introduction to docker-compose - basic aspects explained with .NET Core app and MySQL DB composed together.

If you are familiar with basics of creating Docker images, now it’s time to learn how to compose more than one Docker container together. Once again we’ll learn with .NET Core 2.2 example app, but you should be able to understand Docker compose concepts here, even if you’re not familiar with C# language.

Whole solution is available in SoftwareDeveloperBlog repo on GitHub.

Docker-compose file

Heart of Docker composing is one text file (in yaml format).
By default it’s named docker-compose.yml (or docker-compose.yaml if you prefer), but unlike Dockerfile this name can be changed and specified as command parameter.

Where to keep docker-compose file?

Short answer - it doesn’t matter, put it where it’s convenient for you.
Long answer - some people advice to put it along with deployment scripts, for example in your CI pipeline, because composing is strictly related with deploying. Moreover, this way you can compose images from different repositories.
Where I put docker-compose.yml file? So far I’ve never containerized system spread across multiple repositories, so I put it along with sources, to have it under version control, next to solution file (.sln) - to manage it from the highest perspective (above project directories). Usually I also create solution directory called Docker, referencing docker-compose and .dockerignore files, to be able to edit it from my IDE.

Docker-compose file example

In our example we will use following docker-compose.yml file, kept as I usually do, next to solution file:

version: '3'

services:
  db:
    build: ./Db
  app:
    build:
      context: .
      dockerfile: Aspnetcoreapp/Dockerfile
    ports:
      - 8080:80
    depends_on:
      - db

First we specify docker-compose file format version. I just took the newest version, I don’t see any reasons to take older ones while creating new system.

Then we specify our services. We could specify other aspects, for example networks, volumes, configs, but it’s too wide for this introduction.

Every service has its name, and this service name is important, because we will use it to resolve container ip address and to refer it from other service (for example to specify dependencies).

Build parameter can just point to directory with Dockerfile (like in db service in example above) - then this directory is also taken as build context. If you want to use different directory for build context, than the one with Dockerfile inside, you can separate context and dockerfile options, like in app service above. If you wonder why to do that, I explained choosing build context in previous post.

Specify ports to bind port inside container with one from host machine, like in usual Docker run command.

Lastly we specify that our app depends on db, with depends_on option, taking service name as value. This way we determine services start and stop order (and few other things, not mentioned here for simplification).

Docker-compose commands

Managing composed system is easy, you don’t need to remember many commands and parameters.

Docker-compose up

From directory with docker-compose.yml file you can just run docker-compose up command. It will run containers in order forced by depends_on configuration (if specified). By default images are build only when you run docker-compose up command for the first time, so if you have changed something, you should extend it with --build parameter. If you want to run it from different location then the one with docker-compose.yml file, or if you named compose file not default way, or if you want to pass more than one compose file at once, pass -f FILE_PATH parameter. Last parameter which I use often is -d to detach console from containers. There are other commands to start composed containers, but usually we use this one, so I don’t mention them in this introduction. If you’re curious, they are described well in official documentation. Usually I run docker-compose up command this way:

docker-compose up -d --build

Docker-compose down

If you want to stop composed containers, remove them and theirs networks, just type docker-compose down from directory with docker-compose.yml file. If you want just stop containers without cleaning everything, use docker-compose stop command. Note, that if you have stopped containers with CTRL+C shortcut in attached terminal, you didn’t clean stuff, you just stopped containers and finally will need to run docker-compose down as usual.

Example system

Now with mandatory knowledge, lets create example system. As mentioned in introduction, whole repo is available on GitHub.

.NET Core app

First we create simple ASP.NET Core application. Small startup configuration:

public class Startup
{
    public Startup(IConfiguration configuration) => Configuration = configuration;

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment()) 
            app.UseDeveloperExceptionPage();

        app.UseMvc();
    }
}

Program entry point:

public static class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

Controller with one GET method:

[Route("")]
public class ProductsController : ControllerBase
{
    private readonly ProductsProvider _provider = new ProductsProvider();

    [HttpGet]
    public ActionResult<IEnumerable<Product>> Get()
    {
        try
        {
            return _provider.GetAll();
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception during providing products, maybe DB is not fully initialized yet? " +
                              $"Try again in a few minutes and if it doesn't help, check your docker-compose configuration.\n{e}");
            
            return new Product[0];
        }
    }
}

Controller is coupled with Product type:

public class Product
{
    public int Id { get; }
    public string Name { get; }
    public string Description { get; }
    
    public Product(int id, string name, string description)
    {
        Id = id;
        Name = name;
        Description = description;
    }
}

Controller is also coupled with ProductsProvider class, which uses Dapper to access MySQL DB. Note, that we provide docker-compose service name as DB address (as mentioned earlier - that’s why service name is important). Be careful not to make typo - service name is case-sensitive.

public class ProductsProvider
{
    private const string CONN_STRING = "Server=db;Port=3306;Database=product-db;Uid=root; Pwd=myPass;";
    private const string QUERY = "SELECT Id, Name, Description FROM product";
    
    public Product[] GetAll()
    {
        using (var connection = new MySqlConnection(CONN_STRING))
        {
            return connection.Query<Product>(QUERY).ToArray();
        }
    }
}

Dockerfile is written following Docker image from multi project .NET Core solution post. Note that Aspnetcoreapp is our project name, and project directory name at the same time:

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

COPY . ./
RUN dotnet publish Aspnetcoreapp -c Release -o out

FROM mcr.microsoft.com/dotnet/core/aspnet:2.2
WORKDIR /app
COPY --from=build-env /app/Aspnetcoreapp/out .

ENTRYPOINT ["dotnet", "Aspnetcoreapp.dll"]

MySQL DB

We could point DB image in docker-compose.yml file and configure environment variables and so on there, but I prefer to configure service via Dockerfile. So we just create new directory with Dockerfile. It can be located anywhere, but I prefer the same level as C# project. So I just create new Db directory, next to project directory (Aspnetcoreapp). If you want to see this Dockerfile in IDE, you can reference it in solution directory.

FROM mysql:5.7.13
COPY *.sql /docker-entrypoint-initdb.d

ENV MYSQL_ROOT_PASSWORD myPass

EXPOSE 3306

With above Dockerfile we do several things:

Next we need some .sql script located next to Dockerfile to initialize our database. We use *.sql wildcard, so name doesn’t matter, it just need to end up with .sql extension - for example Init.sql:

CREATE DATABASE `product-db` /*!40100 COLLATE 'latin1_swedish_ci' */;

USE `product-db`;

CREATE TABLE `product` (
	`Id` INT NOT NULL AUTO_INCREMENT,
	`Name` TEXT NOT NULL,
	`Description` TEXT NOT NULL,
	INDEX `Id` (`Id`)
)
COLLATE='latin1_swedish_ci';

INSERT IGNORE INTO product (Id, Name, Description)
VALUES 
(1, "Dependency Injection Principles, Practices, and Patterns", "Book by Steven van Deursen and Mark Seemann"),
(2, "Agile Software Development, Principles, Patterns, and Practices", "Book by Robert C. Martin"); 

Docker-compose.yml and .dockerignore

We take docker-compose.yml file described earlier in this article:

version: '3'

services:
  db:
    build: ./Db
  app:
    build:
      context: .
      dockerfile: Aspnetcoreapp/Dockerfile
    ports:
      - 8080:80
    depends_on:
      - db

Very last thing - .dockerignore file located next to solution (.sln) file. If you don’t know what is this file or why it has to be in this location, I’ve explained it in previous post. Mine currently looks like this:

.dockerignore
.env
.git
.gitignore
.vs
.vscode
*/bin
*/obj
**/.toolstarget
.idea

Running composed system

To run this example system, just go to solution directory, type docker-compose up command (explained earlier) and after a moment (DB initialization takes a while) you can open browser and see data pulled from DB with http://localhost:8080 GET method: Browser with products taken from DB seen as JSON array

Persisting DB data

As you know, killed container is killed along with the data, which of course is not what we want from production database. To persist changes which we introduced to DB during production runtime, we need to use volumes, but I thought that it’s too much for simple introduction article. If you succeeded previous steps, now you can read about docker volumes. Maybe some day I will explain it in whole different post.

Summary

As you see, composing in general is simple, and basically requires one text file which instructs Docker how to run and couple containers together.

I hope you find this introduction useful. If there is something not clear, ask me a question in comment, and if you think it can help other too, share this article on Facebook or Twitter :)

tometchy

Tometchy

Passionate focused on agile software development and decentralized systems

Read More