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:
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:
- Specify which version of MySQL image to use
- Set root user password to myPass
- Expose 3306 port to be able to access it from our app
- Initialize database, by copying SQL scripts to /docker-entrypoint-initdb.d directory inside DB container, because official MySQL image description tells us, that scripts from this directory are invoked when DB is started for the first time.
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:
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 :)
Stay up to date!
Get all the latest & greatest posts
delivered straight to your inbox