In this post, I will describe how to containerise a React project that was built with Waku and React Server Components. I will not build the app here but focus on creating the image and container.
Prerequisites
Make sure you have Docker and Node.js installed on your machine. I was using Node v18.19.0 and Docker v24.0.5. I built my app with Typescript v5.3, Tailwind CSS v3.4, shadcn-ui and Waku v0.20.
What you really want to know
For those that only want the meat and potatoes.
Let's take a look at Dockerfile
.
FROM node:18-alpine as builder
WORKDIR /app
# import dependencies
COPY package.json .
COPY package-lock.json .
RUN npm run install
COPY public/ public/
COPY src/ src/
# UI library/tools/TS config files
COPY components.json .
COPY tailwind.config.js .
COPY postcss.config.js .
COPY tsconfig.json .
RUN npm run build
# Check if build succeeded
RUN [ -d "/app/dist" ] || exit 1
EXPOSE 5000
#start app
CMD ["npm", "run", "start"]
I also used a .dockerignore
file like so:
/node_modules
.vscode
Then I used a docker-compose.yml
file to build the container, like so:
version: '3.8'
services:
frontend:
build:
context: .
dockerfile: ./Dockerfile
container_name: restaurants-fe
image: restaurantsapp
working_dir: /app
environment:
NODE_ENV: production
ports:
- 5000:8080
Finally, I built the container using the following command:
docker-compose up -d --build
Explanation
If you want a bit more information, here are some details on the setup.
Let's start with Dockerfile
. I set it up in a way that it uses Docker's multi-stage build paradigm, in which different layers of the process are cached and reused on subsequent builds if the underlying files have not changed since the previous build. Here's what the commands do:
- FROM: use a Node v18 base image to scaffold the build process
- WORKDIR: set a dir path where all the following commands take place
- COPY: copy dependency info in the working dir (signified by the **dot** at the end)
- RUN install: install project dependencies
- COPY public/src: copy other assets (images, jsx files etc.)
- COPY config: copy config files needed for tailwind and shadcn
- RUN build: build the app with production settings
- EXPOSE: make a port available from the container
- CMD: run the start command
If you wish, you can include a health check command at the end of the Dockerfile
, which will stop the container if the app is down. Something like this:
HEALTHCHECK --interval=30s CMD wget -qO- http://localhost:5000 || exit 1
Now a quick tour of docker-compose.yml
. Of course, this is not necessary for a single container, but I may add more parts to the app in the future and then I can simply add new services to this file. In describing the frontend service:
- I specify the build context ( cwd ) and the dockerfile path.
- I add a container_name, image name, and working dir path for good measure
- I specify any environment variables needed
- I specify port forwarding from the container to the local machine (I used port:5000, but Waku runs on port:8080 by default. You can use that one too, of course)
Finally, I build the container and start it up with the command:
docker-compose up -d --build
where the -d
flag starts the container in the background.
With that, the container should be ready to be deployed to a cloud environment. I leave the topics of 'Docker hub' and 'Deploying Docker containers' to your Googleing skills.
A word about the Waku app
To start building a waku app, take a look at all the examples and starters on Waku's Github. Here I will provide only minimal info for context and potential troubleshooting. The app is a simple website showing info on various restaurants' menus in the area.
This is my project directory structure.
All other scaffolding code comes from a starter example or the basic template installed with create waku@latest
.
In the off-chance that you are building with waku v0.19, I have the alternatives as well. Here is the folder structure.
My waku app's (waku v0.19) file tree (essentials only)
And this is the entries.tsx
file:
import { createPages } from 'waku';
import React from 'react'
import { RootLayout } from './templates/root-layout.js';
import { HomePage } from './templates/home-page.js';
import { AboutPage } from './templates/about-page.js';
export default createPages(async ({ createPage, createLayout }) => {
createLayout({
render: 'static',
path: '/',
component: RootLayout,
});
createPage({
render: 'dynamic',
path: '/',
component: HomePage,
});
createPage({
render: 'static',
path: '/about',
component: AboutPage,
});
});
All other scaffolding code originates just as described above.
What I could not do
In order to decrease the size of the resulting container, I also tried to create an Apache server in the container and copy the static files into its base folder. Similar to what's in this article. I did the following:
.... previous commands ....
EXPOSE 5000
FROM httpd:alpine
WORKDIR /usr/local/apache2/htdocs/
COPY --from=builder /app/dist .
COPY --from=builder /app/dist/public/index.html .
These instructions copy the static JS files and assets from the /dist
folder of the app into the root folder of the Apache server. It expects that the index.html
file is in this directory, but Waku puts it inside the public directory, so we copy the index file as well. But then, not surprisingly, the relative paths of files referenced in index.html
are wrong :-( .
I have no experience configuring http servers, so if anyone does and would like to help out, it would be much appreciated.
Acknowledgement
This post was written on the shoulders of other great tutorials on Docker and React apps. I recommend them for further reading. These are sources from howtogeeks, freecodecamp, Knowledgehut, Antonio Maccarini on Medium and Dan Murphy on Towards Data Science.
Conclusion
Here I show how to package a React app built with the new and awesome Waku JS framework into a Docker container for easy deployment on various platforms. As is probably evident from the above, I am not very experienced with Docker and this was an excellent learning opportunity for me. If you have any comments, suggestions or (reasonable) critique :-) I would be very happy to hear from you.
Top comments (0)