Run Medplum App inside Docker
# support
a
Hello everyone, I hope everyone is well. I'm in a project that aims to containerize Medplum. The project aims to run all parts of Medplum in containers, such as the App, Server, PostgreSQL and Redis. I had a problem: The App container did not communicate correctly on the Docker network, so it was not accessible externally. I decided to research the Vite Server and discovered something important. When we create a Medplum App container, Vite does not provide a default Host for the container, only the port. Therefore, when we try to access the container via the default IP, Vite does not recognize it and does not make the Medplum App available. So that Docker can correctly map the container, we need to change the packages/app/vite.config.ts file with the following code: server: { host: '0.0.0.0', port: 3000, }, This way we guarantee that Vite will provide a valid IP, if there is a different mapping by Docker.
r
Hi @Adriano Freitas ! Sorry for the delay
One note I'll make here is that the Medplum app actually compiles down to a static html + JS bundle, which can be hosted on a CDN. So to deploy to production, you might not need a docker container.
That is how we deploy in our hosted system
app.medplum.com
Out of curiousity - in what environment do you plan on running hte Medplum App container?
a
I needed to create it entirely in Docker via Docker Compose, as our project intends to upload it to the Kubernetes Cluster, and we didn't find anything related to Medplum on the Helm Chart.
r
Are you running Kubernetes on AWS, or on a different cloud hosting provider?
a
We are running K8s on premises. Hence the need to personalize.
But I believe that this change in Vite will not impact the original project, what do you think?
r
I think it should be ok, but I'm not an expert on this. If you open a PR with the proprosed change, our eng team can give you proper feedback. That being said, Vite is primarily a dev environment.
npm run dev
does not produce an optimized bundle. And the server isn't built for production traffic I think a cleaner / more optimized approach would be: 1. use
npm run build -- --filter=app
to create a optimized html/css/js files 2. Use a standard HTTP server docker container (e.g. NGinX) to deploy the files: https://www.dailysmarty.com/posts/steps-for-deploying-a-static-html-site-with-docker-and-nginx
a
But that's exactly what we did, but not just for the App, but for the Server as well. It's a good idea, but we don't use an Nginx container, but a NodeJs 20. The Dockerfile executes the build commands for each container (App and Server).
If you want to help me create production containers, both for the Server and the App, I would appreciate it. Our docker compose looks like this: server: container_name: server build: context: . dockerfile: Dockerfile restart:always networks: - medplum_network entrypoint: packages/server/docker-entrypoint.sh ports: - "8103:8103" depends_on: - postgres - redis app: container_name: app build: context: . dockerfile: Dockerfile restart:always networks: - medplum_network entrypoint: packages/app/docker-entrypoint.sh ports: - '3000:3000' depends_on: - postgres - redis And our Dockerfile is as follows: FROM node:20-slim RUN apt-get update && apt-get install -y git COPY . /usr/src/medplum WORKDIR /usr/src/medplum RUN npm ci RUN npm run build RUN chmod -R 777 /usr/src/medplum/packages/app/docker-entrypoint.sh RUN chmod -R 777 /usr/src/medplum/packages/server/docker-entrypoint.sh CMD["bash"] The Entrypoint.sh commands are: npm run --prefix packages/app/dev It would be a pleasure if you could help me run the production command instead of development. I'm at that point.
r
@Adriano Freitas , there are a few options to get you unblocked: - I tested this dockerfile below, using nginx as the production webserver
Copy code
FROM nginx:alpine
COPY ./dist /usr/share/nginx/html
you could download the dist folder directly from NPM (https://www.npmjs.com/package/@medplum/app?activeTab=code) -
npm run dev
just runs the
vite
command (https://vitejs.dev/guide/cli#dev-server). You could just set the hostname to 0.0.0.0 via command line args, before your PR is supported
a
Thank you very much for the explanations! Then I will try to implement it in the way you suggested. I've never tried it that way.
g
Hi, I am trying to follow along. My Nginx docker image looks like:
Copy code
# syntax=docker/dockerfile:1.7.1
FROM nginx:1.26.0-alpine3.19-slim

RUN rm /usr/share/nginx/html/50x.html && \
    rm /usr/share/nginx/html/index.html
RUN wget -qO- https://registry.npmjs.org/@medplum/app/-/app-3.1.4.tgz | tar --strip-components=2 -xz -C /usr/share/nginx/html
WORKDIR /usr/share/nginx/html

# monkeypatch logo error
RUN cp img/medplum-logo-512-512.png img/medplum-logo-512x512.png
When serving via docker-compose like:
Copy code
medplum-frontend:
    image: medplum-app:latest
    build:
      context: ./medplum-app
    ports:
      - 8080:80
    environment:
      - MEDPLUM_BASE_URL=${MEDPLUM_FRONTEND_BASE_URL}
      - __MEDPLUM_BASE_URL__=${MEDPLUM_FRONTEND_BASE_URL}
      - MEDPLUM_CLIENT_ID=${MEDPLUM_FRONTEND_CLIENT_ID}
      - GOOGLE_CLIENT_ID=${GOOGLE_FRONTEND_CLIENT_ID}
      - RECAPTCHA_SITE_KEY=${RECAPTCHA_FRONTEND_SITE_KEY}
      - MEDPLUM_REGISTER_ENABLED=${MEDPLUM_FRONTEND_REGISTER_ENABLED}
    networks:
      - medplum
I receive these errors:
Copy code
Error: Base URL must start with http or https
    wT client.ts:777
    IQ index.tsx:31
    <anonymous> index.tsx:80
suppress-nextjs-warning.mjs:6:6
Retrieving "b5x-stateful-inline-icon" flag errored: timed out - falling back
the docker container of nginx indeed has the right variables set i.e.
Copy code
/usr/share/nginx/html # echo ${MEDPLUM_BASE_URL}
http://medplum:8103/
/usr/share/nginx/html # echo ${__MEDPLUM_BASE_URL__}
http://medplum:8103/
These variables are NOT set on the client i.e. the system which is browing the http://localhost:8080 of Nginx which is serving the frontend. Do you have any idea how to fix the loading for these env variables? It looks like the JS cannot properly resolve them.
The same is also true when serving via: http://localhost:8103/
I think it is not plain JS only and I need to run it via a node server - not only via Nginx?
It looks like the published package does not contain everything. When I use:
Copy code
# syntax=docker/dockerfile:1.7.1

FROM node:20.12.2-alpine as builder

WORKDIR /app

RUN apk add --no-cache git && \
     git clone https://github.com/medplum/medplum.git && \
     cd medplum && \
     git checkout v3.1.4

WORKDIR /app/medplum
RUN npm ci
RUN npm run build:fast -- --filter=app

FROM nginx:1.26.0-alpine3.19-slim as frontend

RUN rm /usr/share/nginx/html/50x.html && \
    rm /usr/share/nginx/html/index.html

COPY --from=builder /app/medplum/packages/app/dist /usr/share/nginx/html

WORKDIR /usr/share/nginx/html
I can move further. Interestingly, I still get a different result compared to running the frontend locally. when loading http://localhost:8080 - and the app frontend starts to talk to the server
Copy code
POST
http://localhost:8103/fhir/R4/$graphql
this fails with an unauthenticated code.
Strangely, when running the frontend locally no post request is issues and the login form is coming up nicely.
r
Hi @geoheil - the Medplum App distribution just requires static assets, no server, so you should be able to serve these assets w/ any server OR CDN of your choosing
Unfortunately, there's no way for the static JS to read the configuration variables from the environment
Instead, you will have to do a manual string replace inside the js files
g
But alternatively it should be possible to pass the variables during the build of the image?
@rahul1 I think this is working well:
Copy code
# syntax=docker/dockerfile:1.7.1

FROM node:20-slim as builder

WORKDIR /app

ARG MEDPLUM_VERSION

RUN apt-get update && \
    apt-get install -y git && \
     git clone https://github.com/medplum/medplum.git && \
     cd medplum && \
     git checkout v${MEDPLUM_VERSION}

WORKDIR /app/medplum

RUN npm ci --maxsockets 1

ARG MEDPLUM_BASE_URL
ARG MEDPLUM_CLIENT_ID
ARG GOOGLE_CLIENT_ID
ARG RECAPTCHA_SITE_KEY
ARG MEDPLUM_REGISTER_ENABLED

RUN npm run build:fast -- --filter=app

FROM nginx:1.26.0-alpine3.19-slim as frontend

RUN rm /usr/share/nginx/html/50x.html && \
    rm /usr/share/nginx/html/index.html

COPY --from=builder /app/medplum/packages/app/dist /usr/share/nginx/html

WORKDIR /usr/share/nginx/html

# Optional: Fix any file name issues (e.g., the logo file)
#RUN cp img/medplum-logo-512-512.png img/medplum-logo-512x512.png
#RUN cp assets/favicon-*.ico ../favicon.ico
with a compose of:
Copy code
medplum-frontend:
    image: medplum-app:latest
    build:
      context: ./services/medplum-app
      dockerfile: Dockerfile
      target: frontend
      args:
        MEDPLUM_VERSION: ${MEDPLUM_VERSION}
        MEDPLUM_BASE_URL: ${MEDPLUM_FRONTEND_BASE_URL}
        MEDPLUM_CLIENT_ID: ${MEDPLUM_FRONTEND_CLIENT_ID}
        GOOGLE_CLIENT_ID: ${GOOGLE_FRONTEND_CLIENT_ID}
        RECAPTCHA_SITE_KEY: ${RECAPTCHA_FRONTEND_SITE_KEY}
        MEDPLUM_REGISTER_ENABLED: ${MEDPLUM_FRONTEND_REGISTER_ENABLED}
@rahul1 However, unlike the locally built app and server (I am using your docker image) - and also the URL was changed to a local URL I now face the following issue when trying to load the app`s loging page:
Copy code
rror: Unauthenticated
    handleUnauthenticated https://app.mydomain.com/assets/index-GwzdS-hp.js:132
    request https://app.mydomain.com/assets/index-GwzdS-hp.js:132
    post https://app.mydomain.com/assets/index-GwzdS-hp.js:93
    graphql https://app.mydomain.com/assets/index-GwzdS-hp.js:132
    o https://app.mydomain.com/assets/index-GwzdS-hp.js:132
    requestSchema https://app.mydomain.com/assets/index-GwzdS-hp.js:132
    yB https://app.mydomain.com/assets/index-GwzdS-hp.js:522
    xf https://app.mydomain.com/assets/index-GwzdS-hp.js:83
    va https://app.mydomain.com/assets/index-GwzdS-hp.js:83
    mA https://app.mydomain.com/assets/index-GwzdS-hp.js:83
    j https://app.mydomain.com/assets/index-GwzdS-hp.js:68
    I https://app.mydomain.com/assets/index-GwzdS-hp.js:68
    EventHandlerNonNull* https://app.mydomain.com/assets/index-GwzdS-hp.js:68
    <anonymous> https://app.mydomain.com/assets/index-GwzdS-hp.js:68
index-GwzdS-hp.js:60:584
    error https://app.mydomain.com/assets/index-GwzdS-hp.js:60
    (Async: promise callback)
    catch https://app.mydomain.com/assets/index-GwzdS-hp.js:93
    yB https://app.mydomain.com/assets/index-GwzdS-hp.js:522
    xf https://app.mydomain.com/assets/index-GwzdS-hp.js:83
    va https://app.mydomain.com/assets/index-GwzdS-hp.js:83
    mA https://app.mydomain.com/assets/index-GwzdS-hp.js:83
    j https://app.mydomain.com/assets/index-GwzdS-hp.js:68
    I https://app.mydomain.com/assets/index-GwzdS-hp.js:68
    (Async: EventHandlerNonNull)
    <anonym> https://app.mydomain.com/assets/index-GwzdS-hp.js:68
    <anonym> https://app.mydomain.com/assets/index-GwzdS-hp.js:68
i.e. the docker based deployment fails with an authentication failure - the local one works just fine to get my own instance running
Even when pointing the locally running app/UI to the dockerized server it works - only when changing to the containerized deployment and updating the URL accordingly MEDPLUM_BASE_URL=http://localhost:8103/ I am stuck with auth failures
Fixed - identified the issue the Nginx serving the SPA hat to be reconfigured.
Is there interest I contrubte a nicely working docker-compose setup of medplum somewhere?
r
@geoheil that would be fantastic. If you open up a PR against our main repo, we can provide some guidance on the right place to put it Also, if you have any pointers on getting this working on non-aws, you could always provide an update our "Self-hosting" docs
g
r
Thanks for the PR @geoheil - the eng team will take alook
One thing I noticed is that you have checked in a private key. Can you please remove? we don't want to expose any sensitive security data in our github repo
Intead, you can just add a placeholder file
g
it is a dummy key I created with mkcert for the dummy dns myname.com
but maybe your are right and it is better to leave them out
o
Just found the discussion. nice setup. thanks for your effort @geoheil . quick question: @rahul1 would it be possible to create / run medplum bots in this setup (it is actually a lambda function ,right? ) Thanks