Skip to main content

React sample in Docker

Tutorial 2 📦

Source code

main - initial branch
dockerizing - final branch
dockerizing pull request - pull request with code changes

Overview

In this second tutorial we describe the code improvements and the steps to follow in order to deploy the React sample application in Docker.

Docker

Prerequisites

take into account

We recommend reading Docker documentation to get a base understanding of Docker's concepts and principles.

  1. First, you should go through previous tutorials OR clone main branch.
  2. JetBrains Rider or Visual Studio.
  3. Command prompt or Terminal.

In addition, you must download and install the latest version of Docker Desktop.

Backend

The required changes in the backend are illustrated in this section. New files must be added, and some files must be modified to run React sample in Docker.

DatabaseUpgrade

First, the backend should bring up the script for the database. This can be done by creating the class DatabaseUpgrade in the project folder WorkflowApi. This class reads the SQL script from this directory, and it attempts to execute the script in the database. If the execution process presents an exception, the operation will be repeated, a delay is done, and again the procedure for running the script is executed. This is required because the database in the Docker container might start up after the backend application, and during this short time it may be not available, so the exception allows to execute the start script several times to raise the database while it is not successful.

Backend/WorkflowApi/DatabaseUpgrade.cs
using Microsoft.Data.SqlClient;

namespace WorkflowApi;

public class DatabaseUpgrade
{
private const int AttemptCount = 100;
private static readonly TimeSpan Delay = TimeSpan.FromSeconds(5);

private readonly string _connectionString;
private readonly string _sqlScript;

private DatabaseUpgrade(string connectionString, string sqlScript)
{
_connectionString = connectionString;
_sqlScript = sqlScript;
}

public static async Task WaitForUpgrade(string connectionString)
{
var sql = await File.ReadAllTextAsync("./Sql/CreatePersistenceObjects.sql");
var instance = new DatabaseUpgrade(connectionString, sql);
await instance.Upgrade();
await Console.Out.WriteLineAsync("The database has been upgraded");
}

private async Task Upgrade(int attemptNumber = 0)
{
try
{
await Console.Out.WriteLineAsync($"Upgrading database, attempt number: {attemptNumber}");
await UpgradeDatabase();
}
catch (Exception e)
{
await Console.Error.WriteLineAsync(e.Message);
if (attemptNumber >= AttemptCount) throw;

await Task.Delay(Delay);
await Upgrade(attemptNumber + 1);
}
}

private async Task UpgradeDatabase()
{
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
await using var command = connection.CreateCommand();
command.CommandText = _sqlScript;
await command.ExecuteNonQueryAsync();
}
}

WorkflowApiConfiguration

The WorkflowApiConfiguration was included to define the backend configuration. The Cors and LicenseKey will be set in the docker compose file, so these settings are indicated in the configuration.

Backend/WorkflowApi/WorkflowApiConfiguration.cs
namespace WorkflowApi;

public class WorkflowApiConfiguration
{
public WorkflowApiCorsConfiguration Cors { get; set; }

public string LicenseKey { get; set; }
}

public class WorkflowApiCorsConfiguration
{
public List<string> Origins { get; set; }
}

CreatePersistenceObjects

Next, in the Backend a directory called Sql and the SQL script CreatePersistenceObjects.sql must be added also in WorkflowApi project to create the database objects according the Workflow Engine standard.

Database providers

You can download the MS SQL database script here.

Then, set it in the location: Backend/WorkflowApi/Sql/CreatePersistenceObjects.sql.

WorkflowApi

Furthermore, the WorkflowApi.csproj must be modified. The script should be published during assembling in the directory with binary and executed files.

Backend/WorkflowApi/WorkflowApi.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">

<ItemGroup>
<ProjectReference Include="..\WorkflowLib\WorkflowLib.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="WorkflowEngine.NETCore-Core" Version="7.1.3" />
<PackageReference Include="WorkflowEngine.NETCore-ProviderForMSSQL" Version="7.1.3" />
</ItemGroup>

<ItemGroup>
<None Update="Sql\CreatePersistenceObjects.sql">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

</Project>

Program

The Program.cs was also modified. When starting the program, the configuration is set through the configuration file and the script to raise the database is executed. Here, we get apiConfiguration, the Cors settings are changed from AllowAnyOrigin to withOrigins which is taken from the configuration file, the connectionString is gotten by GetConnectionString, the DatabaseUpgrade is launched, and then the connectionString is provided for WorkflowInit. Besides, the LicenseKey is passed to WorkflowRuntime object.

The Cors address will be set from appsettings.json and the script will be run for starting the database. These changes allow to rise the database along with backend and frontend when starting Docker.

Backend/WorkflowApi/Program.cs
using OptimaJet.Workflow.Core.Runtime;  
using WorkflowApi;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

const string rule = "MyCorsRule";
builder.Services.Configure<WorkflowApiConfiguration>(builder.Configuration);
var apiConfiguration = builder.Configuration.Get<WorkflowApiConfiguration>();
if (apiConfiguration?.Cors.Origins.Count > 0)
{
builder.Services.AddCors(options =>
{
options.AddPolicy(rule, policy =>
{
policy.WithOrigins(apiConfiguration.Cors.Origins.ToArray());
});
});
}

var app = builder.Build();

var connectionString = app.Configuration.GetConnectionString("Default");
if (connectionString is null)
{
throw new NullReferenceException("Default connection string is not set");
}
await DatabaseUpgrade.WaitForUpgrade(connectionString);

WorkflowLib.WorkflowInit.ConnectionString = connectionString;

if (!string.IsNullOrEmpty(apiConfiguration?.LicenseKey))
{
WorkflowRuntime.RegisterLicense(apiConfiguration.LicenseKey);
}

...

App settings

In the configuration file appsettings.json, we register the Cors, the LicenseKey and the localhost address for the frontend, and also the ConnectionStrings options are set.

Backend/WorkflowApi/appsettings.json
  "LicenseKey": "",                                                                               
"Cors": {
"Origins" : ["http://localhost:3000"]
},
"ConnectionStrings": {
"Default": "Data Source=(local);Initial Catalog=master;User ID=sa;Password=StrongPassword#1"
}

WorkflowInit

In addition, the ConnectionString must be replaced with property in WorkflowInit.cs because it is set in the configuration file.

Backend/WorkflowLib/WorkflowInit.cs
public static class WorkflowInit 
{
private const string ConnectionString = "Data Source=(local);Initial Catalog=master;User ID=sa;Password=StrongPassword#1";

private static readonly Lazy<WorkflowRuntime> LazyRuntime = new(InitWorkflowRuntime);
private static readonly Lazy<MSSQLProvider> LazyProvider = new(InitMssqlProvider);

public static string ConnectionString { get; set; } = "";
public static WorkflowRuntime Runtime => LazyRuntime.Value;
public static MSSQLProvider Provider => LazyProvider.Value;
...

All of these are the required changes in the backend application.

Docker Compose

A new directory called docker-files is added in the project root directory with these three files: backend.Dockerfile, frontend.Dockerfile and docker-compose.yml.

backend.Dockerfile

The backend.Dockerfile is added for starting the backend application. Here, the backend image is built and placed in a Docker container.

docker-files/backend.Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src

COPY Backend.sln .
COPY WorkflowApi/WorkflowApi.csproj WorkflowApi/WorkflowApi.csproj
COPY WorkflowLib/WorkflowLib.csproj WorkflowLib/WorkflowLib.csproj
RUN dotnet restore Backend.sln --source https://api.nuget.org/v3/index.json

COPY ./ .
RUN dotnet build Backend.sln --configuration Release --output /app

FROM build AS publish
WORKDIR /src/WorkflowApi
RUN dotnet publish WorkflowApi.csproj --configuration Release --output /app

FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app/sample

COPY --from=publish /app ./bin

RUN useradd --user-group --uid 1000 wfe
RUN chown -R wfe:wfe /app

USER wfe

WORKDIR /app/sample/bin
ENTRYPOINT ["dotnet", "WorkflowApi.dll"]

frontend.Dockerfile

Moreover, the frontend.Dockerfile which runs the frontend through NPM package manager.

docker-files/frontend.Dockerfile
FROM node:lts-alpine3.17

RUN mkdir /app
WORKDIR /app

COPY package*.json .
RUN npm install --legacy-peer-deps

ENTRYPOINT ["npm", "run", "start"]

docker-compose.yml

Now, we have docker-compose.yml. We created a YAML file to define the services with Docker compose. It means the list of services (or containers) that we want to run as part of our application.

First, the frontend is defined, it runs on port 3000. The port 3000 inside the container is exposed to port 3000 outside the container.

After that, the backend is started. The environment variables must be specified. The ConnectionStrings settings, the LicenseKey and the Cors Origins as http://localhost:3000 from the frontend. The queries from the frontend must be processed by the backend.

Finally, the database service is also set. The Azure SQL image is used.

docker-files/docker-compose.yml
name: react-example-docker

services:
frontend:
depends_on:
- backend
container_name: frontend
hostname: frontend
build:
context: ../frontend
dockerfile: ../docker-files/frontend.Dockerfile
volumes:
- ../frontend/public:/app/public
- ../frontend/src:/app/src
ports:
- 3000:3000
backend:
depends_on:
- database
container_name: backend
hostname: backend
build:
context: ../Backend
dockerfile: ../docker-files/backend.Dockerfile
environment:
ConnectionStrings__Default: Server=database;Initial Catalog=master;User ID=sa;Password=StrongPassword#1
ASPNETCORE_URLS: http://+:5139
LicenseKey:
Cors__Origins__0: http://localhost:3000
ports:
- 5139:5139
database:
container_name: database
hostname: database
image: mcr.microsoft.com/azure-sql-edge:latest
environment:
MSSQL_SA_PASSWORD: StrongPassword#1
ACCEPT_EULA: 1
cap_add:
- SYS_PTRACE
ports:
- 1433:1433
volumes:
- ./var/mssql-data:/var/opt/mssql/data

Running the application stack

Now, we can start docker-compose.yml file up!

  1. Start up the application stack using the following command. You might add the -d flag to run everything in the background.

    cd docker-files
    docker compose up --build --force-recreate
  2. Once you run the command, you should see this output:

    PS> docker compose up
    [+] Running 4/4
    - Network react-example-docker_default Created 0.9s
    - Container database Created 0.1s
    - Container backend Created 0.4s
    - Container frontend Created 0.1s
    Attaching to backend, database, frontend
    database | Azure SQL Edge will run as non-root by default.
    database | This container is running as user mssql.
    database | To learn more visit https://go.microsoft.com/fwlink/?linkid=2140520.
    database | 2023/02/21 11:49:21 [launchpadd] INFO: Extensibility Log Header: <timestamp> <process> <sandboxId> <sessionId> <message>

    Run Docker

  3. Go to http://localhost:3000/ in your preferred browser.

  4. Click on tab Designer to check. If the Designer is loaded properly, it means that everything works ok!

    Open Designer

  5. Upload the scheme provided in the first tutorial.

    Open Designer

Conclusion

That's it! We showed in this tutorial how you can run the React sample application in Docker Compose.