Docker
Adding Docker Support
Docker is an essential part of any architecture involving a large number of entities. Plugins have the ability to provide Docker Compose services so that users can spin up the entire infrastructure with a single command.
Let's consider an example from aspnet-ddd:
public class AspNetFramework : IFrameworkTemplatePlugin, IDockerCompatible
{
public IEnumerable<DockerComposeService> GetComposeServices(string projectName, IScaffolderEntity? entity, string entityName, int dockerServiceIndex)
{}
}
Firstly, we need to add the IDockerCompatible interface, which allows us to implement the GetComposeServices method. Let's take a closer look at its parameters:
- ProjectName - The name of the project.
- IScaffolderEntity - A class representing our entity in Scaffolder (ScaffolderService, ScaffolderDashboard, ScaffolderGlobalWorker).
- EntityName - The name of the entity. The rules are identical to the Template parameter "entity_name".
- DockerServiceIndex - The index of the Docker service. This is needed to calculate ports. If you add more than one service, don't forget to multiply the offset by the number of services.
The method itself returns a list of DockerComposeService. Let's look at a simplified example from aspnet-ddd:
public IEnumerable<DockerComposeService> GetComposeServices(string projectName, IScaffolderEntity? entity, string serviceName, int dockerServiceIndex)
{
// Declare an empty list of services to return.
List<DockerComposeService> dockerComposeServices = new();
// Calculate port offset based on the dockerServiceIndex.
int portOffset = dockerServiceIndex * 2;
// Calculate ports for the service and database.
int servicePort = 5000 + portOffset;
int databasePort = 5001 + portOffset;
// Declare the database service.
DockerComposeService databaseService = new DockerComposeService
{
// Specify the type of our Compose service.
Type = DockerComposeServiceType.Database,
// Omitted for brevity...
};
// Declare the DockerComposeService for our Framework.
DockerComposeService frameworkService = new DockerComposeService
{
// Specify the type of our Compose service.
Type = DockerComposeServiceType.Framework,
// Fill in the rest of the fields as we would in a regular Docker Compose file.
Build = new ComposeBuild { Context = $"./{projectName}.{serviceName}/", Dockerfile = "Dockerfile.dev" },
ContainerName = $"{serviceName.ToLower()}-service",
Dependencies = new Dictionary<string, ComposeDependency>
{
{ databaseService.ContainerName!, new ComposeDependency { Condition = "service_healthy" } }
},
EnvironmentVariables = new Dictionary<string, string>
{
{ "ASPNETCORE_ENVIRONMENT", "Development" },
{ "ASPNETCORE_URLS", $"http://+:{servicePort}" },
{ "CONNECTION_STRING_DEVELOPMENT", $"Host={databaseService.ContainerName};Port={databasePort};Database={serviceName}Database;Username={serviceName}Login;Password={serviceName}Password;IncludeErrorDetail=true;" }
},
Ports = new Dictionary<int, int?>
{
{ servicePort, servicePort }
}
};
// Add services to the list.
dockerComposeServices.Add(databaseService);
dockerComposeServices.Add(frameworkService);
// Return the list of services.
return dockerComposeServices;
}
It's important not to forget to specify the correct Type of your Compose service so that other entities can iterate and find them easily. Otherwise, the process of declaring a Compose service is identical to writing it in a regular Docker-Compose file, but now it's done inside the DockerComposeService class.
After this, your plugin will start providing a list of services that Scaffolder will include and add to the final docker-compose.dev.yml file. Congratulations!
Generation Order
If any of your plugins need to iterate Compose services during generation for their needs, as is the case, for example, with the Global Worker grafana-prometheus:
{{~ for compose_service in compose_services ~}}
{{~ if compose_service.type == "Framework" ~}}
{{~ if compose_service.linked_entity.flags["Metrics"] == "prometheus" ~}}
- job_name: '{{ compose_service.container_name }}'
static_configs:
{{~ for port in compose_service.ports ~}}
- targets: ["{{ compose_service.container_name }}:{{ port.key }}"]
{{~ end ~}}
{{~ end ~}}
{{~ end ~}}
{{~ end ~}}
It's very important to consider the generation order so that you don't find yourself in a state of confusion if something goes wrong:
Services (Framework plugins) -> Dashboard -> Global Workers
The generation order of Framework plugins goes from top to bottom, as you specify it in the configuration file.