Dockerize a React App with Docker Compose
Build a containerized React and Express app orchestrated with Docker Compose.
Introduction
30 Second Summary
Most apps you use every day have at least two parts: a frontend you see in your browser, and a backend that serves data behind the scenes. Keeping those parts running together on any machine is exactly the problem containers solve.
In this project, you will build a React frontend and an Express backend, then containerize each one with its own Dockerfile. You'll wire them together using Docker Compose so your entire app starts with a single command.
What You'll Build
You'll open your browser and see a React page displaying live messages from an Express backend, both running in separate Docker containers launched by a single command.
By the end of this project, you'll have:
- A production-ready React app served by Nginx inside a Docker container, built using a multi-stage Dockerfile that keeps the final image tiny.
- A multi-container application where a React frontend and Express backend run as separate services, orchestrated by a single Docker Compose file.
- Hands-on experience with container networking, where your frontend automatically discovers and routes requests to the backend by service name.
- Secret Mission: Add health checks and a custom Docker network to make your Compose setup production-ready.
Are there any prerequisites?
This project builds on the "Deploy an App with Docker" NextWork project. You should be comfortable with building Docker images and running containers before starting.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Scaffold and Run a React App
Before you can containerize anything with Docker, you need an app to put inside a container. This project uses a React frontend, so your first job is to get a working React app on your machine.
You'll use Vite to scaffold a complete React starter app in seconds. Vite generates all the files and build tooling you need, so you can spend your time learning Docker instead of writing React code from scratch.
In this step, get ready to:
- Scaffold a new React project using Vite.
- Install the project dependencies with npm.
- Confirm the app runs locally in your browser.
Scaffold your React project
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
macOS
cd ~/Desktop
Windows
cd ~\Desktop
- Scaffold your new React project by running the command below.
- You will see two prompts. When npm asks permission to install the create-vite package, type y and press Enter.
- When it asks to install dependencies and start the dev server now, select No and press Enter. You will do these steps manually so you can see each part in action.
npm create vite@latest react-docker -- --template react
What does this command do?
npm create vite@latest downloads and runs the Vite scaffolding tool. react-docker is the name of the folder it creates for your project.
The -- --template react part tells Vite to use the React template. The extra -- is required by npm 7+ to pass flags through to the underlying tool.
- Confirm the react-docker folder was created by running:
ls
You should see react-docker in the output.
Don't see react-docker?
Make sure you ran the scaffold command from your Desktop. Run pwd to check your current directory.
If you see a permission error, check that Node.js and npm are installed by running npm --version.
help me troubleshoot the Vite scaffold command
- Move into your new project folder and install its dependencies by running these commands:
cd react-docker
npm install
What does npm install do?
npm install reads the package.json file that Vite generated and downloads every package your React app needs. It creates a node_modules folder containing those packages.
You should see output listing the installed packages. This takes a few seconds.
npm install failing?
Check that you are inside the react-docker folder. Run pwd to confirm your current directory ends with react-docker.
If you see network errors, check your internet connection and try running the command again.
help me fix npm install errors
Start the development server
Your project is scaffolded and all dependencies are installed. Time to start the app and confirm everything works.
Before you run the next command, what do you think you'll see when you open the app in a browser?
- Start the Vite development server by running:
npm run dev
What is happening in the terminal?
The dev server takes over your terminal window. You'll see a URL in the output, typically http://localhost:5173. Leave the terminal running while you open the app.
If port 5173 is already in use on your machine, Vite automatically picks the next available port. Use whichever URL your terminal shows.
- Open http://localhost:5173 in your browser (or whichever URL your terminal displayed).
You should see the Vite + React starter page with the Vite and React logos and a counter button that increments when you click it.
- Go back to your terminal.
- Press Ctrl+C (both macOS and Windows) to stop the dev server.
Page not loading in the browser?
Make sure the dev server is still running in your terminal. If you accidentally closed it, run npm run dev again from inside the react-docker folder.
Double-check that the URL in your browser matches the URL shown in your terminal output. Vite may use a different port if 5173 was already taken.
help me get the Vite dev server running
Your React app is scaffolded and running locally. Next up, you'll containerize it inside a Docker container using a multi-stage build.
Dockerize the React App with a Multi-Stage Build
Your React app is up and running locally. Now it is time to package it into a Docker container so it can run anywhere, on any machine, without needing Node.js installed.
In your previous Docker project, you used a simple single-stage Dockerfile that copied an HTML file straight into Nginx. A real React app is different. It needs to be compiled from JSX into plain HTML, CSS, and JavaScript files before Nginx can serve them. A multi-stage build handles both steps in one Dockerfile, keeping the final image tiny.
In this step, get ready to:
- Create a multi-stage Dockerfile that compiles your React app and serves it with Nginx.
- Write an Nginx config and a .dockerignore file to support the build.
- Build a Docker image and run your React app inside a container.
Create the Dockerfile, Nginx config, and .dockerignore
Your multi-stage Docker build needs three configuration files in the project root. The first is a .dockerignore that keeps unnecessary files out of the build context.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the react-docker folder.
- Click Open.
You should see your project files listed in the Explorer sidebar on the left. You will create and edit files here throughout the rest of the project.
- In the Explorer sidebar, hover over the project name and click the New File icon.
- Type .dockerignore and press Enter.
- Paste the following content into the file:
node_modules
dist
.git
.gitignore
What does .dockerignore do?
When Docker builds an image, it sends every file in the project folder to the Docker engine as "build context." The .dockerignore file tells Docker to skip files that the container does not need.
Excluding node_modules is especially important. The build stage installs its own fresh dependencies inside the container, so sending your local 200MB+ node_modules folder would just slow the build down.
- In VS Code, create a new file called Dockerfile (no file extension) in your react-docker folder and paste the following content:
# Build stage
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
What does this Dockerfile do?
This Dockerfile has two stages, which is why it is called a multi-stage build.
- The build stage starts from node:24-alpine, installs your npm dependencies, and runs npm run build to compile your React code into plain HTML, CSS, and JavaScript files inside a dist/ folder.
- The production stage starts fresh from nginx:alpine. It copies only the compiled files from the build stage using COPY --from=build. None of the Node.js tooling comes along.
- The result is a final image around 30-50MB instead of the ~1GB you would get if you shipped Node.js and all your dev dependencies.
- Save both .dockerignore and Dockerfile.
- Confirm both files exist by running:
ls
You should see Dockerfile and .dockerignore listed alongside your existing project files like package.json and src/.
Don't see .dockerignore in the output?
Files starting with a dot are hidden by default on macOS and Linux. Run ls -a instead to show hidden files. You should see .dockerignore in the list.
If the file is missing entirely, make sure you created it inside the react-docker folder and not in a subfolder.
Help me find my .dockerignore file in my react-docker project.
✔️ Awesome, I've got everything!
Great. Make sure you have saved your Dockerfile before moving on.
ⓧ I'd like to double check the full code
# Build stage
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
- In VS Code, create a new file called nginx.conf in your react-docker folder and paste the following content:
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://api:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
What does this Nginx config do?
- try_files $uri $uri/ /index.html enables SPA (single-page app) routing. If a visitor navigates to a path like /about, Nginx serves index.html and lets React handle the route on the client side.
- The gzip block compresses text-based files so they load faster in the browser.
- The /api/ location block is a reverse proxy rule that forwards API requests to a backend server. You will connect this in a later step when you add an Express API.
- The last location block tells browsers to cache static assets like JavaScript, CSS, and images for one year.
- Save nginx.conf.
- Confirm all three configuration files are in place by running:
ls
You should see Dockerfile, nginx.conf, and your existing project files listed. Remember that .dockerignore is hidden by default. Run ls -a if you want to verify it too.
Missing a file?
Make sure all three files are in the react-docker root folder, not inside src/ or any subfolder. The Dockerfile must have no file extension. If your editor saved it as Dockerfile.txt, rename it.
Help me check that my Docker configuration files are set up correctly.
✔️ Awesome, I've got everything!
Great. Make sure nginx.conf is saved before moving on to the build.
ⓧ I'd like to double check the full code
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://api:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Build the Docker image
All three configuration files are in place. Time to build a Docker image from your multi-stage Dockerfile.
- Build the image by running this command:
docker build -t react-app .
Watch the terminal output. You can see the two-stage build in action. Docker first runs the build stage (installing Node.js dependencies and compiling your React app), then switches to the production stage (copying the compiled files into a clean Nginx image).
What does docker build -t react-app . do?
- docker build tells Docker to read the Dockerfile and assemble an image from it.
- -t react-app tags the image with the name react-app so you can refer to it later.
- The . at the end tells Docker to use the current directory as the build context (the files sent to the Docker engine).
🙋♀️ Build failing with an error?
If you see a "no such file or directory" error mentioning nginx.conf, make sure nginx.conf is in the react-docker root, not inside a subfolder.
If you see a "Cannot connect to the Docker daemon" error, make sure Docker Desktop is running. Open it from your system tray or by searching for it.
Help me fix my Docker build error.
Run the container and open it in the browser
Your image is built. Let's start a container from it and see the React app served by Nginx.
- Start a container from your new image by running:
docker run -d -p 8080:80 react-app
What does this command do?
- -d runs the container in the background (detached mode) so your terminal stays free.
- -p 8080:80 maps port 8080 on your machine to port 80 inside the container, where Nginx is listening.
- react-app is the image name you tagged in the build step.
Before you open the browser, think about what you expect to see. Will it be the same Vite + React starter page you saw locally, or something different?
- Open http://localhost:8080 in your browser.
You should see the same Vite + React starter page with the counter button. This time, it is not running through Node.js. It is running inside a Docker container, served by Nginx.
How is this different from your previous Docker project?
In your previous project, you copied a static index.html file directly into Nginx. This time, the Dockerfile compiled an entire React application into static files first, then handed those files to Nginx. Same Nginx, but now serving a real compiled web app through a multi-stage build.
🙋♀️ Not seeing your app at localhost:8080?
Run docker ps to check if the container is running. If the list is empty, the container may have exited. Run docker ps -a to see stopped containers and docker logs <container-id> to read the error.
If port 8080 is already in use, you will see a bind error. Stop whatever is using that port or change the mapping to -p 9090:80 and visit http://localhost:9090 instead.
Help me get my React Docker container running.
Your React app is running inside a production-ready Docker container. Next up, you will build a backend API server, containerize it separately, and discover why managing multiple containers by hand gets painful fast.
Add a Backend API
Your React app is running inside a Docker container, served by Nginx. It looks great, but it only shows static content. A real full-stack application needs a backend to serve data.
In this step, you will create a simple Express API server, containerize it in its own Docker image, and discover what happens when you try to connect two independent containers.
In this step, get ready to:
- Create an Express backend API with a messages endpoint.
- Build and run the API in its own Docker container.
- Discover the limits of running containers independently.
Create the Express API server
Express is a lightweight Node.js web framework for building APIs. You will use it to create a single endpoint that returns a list of messages as JSON.
- Create a new api folder inside your react-docker project by running this command:
mkdir api
- In VS Code, create a new file at api/package.json and paste the following code:
{
"name": "api-server",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^5.2.1"
}
}
What does this file do?
- "type": "module" enables ES module imports (import instead of require), keeping your backend consistent with the React frontend.
- The express dependency is pinned to version ^5.2.1, the latest active release of Express.
- Save api/package.json.
- Install the Express dependency by running these commands:
cd api
npm install
You should see output confirming that express was installed. This also generates a package-lock.json file, which the API's Dockerfile will need later.
Seeing an npm error?
- Make sure you ran cd api first. Running npm install from the wrong directory installs into the React project instead.
- Check that api/package.json is valid JSON. A missing comma or bracket causes install failures.
- Why is npm install failing in my api directory?
- In VS Code, create a new file at api/server.js and paste the following code:
import express from 'express'
const app = express()
const PORT = 3001
app.get('/api/messages', (req, res) => {
res.json([
{ id: 1, text: 'Hello from the API!' },
{ id: 2, text: 'Docker Compose is running both services.' },
{ id: 3, text: 'This message came from the Express backend.' }
])
})
app.listen(PORT, () => {
console.log(`API server running on port ${PORT}`)
})
What does this code do?
- app.get('/api/messages', ...) defines a single GET endpoint that returns a JSON array of messages.
- The server listens on port 3001 so it does not conflict with the React frontend on port 8080.
- Save api/server.js.
- Test the server locally by running this command:
node server.js
You should see API server running on port 3001 in your terminal.
- Open http://localhost:3001/api/messages in your browser.
You should see a JSON array with three messages. Your API is working.
- Switch back to your terminal and press Ctrl+C to stop the server.
Don't see the JSON response?
- Make sure the URL includes /api/messages at the end. Visiting just http://localhost:3001 returns nothing because no route is defined for /.
- If you see a syntax error in the terminal, double-check the res.json() array. Each object needs curly braces and the array needs square brackets.
- Why isn't my Express server responding?
Dockerize the API
The API server works locally. Now you need to package it into its own Docker image, just like you did with the React frontend.
- In VS Code, create a new file at api/Dockerfile and paste the following code:
FROM node:24-alpine
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3001
CMD ["node", "server.js"]
How is this different from the React Dockerfile?
The React Dockerfile uses two stages: one to build the app, and one to serve the static files with Nginx. This API Dockerfile only needs one stage because Node.js runs the server directly.
- npm ci --omit=dev installs only production dependencies, keeping the image small.
- package-lock.json* uses a wildcard so the COPY step does not fail if the lock file is missing.
- Save api/Dockerfile.
- Navigate back to the project root by running this command:
cd ..
- Build the API Docker image by running this command:
docker build -t api-server ./api
Watch the build output in your terminal. You should see Docker pull the node:24-alpine image, install dependencies, and copy your source files.
Build failed?
- Make sure you ran cd .. first. The ./api path in the command is relative to the react-docker root.
- Confirm that api/Dockerfile exists. The build command looks for a file named Dockerfile inside the ./api directory.
- Why is my Docker build failing for the API server?
- Run the API container by running this command:
docker run -d -p 3001:3001 api-server
Before you open the browser, think about what you expect to see at http://localhost:3001/api/messages. Will it look the same as when you tested locally?
- Open http://localhost:3001/api/messages in your browser.
You should see the same JSON array of three messages. The API is now running inside a Docker container, just like your React frontend.
Connect the React app to the backend
You now have two containers running independently: the React frontend on port 8080 and the Express API on port 3001. Time to wire them together by updating the React app to fetch data from the backend.
- Stop the React frontend container so you can rebuild it. Run this command to stop the container using port 8080:
docker stop $(docker ps -q --filter publish=8080)
- Open src/App.jsx in VS Code and replace its entire contents with the following code:
import { useState, useEffect } from 'react'
import './App.css'
function App() {
// Store messages from the API
const [messages, setMessages] = useState([])
const [loading, setLoading] = useState(true)
// Fetch messages when the component loads
useEffect(() => {
fetch('/api/messages')
.then((res) => res.json())
.then((data) => {
setMessages(data)
setLoading(false)
})
.catch((err) => {
console.error('Failed to fetch messages:', err)
setLoading(false)
})
}, [])
return (
<div className="app">
<h1>Docker Compose App</h1>
<p>React frontend + Express backend, orchestrated with Docker Compose</p>
{loading ? (
<p>Loading messages...</p>
) : (
<ul>
{messages.map((msg) => (
<li key={msg.id}>{msg.text}</li>
))}
</ul>
)}
</div>
)
}
export default App
What changed in App.jsx?
The old starter template showed logos and a counter. This new version fetches live data from your backend API.
- useEffect runs the fetch call when the component first loads.
- fetch('/api/messages') sends a request to the relative path /api/messages. Nginx is configured to proxy anything under /api/ to the backend.
- The loading state shows "Loading messages..." while the fetch is in progress, then renders the message list.
- Save src/App.jsx.
- Rebuild the React Docker image by running this command:
docker build -t react-app .
- Run the updated React container by running this command:
docker run -d -p 8080:80 react-app
Before you open the browser, think about this: will the React app successfully fetch messages from the Express API? Both containers are running. What do you think will happen?
- Open http://localhost:8080 in your browser.
You see the heading "Docker Compose App", but the messages never load. The fetch fails.
Why can't the containers talk to each other?
Your nginx.conf proxies /api/ requests to http://api:3001. But api is not a hostname that exists right now. Each container was started independently with docker run, so they have no shared network where names resolve.
You also had to build and start each container separately, stop the old one before running a new one, and keep track of ports manually. This is the pain of managing multiple containers by hand.
🙋♀️ Seeing a port conflict instead?
- If you see an error about port 8080 already being in use, the old React container is still running. Run docker ps to find it, then stop it with docker stop <container-id>.
- Why am I seeing a port conflict when running my Docker container?
✔️ Awesome, I've got everything!
Great. Double check you have saved all your files.
ⓧ I'd like to double check the full code
Here is api/package.json:
{
"name": "api-server",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^5.2.1"
}
}
Here is api/server.js:
import express from 'express'
const app = express()
const PORT = 3001
app.get('/api/messages', (req, res) => {
res.json([
{ id: 1, text: 'Hello from the API!' },
{ id: 2, text: 'Docker Compose is running both services.' },
{ id: 3, text: 'This message came from the Express backend.' }
])
})
app.listen(PORT, () => {
console.log(`API server running on port ${PORT}`)
})
Here is api/Dockerfile:
FROM node:24-alpine
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3001
CMD ["node", "server.js"]
Here is src/App.jsx:
import { useState, useEffect } from 'react'
import './App.css'
function App() {
const [messages, setMessages] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/messages')
.then((res) => res.json())
.then((data) => {
setMessages(data)
setLoading(false)
})
.catch((err) => {
console.error('Failed to fetch messages:', err)
setLoading(false)
})
}, [])
return (
<div className="app">
<h1>Docker Compose App</h1>
<p>React frontend + Express backend, orchestrated with Docker Compose</p>
{loading ? (
<p>Loading messages...</p>
) : (
<ul>
{messages.map((msg) => (
<li key={msg.id}>{msg.text}</li>
))}
</ul>
)}
</div>
)
}
export default App
You have a working backend API, a React frontend that tries to fetch from it, and first-hand experience of why managing containers individually does not scale. Next up, you will solve this problem with Docker Compose, wiring both services together with a single command.
Orchestrate with Docker Compose
You have a React frontend and an Express backend, each running in its own container. But managing them separately with docker run is painful. You had to build and start each one individually, and the containers can't find each other by name.
Docker Compose solves this. You define both services in a single file and launch everything with one command. Compose also creates a shared network where containers can reach each other by service name.
In this step, get ready to:
- Stop and remove the containers you started manually.
- Create a compose.yaml file that defines both services.
- Launch the full stack with a single Docker Compose command.
Stop and remove your existing containers
Before Docker Compose takes over, you need to clean up the containers you started manually in the previous steps. Port 8080 and port 3001 are both in use by those containers.
- Stop all running containers by running this command:
docker stop $(docker ps -q)
- Remove the stopped containers by running:
docker rm $(docker ps -aq)
- Confirm no containers are running by running:
docker ps
You should see an empty list with only the column headers.
Seeing an error about missing container IDs?
If you see a message about needing at least one argument, it means no containers were running when you ran the command. That's fine. The important thing is that docker ps shows an empty list.
If port 8080 or 3001 is still in use when you try to start Docker Compose later, open Docker Desktop and manually stop any lingering containers from the Containers tab.
Still stuck? Help me stop my running Docker containers.
Create the Docker Compose file
Docker Compose uses a YAML file to define all the services that make up your application. Instead of running separate docker build and docker run commands for each container, you describe everything once and let Compose handle the rest.
- In VS Code, create a new file called compose.yaml in your react-docker folder and paste the following content:
services:
frontend:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:80"
depends_on:
- api
api:
build:
context: ./api
dockerfile: Dockerfile
expose:
- "3001"
What does this file do?
- frontend builds from the root Dockerfile (your multi-stage React build). It maps port 8080 on your machine to port 80 inside the container, just like your earlier docker run -p 8080:80 command.
- api builds from ./api/Dockerfile (your Express server). It uses expose instead of ports because only the frontend container needs to reach it. It does not need to be accessible from your host machine.
- depends_on tells Compose to start the api service before the frontend service.
- Save compose.yaml.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
services:
frontend:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:80"
depends_on:
- api
api:
build:
context: ./api
dockerfile: Dockerfile
expose:
- "3001"
How do the frontend and backend connect?
Docker Compose automatically creates a network for your services. Inside that network, each service is reachable by its name. The frontend container can reach the api container at the hostname api.
Your nginx.conf already has a proxy_pass http://api:3001 directive from an earlier step. When the React app fetches /api/messages, the browser sends the request to Nginx, which proxies it to the api container. Docker Compose's network makes the api hostname resolve automatically.
🙋♀️ YAML indentation error?
YAML is very sensitive to indentation. Make sure you use spaces, not tabs, and that each level is indented by exactly two spaces. Check that build, ports, and depends_on are all at the same indent level under their service name.
Need help? Help me fix a YAML indentation error in my compose.yaml file.
Launch the full stack with Docker Compose
Everything is in place. Your compose.yaml defines both services, your nginx.conf proxies API requests, and your React app fetches from a relative path. Time to bring it all up.
Before you run the next command, think about this: will the React app be able to display messages from the Express backend? What is different this time compared to running the containers separately with docker run?
- Build and start both services by running:
docker compose up --build
What does this command do?
The --build flag tells Compose to rebuild both Docker images before starting the containers. Without it, Compose would reuse any cached images from earlier builds.
This command runs in the foreground. Your terminal will show build progress for both services, then stream live logs from both containers. Leave the terminal running and switch to your browser. Press Ctrl+C when you want to stop the stack.
- Wait for the build to complete. You'll see log output from both services, including a line like API server running on port 3001 from the api service.
- Open http://localhost:8080 in your browser.
You should see the React app with the heading "Docker Compose App" and three messages fetched live from the Express backend: "Hello from the API!", "Docker Compose is running both services.", and "This message came from the Express backend."
Compare this to Step 3. You started everything with a single command. The containers found each other automatically by name. No manual networking needed.
Seeing "Loading messages..." but no messages appear?
This means the React app is running but can't reach the backend API. Check these common causes:
- Make sure nginx.conf has the location /api/ block with proxy_pass http://api:3001; inside it.
- Verify that src/App.jsx fetches from /api/messages (a relative path, not http://localhost:3001).
- Check your terminal for error messages from the api service. If you see a build error, stop with Ctrl+C, fix the issue, and rerun docker compose up --build.
- Make sure no other process is using port 8080. If you still have a container from a previous step running, stop it first.
Still stuck? Help me debug why my React app can't fetch data from the backend through Docker Compose.
Secret mission
Add Health Checks and a Custom Network
Your Docker Compose setup runs both services, but Docker has no way to tell if they are actually working. Add health checks so Docker can monitor each container, and wire everything through a custom network to make your setup production-ready.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- Docker containers managed by Docker Compose (frontend and api services).
- Docker images (react-app, api-server, and Compose-built images).
- Project folder at ~/Desktop/react-docker with all source files.
Keep everything running
No action needed. Choose this if you're still actively building or want to experiment more with your Docker Compose setup.
- Your containers keep running in the background and use minimal CPU and memory when idle.
- You can access your app anytime at http://localhost:8080.
- The standalone react-app and api-server images from earlier steps remain on disk but take no resources while idle.
Pause - I'll come back to this later
Shut down running containers to free up memory, but keep your files and images so you can pick up where you left off.
- Make sure your terminal is in the ~/Desktop/react-docker folder.
- Stop the containers without removing them by running:
docker compose stop
- When you're ready to come back, navigate to ~/Desktop/react-docker and run:
docker compose start
- Your app will be back at http://localhost:8080 once the containers start.
Delete - I don't want to use this again
Remove all project containers, images, and files to start completely fresh.
- Make sure your terminal is in the ~/Desktop/react-docker folder.
- Stop and remove all Compose containers, networks, built images, and volumes by running:
docker compose down --rmi all --volumes
- Remove the standalone images you built with docker build in earlier steps by running:
docker image rm react-app api-server
- Clean up any dangling images by running:
docker image prune
- Docker will ask you to confirm. Enter y and press Enter.
- Delete the project folder. Navigate out of it first, then remove it.
macOS
cd ~/Desktop
rm -rf react-docker
Windows
cd %USERPROFILE%\Desktop
rmdir /s /q react-docker
- Confirm the folder is gone by running:
ls ~/Desktop
- You should no longer see react-docker in the output.
Nice Work!
Nice Work!
You just built and deployed a full-stack containerized application from scratch. A React frontend and an Express backend, each in their own container, talking to each other through Nginx and orchestrated by Docker Compose.
You've learned how to:
- Write a multi-stage Dockerfile that compiles a React app and serves it with Nginx, producing a production-ready image under 50MB.
- Create and containerize a separate Express backend API with its own Dockerfile, and experience firsthand why manually managing multiple containers is painful.
- Orchestrate a full-stack application with Docker Compose, where the frontend proxies API calls to the backend through Nginx and both services start with a single command.
- Secret Mission: Add health checks and a custom bridge network to make your Docker Compose setup production-ready.
Ready to quiz yourself?