How to run a Create React App development environment using Docker, Docker-Compose, and Traefik.
You are about to learn:
- How to Dockerize a React App for development
- How to properly manage dependencies in a Dockerized environment
- How to build a custom Docker image and control the build context
- How to run a reverse proxy with Traefik
- How to map a Docker-Compose service to a local URL
- How to map a Docker-Compose service to a custom domain
- Quick Start
- Create the Project's Folder
- Create (a) React App
- Build the Developement Container
- Run the Docker-Compose Project
- Add New Dependencies
- Add the Reverse Proxy
- Proxy the React App
- Use a Custom Domain
If you want a feeling of what you are about to build, follow these instructions:
- Clone the repo:
git clone [email protected]:marcopeg/cra-docker-traefik.git
- Open the project:
cd cra-docker-traefik
- Start the project:
docker-compose up
- Test is with a browser:
http://app.localhost
- Play with the Traefik's console:
http://localhost:8080
NOTE: You need Docker-Compose running on your laptop, and ports
80
and8080
to be available.
As first step, let's just initialize a new project.
I usually do that by creating a new repo under my GitHub's account so that I can easily scaffold it with a basic README and LICENCE file.
Once I'm done, I clone it and get into my project's root:
# Clone my Project:
git clone [email protected]:marcopeg/cra-docker-traefik.git
# Get into the Project's root:
cd cra-docker-traefik
# Start VSCode on my Project:
code .
Over the years I tried many approaches to running a Webpack based App with React. Eventually I settled for Create React App.
It doesn't give me everything I want, or the way I'd have done it myself, but it takes away most of the hassles of running a modern React-based projects and let me focus on my business logic.
# Add a React App to my Project
npx create-react-app .
Usually, I would run an npm start
, open http://localhost:3000
on my Google Canary, and start writing my components.
But today, the goal is different:
- run
docker-compose up
to start my system - open
http://app.localhost
on my browser
There is much literature about running a development container for Webpack and React. Luckily, things got much easier lately.
I like the approach with a custom Dockerfile
that is responsible for installing the NPM Dependencies:
# ----------
# Dockerfile
# ----------
FROM node:16
# Install NPM dependencies:
WORKDIR /usr/src/app
ADD package.json /usr/src/app
RUN npm install
# Run the App:
WORKDIR /usr/src/app
ENTRYPOINT [ "npm", "run", "start" ]
# Configure the Container:
EXPOSE 3000
Explicitly exposing port
3000
helps us flowing CRA's default values to the Traefik's reverse proxy, effectively DRY-ing the configuration that we need to maintain.
It is important to pair this file with a .dockerignore
as so to optimize the amount of data that are passed to the Docker's daemon during the build:
# -------------
# .dockerignore
# -------------
node_modules
build
The two files put together, describe to Docker how to build our Development Container.
Please note that we didn't copy the source code. The
public/
andsrc/
folders will be linked at run time by Docker-Compose.
We can test the build by running:
docker build .
Docker-Compose offers a simple interface for running projects with one or multiple containers.
I use it even for my simplest projects because I appreciate the declarative nature of it docker-compose-yml
:
# ------------------
# docker-compose.yml
# ------------------
version: "3.8"
services:
app:
build: .
volumes:
- ./public:/usr/src/app/public:delegated
- ./src:/usr/src/app/src:delegated
tty: true
ports:
- "3000:3000"
The build .
instruction let Docker-Compose know that we want it to use a custom container using the Dockerfile
and .dockerignore
that we've prepared.
The volumes:
instruction let us link the Project's source folders into the running container so that any change we make will be immediately picked up by the Webpack running inside such container.
tty
makes the logs nice to read but it is not required.
Finally, the ports:
instruction links the process running inside the container to one of our computer's ports.
We are going to remove this instruction later on, when we get to the Traefik reverse proxy part.
To test that all is running properly you just need to run:
docker-compose up
Open your browser to http://localhost:3000
and enjoy a fully working React App.
Also, try to modify arc/App.js
to prove that the hot-reload is properly working for you.
You have 2 possible ways to add new dependencies to your project:
- Edit & Rebuild: add dependencies programmatically
- SSH & Install: add dependencies dynamically
But the end-game of both methods is to make sure that your dependencies are always correctly saved in the project's
package.json
manifest.
Let's say you want to add [Axios][axios] and make some REST calls to some backend.
First, go to [NPM][npm] and find the package you want to add, and its current version.
At the time of writing, the current version is 0.27.2
.
With this info, edit your package.json
and add the dependency:
{
"dependencies": {
"axios": "^0.27.2"
}
}
Now it's just a matter of restarting the project AND rebuild the development container:
docker-compose up --build
This method IS EXPLICIT and it is the one I recommend to use.
It sucks a bit that rebuilding the container is a slow operation. But everyone need coffe once in a while, am I right?
This method consists into connecting to the running container's terminal (via docker exec
) and run the npm add xxx
command in there.
Before we proceed, we need to map a new volume into the app
service, that is to make sure that the dependencies that we add dynamically will be properly persisted in the App's package.json
manifest:
app:
volumes:
- ./package.json:/usr/src/app/package.json:delegated
Restart your project.
Now you can use a separated terminal session to connect into the running container:
docker-compose exec app /bin/bash
And from here, you can finally install Axios:
npm add axios
This method feels faster, but it's easy to miss a dependency this way.
Remember:
- map your
package.json
volume- use
npm add
that always edit the manifest (nonpm install
!)- double check your
package.json
and verify the depenency was properly tracked down
I discourage you from using this method as it increases the chances to miss tracking a dependency in the package.json
, which is a very common mistake.
Nevertheless, I use it in the beginning of any project when adding new dependencies happens by the minute. I really don't want to keep rebuilding the environment: too many coffees!
When the project gets more stable, I remove the package.json
volume and ask my team to use the programmatic method only.
Traefik is the new cool kid in the block when it comes to proxies. Under many aspects, it makes NGiNX feels like a dinosour from the ancient times when we still used to write endless config files.
First, let's add a Traefik container to our docker-compose.yml
:
# ------------------
# docker-compose.yml
# ------------------
version: "3.8"
services:
traefik:
image: "traefik:v2.8"
command: "--api.insecure --providers.docker"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
ports:
- "80:80"
- "8080:8080"
This piece of compose config is basically taken straight out of the official documentation:
https://doc.traefik.io/traefik/user-guides/docker-compose/basic-example/
The combination of
--api.insecure=true
and exposingport 8080
let you access the Traefik's console by aiming your browser to:http://localhost:8080
.You don't technically need it to expose the React App, and you definetly don't expose it when running in production!
The last piece of the puzzle is to properly configure the access to the React App.
Since we use Docker and Traefik, we can do it by adding labels to the app
's container:
# ------------------
# docker-compose.yml
# ------------------
version: "3.8"
services:
app:
labels:
- "traefik.http.routers.app.rule=Host(`app.localhost`)"
๐ Restart the project and enjoy your React App available at http://app.localhost
.
localhost
comes as preconfigured routed host name in most systems. If it doesn't work, you may need to edit the/etc/hosts
file.
Now that you have a full docker-compose.yml
to play with, you may want to start messing around with custom domains.
Let's say that your App should end up working on http://foobar.com
, and you want to use this particular DNS while developing.
First, you can adjust your Host setting in the container's lable:
traefik.http.routers.app.rule=Host(`foobar.com`)
Second, you shoud edit your /etc/hosts
adding the rule:
127.0.0.1 foobar.com
๐ Restart the project, and enjoy your React App available at http://foobar.com
.