在前面一篇文章中,演示了一个ASP.NET Core MVC应用程序,这个程序使用了MySQL,负载均衡,涉及到要创建多个容器,Volume卷以及自定义网络。这种纯手工的方式很容易出错,不仅每一个命令必须输入正确,并且每个步骤还要按照先后顺序来输入,比如再创建MySQL容器的时候,必须事先把其依赖的卷创建好。如果漏了某个步骤,或者某个步骤顺序发送错误,则程序就运行不起来。如果应用程序架构比较简单,这种问题不大,但当应用程序设计模块比较多时,可能会想到自己编写一些脚本来实现自动化,而Docker为我们提供了docker-compose功能,利用这以功能可以实现对复杂应用的管理,包括容器,Volume,自定义网络中。
准备工作
在开始之前,我们将之前创建的容器,网络和Volume都删掉,这些我们在后续都可以通过docker-compose来创建。
docker rm -f $(docker ps -aq)
docker network rm $(docker network ls -q)
docker volume rm $(docker volume ls -q)
这里还是在上一篇的代码基础上修改,先执行如下命令,创建名为DockerCompose的项目:
dotnet new mvc --no-https --output DockerCompose --framework net5.0
内容从上一节里的VolumeAndNetwork复制过来,然后修改HomeController下面的信息:
using Microsoft.AspNetCore.Mvc;
using DockerCompose.Models;
using Microsoft.Extensions.Configuration;
namespace DockerCompose.Controllers
{
public class HomeController : Controller
{
private IRepository repository;
private string message;
public HomeController(IRepository repo, IConfiguration config)
{
repository = repo;
message = $"Essential Docker ({config["HOSTNAME"]})";
}
public IActionResult Index()
{
ViewBag.Message = message;
return View(repository.Products);
}
}
}
安装docker-compose
如果是在Windows上安装的Docker Desktop,则会预先安装有docker-compose,检查是否安装可以运行 docker-compose --version命令查看:
PS D:\Study\DockStudyInVSCode\DockerCompose> docker-compose --version
docker-compose version 1.29.0, build 07737305
如果没有安装,可以执行如下命令(最新版本的docker-compose可以从这里https://github.com/docker/compose/releases获知):
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
创建docker-compose文件
在DockerCompose文件夹下,新建docker-compose.yml文件,内容如下:
version: "3"
volumes:
productdata:
networks:
frontend:
backend:
services:
mysql:
image:"mysql:8.0.23"
volumes:
- productdata:/var/lib/mysql
networks:
- backend
environment:
- MYSQL_ROOT_PASSWORD=mysecret
- bind-address=0.0.0.0
- version,定义yml的版本号
- volumes,自定义卷,用于在Docker容器之外存放数据
- networks,自定义网络
- services,定义需要创建的服务,用来创建容器。这里只定义了mysql,下面的参数跟前一篇创建mysql容器时一样。
然后执行 docker-compose -f docker-compose.yml build
PS D:\Study\DockStudyInVSCode\DockerCompose> docker-compose -f docker-compose.yml build
WARNING: Some networks were defined but are not used by any service: frontend
mysql uses an image, skipping
也可以直接使用docker-compose build命令,他会寻找当前文件夹下的默认docker-compose.yml文件。这里的警告信息意思是我们定义的一个frontend网络但还没被任何服务组件使用。
运行了这个命令只是定义,并没有真正的创建volume、network或者容器。
接下来,运行docker-compose.yml定义的应用,才会真正创建内容,输入一下命令:
PS D:\Study\DockStudyInVSCode\DockerCompose> docker-compose -f docker-compose.yml up
WARNING: Some networks were defined but are not used by any service: frontend
Docker Compose is now in the Docker CLI, try `docker compose up`
Creating network "dockercompose_backend" with the default driver
Creating volume "dockercompose_productdata" with default driver
Pulling mysql (mysql:8.0.23)...
8.0.23: Pulling from library/mysql
f7ec5a41d630: Already exists
9444bb562699: Pull complete
6a4207b96940: Pull complete
181cefd361ce: Pull complete
8a2090759d8a: Pull complete
15f235e0d7ee: Pull complete
d870539cd9db: Pull complete
5726073179b6: Pull complete
eadfac8b2520: Pull complete
f5936a8c3f2b: Pull complete
cca8ee89e625: Pull complete
6c79df02586a: Pull complete
Digest: sha256:6e0014cdd88092545557dee5e9eb7e1a3c84c9a14ad2418d5f2231e930967a38
Status: Downloaded newer image for mysql:8.0.23
Creating dockercompose_mysql_1 ... done
Attaching to dockercompose_mysql_1
mysql_1 | 2021-04-22 06:05:09+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.23-1debian10 started.
mysql_1 | 2021-04-22 06:05:09+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'
mysql_1 | 2021-04-22 06:05:09+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.23-1debian10 started.
mysql_1 | 2021-04-22 06:05:09+00:00 [Note] [Entrypoint]: Initializing database files
mysql_1 | 2021-04-22T06:05:09.521249Z 0 [System] [MY-013169] [Server] /usr/sbin/mysqld (mysqld 8.0.23) initializing of server in progress as process 43
mysql_1 | 2021-04-22T06:05:09.530123Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
mysql_1 | 2021-04-22T06:05:21.108452Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
mysql_1 | 2021-04-22T06:06:03.565943Z 6 [Warning] [MY-010453] [Server] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.
mysql_1 | 2021-04-22 06:07:13+00:00 [Note] [Entrypoint]: Database files initialized
mysql_1 | 2021-04-22 06:07:13+00:00 [Note] [Entrypoint]: Starting temporary server
mysql_1 | 2021-04-22T06:07:13.538744Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.23) starting as process 88
mysql_1 | 2021-04-22T06:07:13.599098Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
mysql_1 | 2021-04-22T06:07:14.576855Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
mysql_1 | 2021-04-22T06:07:14.899118Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Socket: /var/run/mysqld/mysqlx.sock
mysql_1 | 2021-04-22T06:07:15.789867Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
mysql_1 | 2021-04-22T06:07:15.790154Z 0 [System] [MY-013602] [Server] Channel mysql_main configured to support TLS. Encrypted connections are now supported for this channel.
mysql_1 | 2021-04-22T06:07:15.963796Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
mysql_1 | 2021-04-22T06:07:15.983715Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.23' socket: '/var/run/mysqld/mysqld.sock' port: 0 MySQL Community Server - GPL.
mysql_1 | 2021-04-22 06:07:15+00:00 [Note] [Entrypoint]: Temporary server started.
mysql_1 | Warning: Unable to load '/usr/share/zoneinfo/iso3166.tab' as time zone. Skipping it.
mysql_1 | Warning: Unable to load '/usr/share/zoneinfo/leap-seconds.list' as time zone. Skipping it.
mysql_1 | Warning: Unable to load '/usr/share/zoneinfo/zone.tab' as time zone. Skipping it.
mysql_1 | Warning: Unable to load '/usr/share/zoneinfo/zone1970.tab' as time zone. Skipping it.
mysql_1 |
mysql_1 | 2021-04-22 06:07:28+00:00 [Note] [Entrypoint]: Stopping temporary server
mysql_1 | 2021-04-22T06:07:28.394283Z 10 [System] [MY-013172] [Server] Received SHUTDOWN from user root. Shutting down mysqld (Version: 8.0.23).
mysql_1 | 2021-04-22T06:07:48.442075Z 0 [System] [MY-010910] [Server] /usr/sbin/mysqld: Shutdown complete (mysqld 8.0.23) MySQL Community Server - GPL.
mysql_1 | 2021-04-22 06:07:49+00:00 [Note] [Entrypoint]: Temporary server stopped
mysql_1 |
mysql_1 | 2021-04-22 06:07:49+00:00 [Note] [Entrypoint]: MySQL init process done. Ready for start up.
mysql_1 |
mysql_1 | 2021-04-22T06:07:49.641926Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.23) starting as process 1
mysql_1 | 2021-04-22T06:07:49.694335Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
mysql_1 | 2021-04-22T06:07:50.524989Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
mysql_1 | 2021-04-22T06:07:50.768158Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Bind-address: '::' port: 33060, socket: /var/run/mysqld/mysqlx.sock
mysql_1 | 2021-04-22T06:07:51.306519Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
mysql_1 | 2021-04-22T06:07:51.306889Z 0 [System] [MY-013602] [Server] Channel mysql_main configured to support TLS. Encrypted connections are now supported for this channel.
mysql_1 | 2021-04-22T06:07:51.363234Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
mysql_1 | 2021-04-22T06:07:51.385233Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.23' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server - GPL.
从上面打印的输出可以看到,mysql已经创建成功。可以看到,docker-compose在依次创建network,volume和容器实例,并且每个实力前面都带了一个dockercompose前缀,这个前缀取自docker-compose.yml这个文件所在的目录名称,也可以使用-p参数来修改这个前缀,添加前缀是为了避免了冲突。另外,比如mysql容器的实例名字为dockercompose_mysql_1,带有_1后缀,这个是为了横向扩展做集群时用到。
创建完成之后,可以使用前面两篇文章介绍的命令来查看相关信息:
PS D:\Study\DockStudyInVSCode> docker volume ls
DRIVER VOLUME NAME
local dockercompose_productdata
PS D:\Study\DockStudyInVSCode> docker network ls
NETWORK ID NAME DRIVER SCOPE
d5fbb3a5a2b3 bridge bridge local
1d2ed3540454 dockercompose_backend bridge local
49ce19152f68 host host local
819bf5184145 none null local
PS D:\Study\DockStudyInVSCode> docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
933f312e1e8e mysql:8.0.23 "docker-entrypoint.s…" 50 seconds ago Up 48 seconds 3306/tcp, 33060/tcp dockercompose_mysql_1
在Docker Desktop里也能看到创建的docker-compose。
按CTRL+C,就可以停止docker-compose,并停止容器。也可以使用docker-compose down -v 来删除docker-compose创建的网络和容器 (容器里的数据也会删除,如果保留volume,则把-v参数去掉):
PS D:\Study\DockStudyInVSCode\DockerCompose> docker-compose down -v
WARNING: Some networks were defined but are not used by any service: frontend
Removing dockercompose_mysql_1 ... done
Removing network dockercompose_backend
Removing volume dockercompose_productdata
如果不带-v参数,则不会删除volume。
PS D:\Study\DockStudyInVSCode\DockerCompose> docker-compose down
WARNING: Some networks were defined but are not used by any service: frontend
Removing dockercompose_mysql_1 ... done
Removing network dockercompose_backend
准备数据库及应用程序
在前面的一篇文章中,是通过在Startup的时候,在SeedData方法里使用 context.Database.Migrate();方法来实现数据库迁移的。这能保证模型定义跟数据库定义保持同步。
这种方式在开发阶段比较实用,但是在实际生产环境中,自动应用迁移很容易导致数据丢失。EntityFramework Core的迁移实际是一系列作用于数据库的SQL语句,这些语句根据模型定义来修改数据库的表结构。比如,在定义的模型中删除某个字段,那么执行Migration迁移后,数据库中对应表的字段就会删掉。对实际生产数据库执行这种迁移操作风险很大。所以只有可控更新中才使用这种方式,而不是在每次应用程序启动时就执行迁移。在接下来我们创建两个不同的容器,一个容器来执行数据库初始化和数据库迁移,一个来运行ASP.NET Core MVC应用程序。
首先要修改一下ProductDbContext类,修改内容如下:
using Microsoft.EntityFrameworkCore;
using System;
namespace DockerCompose.Models
{
public class ProductDbContext : DbContext
{
public ProductDbContext() { }
public ProductDbContext(DbContextOptions<ProductDbContext> options) : base(options) { }
protected override void OnConfiguring(DbContextOptionBuilder options)
{
var envs = Environment.GetEnvironmentVariables();
var host = envs["DBHOST"] ?? "localhost";
var port = envs["DBPORT"] ?? "3306";
var password = envs["DBPASSWORD"] ?? "mysecret";
options.UseMySql($"server={host};userid=root;pwd={password};port={port};database=products", ServerVersion.FromString("8.0.23"));
}
public DbSet<Product> Products { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace DockerCompose
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateHostBuilder(args);
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var configuration = services.GetService<IConfiguration>();
var initDb = configuration.GetValue("INITDB", "false") == "true";
if (initDb)
{
System.Console.WriteLine("Preparing Database...");
SeedData.EnsurePopulated(services);
System.Console.WriteLine("Database Preparation Complete");
}
else
{
host.Run();
}
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
// webBuilder.UseDefaultServiceProvider(i => i.ValidateScopes = false);
webBuilder.UseStartup<Startup>();
});
}
}
在配置文件中,设置了INITDB,如果该值为TRUE,则表示初始化数据库,如果设置为false,则运行ASP.NET Core MVC应用程序。这个配置文件从命令行读取或者从环境变量读取。从命令行读取需要额外的NuGet包,安装如下:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.5"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.5"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.3"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.5"/>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="5.0.0-alpha.2"/>
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="5.0.0"/>
</ItemGroup>
</Project>
最后,在Startup里,删除数据库配置,以及SeedData的调用。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using DockerCompose.Models;
using Microsoft.EntityFrameworkCore;
namespace DockerCompose
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ProductDbContext>();
services.AddSingleton<IConfiguration>(Configuration);
services.AddTransient<IRepository, ProductRepository>();
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
定义数据库初始化以及MVC服务
在前面一篇中,我们是按照顺序一个一个启动服务的,这是因为有时候后面的服务需要依赖前面的服务,比如MySQL容器初次启动时比较耗时,而MVC应用程序需要等到MySQL完全初始化等待连接时,才能正常运行。
但Docker在使用Docker Compose文件时,不支持这种,他不知道MySQL服务什么时候会启动完成。他只会逐个执行操作。为了保证MVC的查询在MySQL容器初始化成功之后执行,我们需要安装一个名为wait-for-it的包,他会等到一个TCP端口接收连接。我么可以使用下面的命令来安装这个npm包:
PS D:\Study\DockStudyInVSCode\DockerCompose> npm install wait-for-it.sh@1.0.0
安装完成之后,在DockerCompose文件夹下,会创建node_modules文件夹,下面就有wait-for-it.sh。接下来,我们新建Dockfile,然后编写如下脚本:
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
COPY dist /app
COPY node_modules/wait-for-it.sh/bin/wait-for-it /app/wait-for-it.sh
RUN chmod +x /app/wait-for-it.sh
WORKDIR /app
EXPOSE 80/tcp
ENV WAITHOST=mysql WAITPORT=3306
ENTRYPOINT ./wait-for-it.sh $WAITHOST:$WAITPORT --timeout=0 && exec dotnet DockerCompose.dll
这里面,首先先将wait-for-it脚本拷贝到容器内,然后给予执行权限。然后在入口设置了,等待MySQL启动之后(这里通过监听3306端口是否能接受连接来实现),再执行ASP.NET Core MVC程序。
接着,打开docker-compose.yml文件继续编辑。
version: "3"
volumes:
productdata:
networks:
frontend:
backend:
services:
mysql:
image: "mysql:8.0.23"
volumes:
- productdata:/var/lib/mysql
networks:
- backend
environment:
- MYSQL_ROOT_PASSWORD=mysecret
- bind-address=0.0.0.0
dbinit:
build:
context: .
dockerfile: Dockerfile
networks:
- backend
environment:
- INITDB=true
- DBHOST=mysql
depends_on:
- mysql
mvc:
build:
context: .
dockerfile: Dockerfile
networks:
- backend
- frontend
environment:
- DBHOST=mysql
depends_on:
- mysql
loadbalance:
image: "dockercloud/haproxy"
ports:
- 3000:80
links:
- mvc
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- frontend
添加MVC和loadbalance依赖,这里需要注意的是,在loadbalance中的image,现在用的是“dockercloud/haproxy:1.6.7”,跟前一篇文章里的用法不一样,前一篇文章里的用法需要提供haproxy.cfg配置文件,这里不用,只需要配置links即可。
另外一点可以看到,服务要么直接配置image,要么配置build和Dockerfile,用来生成image。
接下来,先把本地的应用程序发布。使用以下命令:
PS D:\Study\DockStudyInVSCode\DockerCompose> dotnet restore
PS D:\Study\DockStudyInVSCode\DockerCompose> dotnet publish --framework net5.0 --configuration Release --output dist
然后,再编译docker-compose文件:
PS D:\Study\DockStudyInVSCode\DockerCompose> docker-compose build
接着执行db初始化:
PS D:\Study\DockStudyInVSCode\DockerCompose> docker-compose up dbinit
随后,启动mvc和loadbalance服务:
PS D:\Study\DockStudyInVSCode\DockerCompose> docker-compose up mvc loadbalance
执行成功之后,可以看到,在Docker Desktop里的容器运行情况:
现在,在浏览器里输入localhost:3000 即可看到网页内容:
目前自由1个mvc站点,可以通过以下命令,对mvc站点进行扩容,比如以下命令可以扩容5个站点。
PS D:\Study\DockStudyInVSCode\DockerCompose> docker-compose scale mvc=5
再次刷新上述网页,可以看到不同的HOSTNAME:
这里我遇到了一个问题,就是当scale mvc=2n偶数个时,实际上刷新时,只会出现n个不同的HOSTNAME,scale mvc=奇数个时,则正常😂。
如果要降级,只需要减少scale后面的数字即可:
PS D:\Study\DockStudyInVSCode\DockerCompose> docker-compose scale mvc=3
系统会自动删除三个mvc容器。
关闭所有服务的命令是:
PS D:\Study\DockStudyInVSCode\DockerCompose> docker-compose stop
Stopping dockercompose_mvc_5 ... done
Stopping dockercompose_mvc_4 ... done
Stopping dockercompose_mvc_3 ... done
Stopping dockercompose_mvc_2 ... done
Stopping dockercompose_loadbalance_1 ... done
Stopping dockercompose_mvc_1 ... done
Stopping dockercompose_mysql_1 ... done
启动所有服务命令:
PS D:\Study\DockStudyInVSCode\DockerCompose> docker-compose start
Starting mysql ... done
Starting dbinit ... done
Starting mvc ... done
Starting loadbalance ... done
总结
这篇文章讲解了docker-compose文件如何用来描述一个复杂的系统,他能定义容器,自定义卷,一个自定义网络。然后演示了docker-compose相关命令,最后展示了如何使用scale参数来进行秒级扩容和降级。下面列出了本文用到的所有命令:
//各种删除
docker rm -f $(docker ps -aq)
docker network rm $(docker network ls -q)
docker volume rm $(docker volume ls -q)
//查看docker-compose版本
docker-compose --version
//执行docker-compose.yml构建
docker-compose -f docker-compose.yml build
//运行docker-compose.yml定义的应用创建容器
docker-compose -f docker-compose.yml up
//删除所有docker-compose.yml中定义的应用,如果要保留volume,则去掉-v参数
docker-compose –f docker-compose.yml down -v
//安装wait-for-it包,主要用来判断tcp某个端口是否能够开始连接,否则等待
npm install wait-for-it.sh@1.0.0
//运行docker-compose.yml中定义的某个服务
docker-compose up dbinit
docker-compose up mvc loadbalance
//扩展某个应用程序
docker-compose scale mvc=5
//比之前个数少的话,表示降级
docker-compose scale mvc=5
//关闭所有服务
docker-compose stop
//启动所有服务
docker-compose start