The advent of cloud-native computing has greatly transformed the landscape of building and distributing software. You have various tools to facilitate everything from integrating and packaging application code to deploying and scaling it. The practice of doing this is known as DevOps, and the concept that sits at the heart of modern-day DevOps is containers.
In this tutorial, you will learn to deploy a React app by creating a Docker image, pushing it to a Container Registry, and deploying it using a simple Droplet server.
docker
and npm
should be installed on your machine.Before proceeding, let’s walk through some basic terminology.
A container is “a standard unit of software that packages up code and all its dependencies so the application runs quickly and reliably from one computing environment to another.”
Imagine a container as a lightweight virtual machine running your application, and you can spin it up/down using a container image.
You cannot talk about containers without talking about container image, which is “a lightweight, standalone, executable package of software that includes everything needed to run an application: code, runtime, system tools, system libraries, and settings.”
A container image becomes a container at runtime. Containers make it really easy to package, deploy, distribute, and scale application code. They encapsulate your code and all the dependencies required to run it in a neat, multi-platform, easily replicable bundle.
One of the most well-known and widely used ways of containerization is using Docker. You can read about it in this article.
Now, let’s dive into how you actually containerise your application.
You will start off by installing Docker. Here’s a pretty good guide to get it up and running. Once it is set up, check the docker
version to ensure it’s running fine.
docker --version
Docker version 26.0.0, build 2ae903e
Next, you spin up the React app. You should have your React app on your local machine or a GitHub repository. If you have it on Github, clone the repo to your machine OR create a new sample react app using Vite, by running the following command:
npm create vite@latest react-app -- --template react
This should initialize a sample React app inside the react-app
directory. Let’s run the app to ensure it’s working properly.
cd react-app
npm install
npm run dev
Wait for your app to compile. Once it’s done, you should see a message similar to the following.
OutputVITE v5.2.11 ready in 712 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
And you should be able to view your app in your browser at localhost:5173
.
Great, the app is up and running on your machine. Now, all that remains is to package it somehow and get it to production.
Docker helps you containerize and run your application code using Docker/container images. To build such an image you use something called Dockerfile
which contains instructions to build the image. This image consists of many read-only layers and each Dockerfile
instruction adds a new layer to the image. Each of these layers is stored as a SHA-256
hash. You can learn more about the Docker image constitution here.
So you will create a Dockerfile
to build your app image and it will consist of two important steps:
nginx
to serve the app.Let’s go over each one.
Note: In the next two steps, all the instructions will go inside the Dockerfile
.
You will ask Docker to use the latest Node.js image as a base to build your React app. It’s like setting up the foundation for building construction – you need a solid starting point.
FROM node:latest AS builder
Establish the working directory within the container to execute all subsequent commands. Think of it as defining the workspace where your project will come to life.
WORKDIR /app
Now, you will copy your package.json
file into the container and run npm install
to install all the dependencies listed in the package.json
file. This step is crucial for ensuring that your React app has everything it needs to run smoothly.
COPY package.json .
RUN npm install
Next, copy the rest of your project files into the container and run npm run build
to compile the app. This step transforms the source code into a production-ready bundle optimized for performance and efficiency and places it under the dist/
directory.
COPY . ./
RUN npm run build
Nginx is a popular web server known for its speed and efficiency, making it ideal for serving your React app to the users. You will use the latest nginx
image as the base for your server.
Firstly, you will customize nginx
’s configuration by replacing its default settings with your own. This ensures that nginx
knows how to handle requests and serve the app correctly.
FROM nginx:latest
RUN rm -rf /etc/nginx/conf.d/default.conf
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
Then copy the compiled React app from the builder
container into the directory where nginx
expects to find the files it serves.
This step ensures that nginx
can access your app’s files and serve them to visitors.
COPY --from=builder /app/dist /usr/share/nginx/html
Set the working directory within the nginx
container to where your React app files are located.
WORKDIR /usr/share/nginx/html
Lastly, start the nginx
server with the following command. It tells Docker to launch nginx
and keep it running.
CMD ["/bin/bash", "-c", "nginx -g "daemon off;""]
Finally putting everything together, this is what your Dockerfile
will look like:
# ------------------------
# Step 1: Build react app
# ------------------------
# Use node:latest as the builder image
FROM node:latest AS builder
# Set the working directory
WORKDIR /app
# Copy package.json and install app dependencies
COPY package.json .
RUN npm install
# Copy other project files and build
COPY . ./
RUN npm run build
# --------------------------------------
# Step 2: Set up nginx to serve the app
# --------------------------------------
# Use nginx:latest as the base image
FROM nginx:latest
# Overwriting nginx config with our own config file
RUN rm -rf /etc/nginx/conf.d/default.conf
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
# Copy over the build created in the Step 1
COPY --from=builder /app/dist /usr/share/nginx/html
# Set the working directory
WORKDIR /usr/share/nginx/html
# Start nginx server
CMD ["/bin/bash", "-c", "nginx -g \"daemon off;\""]
So create a new file called Dockerfile
in the root directory of your app:
touch Dockerfile
And copy the contents of the Dockerfile
we created above inside this file.
You’re almost done. Just missing one crucial piece of the puzzle before you are ready to build the image. If you’re wondering where your nginx
config is, you’re right on the money. That’s the vital component you need to complete the picture.
To ensure step 2 works correctly, create a directory called nginx
at the root of your app and add an empty default.conf
file to the same directory.
mkdir nginx
cd nginx && touch default.conf
Copy the following configuration to default.conf
:
server {
listen 80;
add_header Cache-Control no-cache;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
expires -1;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
This asks nginx
to start listening on port 80
, specifying the files to serve, and configuring some default error based on the status code. You can read this tutorial to understand the nginx
configuration file structure and configuration contexts. Also, here’s a really handy UI tool for constructing nginx
config files.
With that, you are all set to build and deploy your image.
Make sure you cd
back to the root dir of your app – where the Dockerfile
is and run the following:
docker image build -t react-app:v1.0 . --platform linux/amd64
An image is identified by its name and tag, which are specified by the —t
flag. So, the name of your image is react-app
, and the tag is v1.0
. Notice that there’s a --platform
flag to specify the architecture for which you build the image. Usually, you can omit this, but sometimes, while building cross-platform images, it’s required so that the image is compatible with the platform it’s intended to run on. You can build the same image for multiple platforms if required.
You should see something similar to the following once the build finishes:
[+] Building 113.9s (17/17) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 982B
...
...
=> => writing image sha256:d125b102b094224c82ddb69f9ece98c7161c8660fe72fd5aec57e41a6d72cf2f
=> => naming to docker.io/library/react-app:v1.0
Verify that your image is built successfully. Use the following command:
docker image list
OutputREPOSITORY TAG IMAGE ID CREATED SIZE
react-app v1.0 d125b102b094 6 minutes ago 188MB
You should see your app in the list of images, as shown above.
Now that the image is ready, it’s time to prepare it for distribution and deployment. As mentioned earlier, the awesome part about an image is that it is self-sufficient and can be run on any platform that supports Docker.
Naturally, the image must be stored in a centralized location before it can be distributed and deployed.
That’s where a Container Registry comes into the picture.
Container Registry is a centralized repertoire of container images. It lets you store container images for rapid deployment. You can push your images to a Container Registry and then pull from anywhere – be it locally, from a Kubernetes cluster, CI/CD pipeline, etc. Let’s look at how to push the image to a Container Registry.
You must set up your Container Registry before you can start pushing images to it. You should use the private Container Registry by DigitalOcean. It is simple, private, and secure. You can choose from multiple tiers, and you can start by simply using the free tier, which offers up to 500MB of storage with a single repository—which should be more than enough to push your app image.
Setting up the DigitalOcean Container Registry is fairly simple and fast. You can follow these instructions to get started.
For this example, you can set up a free-tier registry in the BLR1
region and call it my-container-registry
.
Note: You need to have a DigitalOcean account to use Container Registry, so make sure to sign up if you haven’t already.
The next step is to get the API token to access the registry. You’ll need this later on. Go to the API Tokens page to generate the token.
Click on Generate New Token to see a form. Fill out the form details and select Full Access. Since you are the only one using this token, it shouldn’t really be a security concern.
An API token will be generated for you. Make sure to copy this token somewhere safe. Later, you will need it to log in to your registry.
Now let’s log in to the registry. Run the following in your terminal and enter the login credentials:
docker login registry.digitalocean.com
OutputUsername: ravish.foo@bar.com
Password:
Login Succeeded
Now, you are ready to push the image. So, let’s tag it for pushing.
docker tag react-app:v1.0 registry.digitalocean.com/my-container-registry/react-app
And push the image:
docker push registry.digitalocean.com/my-container-registry/react-app
OutputUsing default tag: latest
The push refers to repository [registry.digitalocean.com/my-container-registry/react-app]
5f70bf18a086: Pushed
fafde3127bc5: Pushed
155c640ab606: Pushed
993afd9f06ad: Pushed
9fd54926bcae: Pushed
175aa66db4cc: Pushed
e6380a7057a5: Pushed
1db2242fc1fa: Pushed
b09347a1aec6: Pushed
bbde741e108b: Pushed
52ec5a4316fa: Pushed
latest: digest: sha256:227a8c3ede3fc5c81b281228600bec84939f8a4a0cc770fc6e5527b3261e77f4 size: 2608
The image you built now sits inside the Container Registry and should appear on the cloud control panel.
As you can see, the image has been pushed to the registry and is tagged latest
. That’s because no tag name was specified while tagging the image for pushing. If no tag is specified, latest
is chosen as the default tag name.
You must note that you can tag your Docker image with any identifier you choose. One practical application of this feature is versioning your images.
For instance, let’s say you implement changes to your code and want to associate those changes with a specific release, such as v1.0
. By tagging your image with v1.0
and pushing it to your registry, you establish a clear reference point for that release. As you continue to update and release, you can incrementally increase the version numbers (e.g., v1.1
, v1.4
, v2.0
), providing a chronological record of your code releases within your registry. This systematic approach helps maintain a structured history of your app’s development and simplifies tracking and managing different versions over time.
You have built and pushed your image. Now, you only need to spin up a Droplet and deploy your container using the image.
Follow the guide here to spin up a droplet. The $6 variant should be good; set Ubuntu as the OS of choice. Once you have a Droplet running, you should see it in your control panel.
Click it open and copy the IP address of your droplet. As the arrow points out, you will find it right under the droplet’s name and details.
Using the IP address, you will now ssh
into the Droplet as root.
ssh root@143.244.130.234
root@143.244.130.234's password:
Based on the authentication method you chose while setting up the Droplet, you may or may not have to provide a password to ssh
into your Droplet.
Once you are successfully ssh
-ed into your Droplet, you should see a welcome message similar to the root prompt.
Welcome to Ubuntu 23.10 (GNU/Linux 6.5.0-9-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Mon Apr 29 08:07:53 UTC 2024
System load: 0.32 Processes: 106
Usage of /: 9.3% of 23.17GB Users logged in: 0
Memory usage: 40% IPv4 address for eth0: 143.244.130.234
Swap usage: 0% IPv4 address for eth0: 10.47.0.5
47 updates can be applied immediately.
To see these additional updates run: apt list --upgradable
*** System restart required ***
Last login: Mon Apr 29 08:07:55 2024 from 198.211.111.194
root@ubuntu-s-1vcpu-1gb-blr1-01:~#
You’re now inside the droplet.
Once you’re inside the droplet, you must pull and run the app image from your registry.
Check if you have Docker on your Droplet:
root@ubuntu-s-1vcpu-1gb-blr1-01:~# docker --version
Docker version 24.0.5, build ced0996
If not, install Docker:
root@ubuntu-s-1vcpu-1gb-blr1-01:~# snap install docker
Once installed, log into the registry again following the same steps as stated earlier:
root@ubuntu-s-1vcpu-1gb-blr1-01:~# docker login registry.digitalocean.com
Username: ravish.foo@bar.com
Password:
Login Succeeded
And pull the image:
root@ubuntu-s-1vcpu-1gb-blr1-01:~# docker pull registry.digitalocean.com/my-container-registry/react-app
OutputUsing default tag: latest
latest: Pulling from my-container-registry/react-app
b0a0cf830b12: Already exists
8ddb1e6cdf34: Already exists
5252b206aac2: Already exists
988b92d96970: Already exists
7102627a7a6e: Already exists
93295add984d: Already exists
ebde0aa1d1aa: Already exists
6156e0586e61: Already exists
88bdf71f3911: Pull complete
f5524cce8c8a: Pull complete
4f4fb700ef54: Pull complete
Digest: sha256:227a8c3ede3fc5c81b281228600bec84939f8a4a0cc770fc6e5527b3261e77f4
Status: Downloaded newer image for registry.digitalocean.com/my-container-registry/react-app:latest
registry.digitalocean.com/my-container-registry/react-app:latest
This pulls the image with the latest
tag. Remember that you can specify the tag you want to pull. Since the tag name was omitted, docker pulled the latest
one by default.
Now you have your container image, which consists of the latest React app code with all of the dependencies required to run it. If you recall, a container image is what becomes a container at runtime. So, let’s run the container using the image you just pulled now.
docker run -dp 80:80 registry.digitalocean.com/my-container-registry/react-app
The -d
flag detaches the container and runs it in the background. The -p 80:80
maps port 80
on the host to port 80
on the container, directing traffic to the container’s port.
Port 80
is exposed because it is the standard port for HTTP traffic and where nginx
would be listening for incoming requests.
Run the following to make sure your container is up:
root@ubuntu-s-1vcpu-1gb-blr1-01:~# docker ps -a
You should see your container in the list. Check the status - it should say something like “UP 2 seconds”. That means your container is up and running.
Congratulations! Your React app is now live on the internet and anyone can see it.
Type in the IP address of your Droplet in the browser, and you should be able to see and interact with your web app.
You can stop the container by running:
docker stop <container_id>
Once stopped, you can also remove the container by running:
docker rm <container_id>
You can go a step further and automate this process – write a bash script to integrate your GitHub so that every commit automatically builds an app image and pushes it to the Container Registry. You can also automate pulling the image from the Registry and deploying it to the cloud, creating a CI/CD pipeline of sorts. Tools like Concourse and GitHub Actions are a more sophisticated rendition of exactly this.
Further, you could use Kubernetes to manage a whole slew of containers to manage large-scale, production-grade services. DigitalOcean has it’s own managed Kubernetes that can be seamlessly integrated with DigitalOcean’s Container Registry and unlock the real potential of container-based cloud-native deployments.
You can read more about it here.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Sign up for Infrastructure as a Newsletter.
Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.