步骤:
- 创建.NET应用样例
- 创建包含生成.NET镜像所需引导的Dockerfile
- 构建一个镜像并基于此创建一个容器
- 设置容器数据卷和网络设置
- 使用Docker Compose编排容器
- 使用容器构建开发坏境
创建镜像
先决条件:
了解Docker的基础概念。
总览:
一个镜像包含了运行一个应用所需的一切:代码、二进制文件、运行时、依赖以及其他所需的文件系统。
完成本次学习需要:
安装.NET SDK 6.0或更新版本;
本地运行Docker;
一个编译器;
应用样例:
使用.NET模板创建一个简单的应用作为样例。在本地创建dotnet-docker文件夹。打开一个命令行来对文件夹进行修改。运行以下命令基于ASP.NET core Web App模板创建一个C#应用。
mkdir dotnet-docker
cd dotnet-docker
dotnet new webapp -n myWebApp -o src --no-https
创建成功后出现:
已成功创建模板“ASP.NET Core Web 应用”。
此模板包含除 Microsoft 以外其他方的技术,请参阅 https://aka.ms/aspnetcore/7.0-third-party-notices 以获取详细信息。
上述指令将会创建一个新的文件夹--src。
测试应用:
打开命令行进入src文件夹并使用dotnet run指令。
dotnet run --urls http://localhost:5000
运行成功后出现下方信息:
Building...
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\Users\username\dotnet-docker\src\
在上述样例中,“ Now listening on: http://localhost:5000”表示可以通过http://localhost:5000来访问应用。
打开浏览器访问上述地址,会出现以下页面:
在命令行界面按Ctrl+C停止应用。
创建Dockerfile
在dotnet-docker文件夹中,创建一个文件并命名为Dockerfile。
接下来,需要在Dockerfile文件中添加指令来告诉Docker我们要使用什么镜像来构建应用。在编译器或文本编辑器中打开Dockerfile,并添加下列指令:
# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/sdk:6.0 as build-env
Docker镜像可以被其他镜像继承。因此,我们使用官方镜像而不是全部自己创建。我们将使用官方.NET SDK镜像,它已经包含了创建一个.NET应用所需的全部工具和包。
在此使用多阶段构建,在Dockerfile中使用as来定义一个build-env阶段。
为了使运行后续的指令更加简单,将为资源文件创建一个工作文件夹。这指示Docker使用额zhe路径作为后续指令的默认位置。通过这种方式,我们不必输入文件的全部路径,而是输入基于工作文件夹的相对路径即可。
WORKDIR /src
尽管不是必要的,下述指令将仅复制csproj文件并运行dotnet restore。每个指令创建一个新的容器层。为了加速容器的创建过程,Docker会缓存这些中间层。因为这些文件不会经常变动,我们可以利用复制这些文件的缓存将restore作为独立的指令来运行。(意思是项目文件csproj变动比较少,单独生成中间容器层并进行缓存,可以减少构建容器中的工作量,加快容器构建的速度)。
COPY src/*.csproj .
RUN dotnet restore
接下来,需要将剩余所有的文件复制到镜像中。下列指令将本地src文件夹中的文件复制到镜像中src文件夹下。
COPY src .
接下来,运行dotnet publish指令来构建项目。
RUN dotnet publish -c Release -o /publish
指定运行应用所需的容器,并将它定义为运行时阶段。
FROM mcr.microsoft.com/dotnet/aspnet:6.0 as runtime
确定这个阶段的工作文件夹。
WORKDIR /publish
从build-env阶段中复制publish文件夹至运行时阶段。
COPY --from=build-env /publish .
暴露80端口用于访问。
EXPOSE 80
我们需要在镜像在一个容器中被执行时告诉Docker执行什么指令。在Dockerfile中使用ENTRYPOINT指令来实现。
ENTRYPOINT ["dotnet", "myWebApp.dll"]
.dockerignore file
为了确保你的构建环境尽可能的小,在dotnet-docker文件夹中添加.dockerignore file文件,并复制以下内容到文件中。
**/bin/
**/obj/
创建镜像:
我们使用docker build指令来创建镜像,该指令通过Dockerfile和环境来构建Docker镜像。创建的环境是指位于指定路径的文件的集合。Docker创建过程可以连接位于这个环境中的任何文件。
创建指令可以选择 --tag参数来为镜像命名,漆命名格式为:名称:编号。在没有指定编号时,Docker使用latest作为默认编号。
使用以下指令来进行镜像的构建
cd /path/to/dotnet-docker
docker build --tag dotnet-docker .
查看本地镜像
可以通过命令行语句和Docker Desktop两种方式来查看本地镜像。
使用docker images指令来进行查看。
基于镜像生成容器
总览:
在上一步中,我们创建了应用样例和Dockerfile。并使用docker build指令创建了镜像。本节将运行镜像确认应用是否能够正确运行。
容器是一个正常的操作系统进程,但是它头自己独立于宿主机的文件系统、自己的网络和自己的进程树。
我们使用docker run来在容器中运行镜像。dokcer run指令必需的参数是镜像的名称。使用下列指令来运行容器:
docker run dotnet-docker
运行这个指令之后,没有返回到指令提示行,这是因为应用此时作为一个服务运行在一个等待传入请求的loop中,并没有将控制返回至宿主机操作系统,直到我们停止容器。
当我们打开浏览器访问http://localhost:80时,该连接是被拒绝的。这意味着我们无法连接本地80端口,当然这是预料之中的,因为容器的网络也是独立于宿主机的。停止容器,重新使用本地5000端口来进行发布。
Ctrl+C停止容器并返回到命令行界面。
使用--publish(-p)在docker run指令中来为容器进行端口映射。--publish的格式为 【宿主机端口】:【容器端口】。运行新的指令来将宿主机5000端口与容器80端口进行开通:
docker run --publish 5000:80 dotnet-docker
现在连接http://localhost:5000可以正常访问。
Run in detached mode:在独立模式中运行容器,即使容器保持在后台运行
我们的应用是一个web服务,所以我们不必时刻与容器保持连接。Docker可以将容器运行在独立环境或者在后台,用 --detach 或者 -d来实现。Docker将会按照与之前相同的方式来启动容器,但是会返回到命令行操作界面。
docker run -d -p 5000:80 dotnet-docker
Docker在后台运行容器并在命令行界面返回容器ID.
此时访问http://localhost:5000,可以成功访问。
容器列表
使用docker ps指令来查看正在运行的容器,使用docker ps -a/--all来查看所有的容器。
开始、停止、命名容器
开始:docker restart
停止:docker stop
命名:--name
删除:docker rm
使用容器构建开发环境
简介
这一节,将为上一节创建的应用设置本地开发环境。将使用Docker创建镜像,并利用Docker Compose使这个过程更加简单。
在容器中运行数据库
首先,我们将了解如何在容器中运行数据库,并学习如何使用数据卷和网络来保存我们的数据,以及实现应用与数据库的通信。之后我们会将所有的容器放入一个Compose文件中,这样我们就可以通过一条指令来设置并运行本地开发环境。
我们可以使用Docker官方提供的PostgreSQL镜像创建容器来提供服务,而不用下载、安装、配置。
在运行数据库容器之前,我们将先创建一个数据卷以便Docker保存数据。此处我们使用Docker提供的数据卷而不是绑定挂载。
创建数据卷
docker volume create postgres-data
现在我们创建一个网络实现数据库之间的相互通信。这个被称为用户定义桥网络的网络可以根据创建的连接语句为我们提供一个非常好的DNS查看服务。
docker network create postgres-net
在Windows环境中,使用以下语句进行网络创建。
docker network create --driver nat postgres-net
在容器中运行PostgreSQL,并连接之前创建的数据卷和网络。Docker将会从Hub中拉取需要的镜像。在下面的指令中, -v是指定数据卷的参数。
docker run --rm -d -v postgres-data:/var/lib/postgresql/data \
--network postgres-net \
--name db \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=example \
postgres
确认PostgresSQL数据库正在运行,使用以下指令在容器中连接数据库
docker exec -ti db psql -U postgres
Ctrl+D推出内部命令行。
将应用连接到数据库
在上面的指令中,通过psql指令在db容器中登录到PostgresSQL数据库中。
接下来,我们将升级应用样例,在应用文件夹中增加一个包使应用可以与数据库连接,并升级资源文件。在命令行执行下列指令:
cd /path/to/dotnet-docker/src
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
在之前创建的应用中,添加或修改下列文件:
src目录下创建Models文件夹,添加Student.cs,添加以下代码:
using System;
using System.Collections.Generic;
namespace myWebApp.Models
{
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
}
}
在src目录下,创建Data目录,并添加SchoolContext.cs,添加以下代码:
using Microsoft.EntityFrameworkCore;
namespace myWebApp.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options) { }
public DbSet<Models.Student>? Students { get; set; }
}
}
修改Program.cs中的代码如下:
using Microsoft.EntityFrameworkCore;
using myWebApp.Models;
using myWebApp.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
// Add services to the container.
builder.Services.AddDbContext<SchoolContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("SchoolContext")));
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
// add 10 seconds delay to ensure the db server is up to accept connections
// this won't be needed in real world application
System.Threading.Thread.Sleep(10000);
var context = services.GetRequiredService<SchoolContext>();
var created = context.Database.EnsureCreated();
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB.");
}
}
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
修改appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"SchoolContext": "Host=db;Database=my_db;Username=postgres;Password=example"
}
}
修改Pages中的Index.cshtml
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>
<div class="row mb-auto">
<p>Student Name is @Model.StudentName</p>
</div>
修改Pages下的Indec.cshtml.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace myWebApp.Pages;
public class IndexModel : PageModel
{
public string StudentName { get; private set; } = "PageModel in C#";
private readonly ILogger<IndexModel> _logger;
private readonly myWebApp.Data.SchoolContext _context;
public IndexModel(ILogger<IndexModel> logger, myWebApp.Data.SchoolContext context)
{
_logger = logger;
_context= context;
}
public void OnGet()
{
var s =_context.Students?.Where(d=>d.ID==1).FirstOrDefault();
this.StudentName = $"{s?.FirstMidName} {s?.LastName}";
}
}
打开命令行,在dotnet目录下重新生成镜像。
docker build --tag dotnet-docker .
接下来,在与数据库相同的网络中运行应用容器,这样就可以通过容器名称连接到数据库。
docker run \
--rm -d \
--network postgres-net \
--name dotnet-app \
-p 5000:80 \
dotnet-docker
连接Adminer(轻量化数据库管理工具)填充数据库
docker run \
--rm -d \
--network postgres-net \
--name db-admin \
-p 8080:8080 \
adminer
访问http://localhost:8080
进入Adminer登录界面
登录信息如下:
* System: PostgreSQL
* Server: db
* Username: postgres
* Password: example
* Database: my_db
进入Schema:public页面
点击表格中的Student,进入该表格内
点击New item,为Student表格添加新数据。
重新进入http://localhost:5000,保存的学生信息已显示。
使用Docker Compose使该过程更加高效
通过Docker Compose,可以创建Compose file文件,通过一条指令来启动以上所有的容器。
在dotnet-docker目录下创建一个docker-compose.yml的文件,并复制以下内容(实际操作时发现需要在文件中添加version信息,否则会出现报错)
services:
db:
image: postgres
restart: always
environment:
POSTGRES_PASSWORD: example
volumes:
- postgres-data:/var/lib/postgresql/data
adminer:
image: adminer
restart: always
ports:
- 8080:8080
app:
build:
context: .
ports:
- 5000:80
depends_on:
- db
volumes:
postgres-data:
Compose file使容器创建工作更加便捷,可以使用Compose file文件对所有创建过程进行声明,而不需要使用docker run指令一一生成。
在上面创建的Compose file中,进行端口暴露使得可以从外部访问容器,也将本地代码映射到运行的容器中以确保代码的修改可以在容器中被选取。
使用Compose file的另外一个非常cool的特点是,可以使用服务名称来指定需要的服务。因此,可以在应用配置文件的数据库连接信息中使用“db”,来指定在Compose file中间已经命名为“db”的PostgreSQL容器。
使用下列指令运行应用
docker-compose up --build
使用Ctrl+C关闭所有运行的容器。使用docker-compose down删除所有已经停止运行的容器。
Detached mode/隔离模式/后台模式
与docker run指令类似,docker-compose指令同样可以通过添加 -d指令来进入后台运行模式。
.env文件
Docker compose可以自动从.env文件中读取环境变量。例如在上述的应用中,需要将数据库的连接密码作为变量设置,在Compose file所在目录中创建一个.env文件并添加以下内容:
POSTGRES_PASSWORD=example
更新compose file如下:(按照官方文档直接运行,报错environment参数格式出错,查询后删除“${POST...set}”等内容后可以正常运行)
services:
db:
image: postgres
restart: always
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?database password not set}
volumes:
- postgres-data:/var/lib/postgresql/data
adminer:
image: adminer
restart: always
ports:
- 8080:8080
app:
build:
context: .
ports:
- 5000:80
depends_on:
- db
volumes:
postgres-data: