Yuan Ji
Posted on October 13, 2024
After completing my demo project on Keycloak OAuth2 Token Exchange, I decided to expand the idea and created a new demo project called my-ai-doctor. Unlike the previous demo, which focused on backend integration, this project uses Elm for the UI and Docker Compose to run all six servers locally: the Keycloak server, API server, and the SPA web servers for both MyDoctor and MyHealth.
Fixing the Elm Compiler for Linux ARM 64
The first challenge was building the Elm Land project and deploying it to Docker containers. Initially, I used a standard Dockerfile to build my Elm UI project, but soon hit a roadblock:
> [build 8/9] RUN npm install:
8.709 npm error code 1
8.709 npm error path /app/node_modules/elm
8.709 npm error command failed
8.709 npm error command sh -c node install.js
8.709 npm error -- ERROR -----------------------------------------------------------------------
8.709 npm error
8.709 npm error I am detecting that your computer (linux_arm64) may not be compatible with any
8.709 npm error of the official pre-built binaries.
8.709 npm error
8.709 npm error I recommend against using the npm installer for your situation. Check out the
8.709 npm error alternative installers at https://github.com/elm/compiler/releases/tag/0.19.1
8.709 npm error to see if there is something that will work better for you.
8.709 npm error
8.709 npm error From there I recommend asking for guidance on Slack or Discourse to find someone
8.709 npm error who can help with your specific situation.
8.709 npm error
8.709 npm error --------------------------------------------------------------------------------
After some investigation, I found that the latest Elm compiler (version 0.19.1) does not support linux_arm64. This meant I could build and run Elm projects on my Apple Mac Mini Pro natively, but not inside Docker. Fortunately, Simon Lydell and Mario Rogic created a new version of the elm npm package, which supports Linux ARM 64. Based on this discussion, the solution was to override the elm package in my package.json
file by adding these lines:
"overrides": {
"elm": "npm:@lydell/elm"
}
This replaces the Elm npm module in the locally downloaded node_modules
directory with the @lydell/elm
module, allowing the unofficial Elm compiler to work correctly on the Linux ARM64 platform.
Passing Environment Variables to UI Code
When developing the AI Doctor system locally, the UI SPA connects to the backend API at http://api.mydoctor:8081/api/v1/records
. However, when running the entire demo inside Docker, the API is accessible via port 80 with the help of an Nginx reverse proxy: http://api.mydoctor/api/v1/records
. The challenge was how to dynamically change the backend API URL in the Elm code.
The Elm Land documentation briefly mentions environment variables, but without detailed examples. I found a helpful blog, Deploying an Elm Frontend to Cloudflare Pages, which provided some hints on how to pass environment variables to an Elm Land app. After studying the Elm Land documentation again (here and here), I realized it was quite straightforward to pass different values as environment variables to Elm code.
In the interop.js
file, we can pick environment variables and pass them to Elm code as Flags
. For example:
export const flags = ({ env }) => {
return {
apiBaseUrl: env.API_BASE_URL
};
};
Here, we pass the backend API base URL as a field in flags
, which is then saved into Shared.Model
.
First, I needed to customize the Shared module:
elm-land customize shared
This command will move three files from .elm-land
into src
folder: Shared.elm
, Shared/Model.elm
and Shared/Msg.elm
.
Next, I added the field apiBaseUrl
to Shared.Model.Model
. Here is the code for Shared/Model.elm
:
module Shared.Model exposing (Model)
type alias Model =
{ apiBaseUrl : String
}
In Shared.elm
, I decoded the flags and passed apiBaseUrl
to Shared.Model.Model
:
-- FLAGS
type alias Flags =
{ apiBaseUrl : String
}
decoder : Json.Decode.Decoder Flags
decoder =
Json.Decode.map Flags
(Json.Decode.field "apiBaseUrl" Json.Decode.string)
-- INIT
type alias Model =
Shared.Model.Model
init : Result Json.Decode.Error Flags -> Route () -> ( Model, Effect Msg )
init flagsResult route =
( { apiBaseUrl =
case flagsResult of
Ok flags -> flags.apiBaseUrl
Err _ -> ""
}
, Effect.none
)
The Shared.Model.Model
(or Shared.Model
) is then passed to each page, so the apiBaseUrl
can be accessed whenever we need to call the backend API. See example code at Pages/Test.elm
Setting Environment Variables
To set environment variables like API_BASE_URL
, you first need to declare them in the elm-land.json
file under app.env, as:
{
"app": {
"env": [ "API_BASE_URL" ]
}
}
For security reasons, all environment variables are hidden from your Elm Land application by default.
When you want to share an environment variable with your app, the app.env field is the one spot check
See my elm-land.json
file, and I also declared AUTH_SERVER_URL
for Keycloak server address and WS_BASE_URL
for backend chatbot WebSocket address.
Then, you can pass API_BASE_URL
in the command line when developing locally:
API_BASE_URL=http://api.mydoctor:8081/api/v1 elm-land server
For production builds, use the ENV
directive in the Dockerfile
like this:
ENV API_BASE_URL="http://api.mydoctor/api/v1"
See my Dockerfile
for mydoctor-ui app.
You can find all source code at https://github.com/jiwhiz/my-ai-doctor. This is still a work in progress. Please let me know if you find this blog is helpful.
Posted on October 13, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.