Learning Docker to fix Jester deployment

Nuh

I had several issues deploying to two separate platforms with the following dockerfile and few variants of it.

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["Jester/Jester.csproj", "Jester/"]
RUN dotnet restore "Jester/Jester.csproj"
COPY . .

WORKDIR "/src/Jester"
RUN dotnet build "Jester.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "Jester.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .

ENTRYPOINT ["dotnet", "Jester.dll"]

I had one significant issue with this Dockerfile where 1 platform was happy and the other wasn’t.

Railway.app complained it couldn’t find a file I was loading from the filesystem “sample-news.json” which lives in the same directory as the Dockerfile

I attempted to solve it by copying manually in various stages of the docker processes. I tried it in the final stage and build stage with various ways of copying.

It has worn me out, I need to learn docker and I can’t trust AI to teach me because sometimes they are wrong and they aren’t aware of it. And worse, I won’t be aware of it before burning a lot of time.

Anyhow, this post will capture how I am levelling up with Docker (especially for understanding Dockerfile).

  1. Looked up “Docker Playground”, and found Docker 101. Following the instructions, I spun up the docker container which built a copy of the 101 steps accessible via localhost

  2. I decided I will use this current project of mine (Jester) instead of their basic node.js app. I will run the same commands and adjust where necessary

  3. First was to build the image of the application using docker build -t jester-scratch . (I am calling the image ‘jester-scratch’)

  4. I feel a bit better seeing step by step logging from docker. However, the build ended in failure

    Error 1: The docker build ended with an error
    Error 1: The docker build ended with an error

  5. We see exactly where it went wrong

    Further details to Error 1 from docker build asserting that it couldn't copy app folders
    Further details to Error 1 from docker build asserting that it couldn't copy app folders
    It is struggling to copy the specificed files and folders. This is reasonable to me as:

    • I am currently inside Jester/Jester in my host machine

    • The Dockerfile is inside this same directory

    • The copy command is starting from this directory and trying to find the folder Jester in this directory. It can’t find it, there’s no such folder here. If we went up 1 level to the parent container, then you will find it.

      Alert: I have just discovered two Jester.sln files, one in repo top-level dir Jester/ and the other in the source folder Jester/Jester. It is news to me that there’s a solution in Jester/Jester. It sneaked in last week, and it appears to be harmless with regards to prod builds etc. I will remove the one that sneaked in as it needs to be level with projects it groups.

  6. Here’s my folder structure, more or less. (I generated using this)

    Jester/
    ├─ Jester/
    │  ├─ Controllers/
    │  ├─ Data/
    │  ├─ Models/
    │  ├─ UI/
    │  │  ├─ assets/
    │  │  ├─ scss/
    │  │  ├─ ts/
    │  │  ├─ gulp.js
    │  │  ├─ package.json
    │  ├─ Views/
    │  ├─ Jester.csproj
    │  ├─ Dockerfile
    │  ├─ Program.cs
    │  ├─ sample-news.json
    │  ├─ appsettings.json
    ├─ .gitignore
    ├─ .dockerignore
    ├─ Jester.sln
  7. I have modified the Dockerfile to account for the above issue that led to build breaking. Update the affected line to

    COPY ["Jester/Jester.csproj", "Jester/"]
  8. So running that again has thrown another error

    [+] Building 420.0s (15/17)                                                                              docker:default
    => [internal] load build definition from Dockerfile                                                    
    => [internal] load build context                                                                                 46.2s
    => => transferring context: 92.56MB                                                                              46.0s
    => CACHED [base 2/2] WORKDIR /app                                                                                 0.0s
    => CACHED [final 1/2] WORKDIR /app                                                                                0.0s
    => [build 2/7] WORKDIR /src                                                                                       0.3s
    => [build 3/7] COPY [Jester.csproj, Jester/]                                                                      0.1s
    => [build 4/7] RUN dotnet restore "Jester/Jester.csproj"                                                        136.4s
    => [build 5/7] COPY . .                                                                                          14.5s
    => [build 6/7] WORKDIR /src/Jester                                                                                0.1s
    => ERROR [build 7/7] RUN dotnet build "Jester.csproj" -c Release -o /app/build                                    9.7s
    ------
    > [build 7/7] RUN dotnet build "Jester.csproj" -c Release -o /app/build:
    1.625 MSBuild version 17.3.4+a400405ba for .NET
    2.918   Determining projects to restore...
    3.890   All projects are up-to-date for restore.
    9.558 CSC : error CS5001: Program does not contain a static 'Main' method suitable for an entry point [/src/Jester/Jester.csproj]
    9.578
    9.578 Build FAILED.
    9.578
    9.578 CSC : error CS5001: Program does not contain a static 'Main' method suitable for an entry point [/src/Jester/Jester.csproj]
    9.579     0 Warning(s)
    9.579     1 Error(s)
    9.580
    9.580 Time Elapsed 00:00:07.81
    ------
    Dockerfile:15
    --------------------
    13 |
    14 |     WORKDIR "/src/Jester"
    15 | >>> RUN dotnet build "Jester.csproj" -c Release -o /app/build
    16 |
    17 |     FROM build AS publish
    --------------------
    ERROR: failed to solve: process "/bin/sh -c dotnet build \"Jester.csproj\" -c Release -o /app/build" did not complete successfully: exit code: 1   
  9. You can tell it couldn’t find the file. Let me try running it from the repo root dir (while undoing the previous modification). Result? Same issue.

  10. I updated the dockerfile one more time. It was copying all to src/ instead of src/Jester/ as should be the case

    COPY ["Jester.csproj", "Jester/"]
    RUN dotnet restore "Jester/Jester.csproj"
    COPY . ./Jester

    It ran successfully now until the publish stage, it crashed again. But we made progress.

    Second error building docker image
    Second error building docker image

    I am familiar with this issue and know it is happening because dotnet runs the prebuild command npm i before intending to run npm run gulp for front end compilation.

    What this error means is that we don’t have npm as a command. In other words, we don’t have node. It is not contained in the .net image. We need to add it separately or find an image that contains both .net and node.

    I will just install it into the image. Let’s modify the Dockerfile.

    [Several goose chases later trying fnm, nvm etc]

    I am going to abandon the approached that coupled dotnet build process with npm build. I can build in a stage of its own with node, and copy over built assets to the correct dir before dotnet build begins. A lot easier, decoupled and readable. Explains why I never saw such a mixture even when we were integrating React apps into .NET codebases.

  11. I asked our resident AI to write the basics of adding a frontend build stage. It mostly did fine but needed some tweaking such as node image version, file paths and node-sass complaint handling.

    Most importantly, it worked and it looks clean.

    End of nightmare, I hope.

  12. Running the container now with docker run -dp 5000:80 jester-scratch, it succeeds. 🚀 we can breathe now.

Here’s our completed Dockerfile with the bonus of knowing every step and command, and contexts.

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["Jester.csproj", "Jester/"]
RUN dotnet restore "Jester/Jester.csproj"
COPY . ./Jester

WORKDIR "/src/Jester"
RUN dotnet build "Jester.csproj" -c Release -o /app/build

## Node Build Stage
FROM node:lts-alpine AS node_build
WORKDIR /frontend

COPY ./UI/ ./UI/
COPY ./Views ./Views # Tailwind needs to capture classes used 
COPY ./ViewComponents ./ViewComponents # Tailwind needs to capture classes used

WORKDIR /frontend/UI
RUN npm install
RUN npm rebuild node-sass
RUN npm run build

FROM build AS publish
RUN dotnet publish "Jester.csproj" -c Release -o /app/publish

## Copy built UI assets from node_build stage
COPY --from=node_build /frontend/wwwroot /app/publish/wwwroot

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .

ENTRYPOINT ["dotnet", "Jester.dll"]

And the celebratory image showing successful deployment (with a dev-cert as work around to https for now)

Site is live, hosted by Railway.app
Site is live, hosted by Railway.app

Nuh © 2024