Salvador Hernandez
Posted on October 31, 2020
When I'm developing a React Application and want to compare the output of the current work with another branch like develop
on my local I have to go through a series of time-consuming steps to get develop
branch up and running. Because of this ReactBranchContainer was born.
Steps without ReactBranchContainer:
- Stash local changes with
git stash
- Checkout
develop
branch withgit checkout develop
- Re-install dependencies
npm install
- Re-install dependencies when using new libraries
- When using SASS: Generate CSS from SCSS files with
npm run css
- Run application
npm run start
- Go to
localhost:3000
to see output
Solution
I embarked on a journey to create a tool that would allow me to keep run the application with the use of Docker, Dockerfile, Docker-Compose and a bash
script. Now I can compare my current work with another branch side-by-side.
Steps with ReactBranchContainer:
- Run
./rbc.sh -b develop
- Go to
localhost:5000
to see output
Demo
Left window is watching for latest code changes and serving them on localhost:3000
and right window is running a Docker container with develop
branch on localhost:5000
.
I spent many hours creating a tool that alleviates a 5 minute problem. Is the Remedy worst than the decease? Maybe. Did I have fun learning about Docker? Yes. Am I a Docker know-it-all? No.
What I learned
- How to add Private SSH Key to Node Docker Image
- How to pass build arguments(
--build-arg
,ARG
) fromdocker-compose.yml
to.Dockerfile
- How to access environment variables in
docker-compose.yml
- How to containerize a React Application in a Docker image
- How to accept flags in bash script with
getopts
How to use it
Setup
Before using it, you need to update rbc.sh
with the proper variables:
-
REPOSITORY_URL
- Contains the SSH URL to your repository
-
PROJECT_NAME
- Name of the project (name of repository)
-
BRANCH_NAME
- Branch to build
- Defaults to
develop
-
NODE_VERSION_REACT_APP
- Node version used to develop React Application
- Defaults to
latest
- This is used to create pull correct image:
node:latest
-
LOCAL_PORT
- Port used by host
- Defaults to
5000
-
REACT_CONTAINER_PORT
- Port used by react application
- Defaults to
3000
-
ID_RSA_PATH
- path to SSH RSA Key
# Project Information
REPOSITORY_URL="git@github.com:salhernandez/test-react.git"
PROJECT_NAME="test-react"
BRANCH_NAME="develop"
NODE_VERSION_REACT_APP="latest"
# default create-react-app port
REACT_CONTAINER_PORT=3000
LOCAL_PORT=5000
# path to SSH RSA KEY
ID_RSA_PATH="/c/Users/User/.ssh/id_rsa"
rbc.sh
will copy the SSH key into the container and use it to pull the repository.
Run it
# run with defaults
./rbc.sh
# access application via localhost:5000
# run with a specific branch
./rbc.sh -b bug-fix
# access application via localhost:5000
# run with a specific branch and set local port
./rbc.sh -b bug-fix -p 4001
# access application via localhost:4001
# run with a specific branch, set local port and container port
./rbc.sh -b bug-fix -p 4001 -c 3001
# access application via localhost:4001
Under the hood
What you need
-
Working SSH private key that has access to your GitHub account
- This can also be configured for other remotes
- Docker
- Bash Shell
rbc.sh
BRANCH_NAME, PROJECT_NAME, REPOSITORY_URL, REACT_CONTAINER_PORT, and ID_RSA_PATH
are passed into docker-compose build
as build-time variables(--build-arg
) and IMAGE_NAME
is added as an environment variable with a value of ${PROJECT_NAME}/${BRANCH_NAME}:latest
which translates to test-react/develop:latest
# build image
docker-compose build \
$BUILD_CACHE \
--build-arg BRANCH_NAME=$BRANCH_NAME \
--build-arg PROJECT_NAME=$PROJECT_NAME \
--build-arg REPOSITORY_URL=$REPOSITORY_URL \
--build-arg REACT_CONTAINER_PORT=$REACT_CONTAINER_PORT \
--build-arg NODE_VERSION_REACT_APP=$NODE_VERSION_REACT_APP \
--build-arg SSH_PRIVATE_KEY="$(cat ${ID_RSA_PATH})"
# translates to
docker-compose build \
$BUILD_CACHE \
--build-arg BRANCH_NAME="develop" \
--build-arg PROJECT_NAME="test-react" \
--build-arg REPOSITORY_URL="git@github.com:salhernandez/test-react.git" \
--build-arg REACT_CONTAINER_PORT=3000 \
--build-arg NODE_VERSION_REACT_APP="latest" \
--build-arg SSH_PRIVATE_KEY="$(cat /c/Users/User/.ssh/id_rsa)"
After the image is built, it will be tagged with the name test-react/develop:latest
.
Then it runs the image
# in interactive mode
docker run -it --rm -p $LOCAL_PORT:$REACT_CONTAINER_PORT $IMAGE_NAME
# translates to
docker run -it --rm -p 5000:3000 test-react/develop:latest
docker-compose.yml
BRANCH_NAME, PROJECT_NAME, REPOSITORY_URL, REACT_CONTAINER_PORT, and SSH_PRIVATE_KEY
are passed into .Dockerfile
as build-time variables(ARG
). Image will have the name defined by environment variable IMAGE_NAME
version: '3.7'
services:
the_container:
image: ${IMAGE_NAME} # environment variable
build:
context: ./
dockerfile: .Dockerfile
args:
BRANCH_NAME: ${BRANCH_NAME} # --build-arg
PROJECT_NAME: ${PROJECT_NAME} # --build-arg
REPOSITORY_URL: ${REPOSITORY_URL} # --build-arg
REACT_CONTAINER_PORT: ${REACT_CONTAINER_PORT} # --build-arg
NODE_VERSION_REACT_APP: ${NODE_VERSION_REACT_APP} # --build-arg
SSH_PRIVATE_KEY: ${SSH_PRIVATE_KEY} # --build-arg
stdin_open: true
# translates to
version: '3.7'
services:
the_container:
image: test-react/develop:latest # environment variable
build:
context: ./
dockerfile: .Dockerfile
args:
BRANCH_NAME: develop # --build-arg
PROJECT_NAME: test-react # --build-arg
REPOSITORY_URL: git@github.com:salhernandez/test-react.git # --build-arg
REACT_CONTAINER_PORT: 3000 # --build-arg
NODE_VERSION_REACT_APP: latest # --build-arg
SSH_PRIVATE_KEY: <private_key> # --build-arg
stdin_open: true
.Dockerfile
Using ARG
s the dockerfile does the following:
- Uses
node:<NODE_VERSION_REACT_APP>
as base image - Sets
ARG
s - Sets working directory
- Copies SSH RSA key into the container
- Clones repository from
REPOSITORY_URL
- Sets working directory again, but now it is based on the project folder cloned
- Installs dependencies
- Removes SSH key
- Exposes port to be used by the application:
REACT_CONTAINER_PORT
- Runs the application with
npm start
# latest version of Node.js
ARG NODE_VERSION_REACT_APP="latest"
ARG DOCKER_NODE_IMAGE="node:${NODE_VERSION_REACT_APP}"
# Builds from node image, defaults to node:latest
FROM "${DOCKER_NODE_IMAGE}"
# Will only be used once
ARG SSH_PRIVATE_KEY=0
ARG BRANCH_NAME=0
ARG REPOSITORY_URL=0
ARG PROJECT_NAME=0
ARG REACT_CONTAINER_PORT=3000
ARG BASE_WORKDIR="/app"
ARG PROJECT_WORKDIR="${BASE_WORKDIR}/${PROJECT_NAME}"
# Set working directory
WORKDIR "${BASE_WORKDIR}"
# Setup SSH
RUN mkdir ~/.ssh/
RUN echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa
# Make sure your domain is accepted
RUN ssh-keyscan github.com >> ~/.ssh/known_hosts
RUN chmod 0600 ~/.ssh/id_rsa
# Clone repository via SSH
RUN git clone "${REPOSITORY_URL}"
# Set working directory again, now we're inside the react project itself
WORKDIR "${PROJECT_WORKDIR}"
# Get all branches from remote
RUN git fetch
# Checkout branch
RUN git checkout "${BRANCH_NAME}"
# Install dependencies
RUN npm install
RUN npm install react-scripts
# Remove SSH KEY
RUN rm -rf ~/.ssh/
# Expose port which is used by the actual application
EXPOSE $REACT_CONTAINER_PORT
# Finally runs the application
CMD [ "npm", "start" ]
Bundling it all together
rbc.sh
runs two commands, one to build the image, and one to run it.
# build image
docker-compose build \
$BUILD_CACHE \
--build-arg BRANCH_NAME=$BRANCH_NAME \
--build-arg PROJECT_NAME=$PROJECT_NAME \
--build-arg REPOSITORY_URL=$REPOSITORY_URL \
--build-arg REACT_CONTAINER_PORT=$REACT_CONTAINER_PORT \
--build-arg NODE_VERSION_REACT_APP=$NODE_VERSION_REACT_APP \
--build-arg SSH_PRIVATE_KEY="$(cat ${ID_RSA_PATH})"
# run image
docker run -it --rm -p $LOCAL_PORT:$REACT_CONTAINER_PORT $IMAGE_NAME
# go to localhost:5000 to see the live react app
Warning!
DO NOT USE THIS TO PUSH AN IMAGE TO DOCKER HUB! If you run docker history <image_name> --no-trunc
you will see all the variables passed into the image like your ID_RSA token! This should only be used for development purposes only! More information [here].(https://docs.docker.com/engine/reference/commandline/history/)
For a more secure way to pass build secret information use BuildKit: New Docker Build secret information
BuildKit is still experimental and not supported by Windows
Useful information
Since this will be generating new containers, you will want to clean up intermediate and unused containers every now and then, use the following commands to help you free up some space:
Docker provides a single command that will clean up any resources — images, containers, volumes, and networks — that are dangling (not associated with a container):
docker system prune
To additionally remove any stopped containers and all unused images (not just dangling images), add the a
flag to the command:
docker system prune -a
Helpful URLs
- Docker ARG, ENV and .env - a Complete Guide
getops
- Access Private Repositories from Your Dockerfile Without Leaving Behind Your SSH Keys
- Fetching private GitHub repos from a Docker container
Checkout the project on GitHub.
Q&A
What problem did you try to fix with a tool/project because you did not want repeat a series of tasks?
Posted on October 31, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.