How to Host a React and Node.js App using Caddy and Docker

Overview - Caddy, Docker, React, Node.js
In this blog post, I’m going to share how you can set up a reverse proxy to host a React and Node.js app from a single server — all while having automatic SSL. Here are the technologies used:
- Docker
- Caddy
- A React front-end
- A Node.js back-end
Hosting?
You’ve built your React front-end and a Node.js back-end. It all works beautifully using Vite’s local hosting options. You’d now like to embark on hosting this publicly and ensure you have SSL certificates that are automatically renewed. This means you’d need to solve for a few things:
- Hosting the front-end application
- Hosting the back-end server
- Ensuring the above have SSL certs
Cloud hosting solutions are ubiquitous these days — DigitalOcean’s App Platform, Netlify, Vercel, etc. These are convenient and provide a relatively quick way to host your services. However, there’s an alternative: hosting these yourself on a virtual private server (VPS). The self-hosting route takes a bit more effort and your application may not easily scale, but for the project I had in mind, these weren’t big enough blockers. Hence, I opted for the latter option.
Self-hosting
Solving the reverse proxy problem
Why do I need a reverse proxy?
A reverse proxy is the system/tool that can redirect all requests made to a single public IP address to specific applications internally.
For example: your VPS has a public IP of 172.14.1.129. When you set up a DNS record to point to that public IP, all traffic will be routed there. There’s no distinguishing between the front-end and back-end at this point. Herein lies the value of a reverse proxy. We can set up the following DNS records:
- Front-end (React):
myapp.com->172.14.1.129 - Back-end (Node.js):
api.myapp.com->172.14.1.129
Then, the reverse proxy will do the following:
- Front-end (React):
myapp.com->172.14.1.129-> Reverse Proxy -> redirect to the internal React app - Back-end (Node.js):
api.myapp.com->172.14.1.129-> Reverse Proxy -> redirect to the internal Node.js app
Choosing your reverse proxy tool
In a previous blog post, I used a tool called Traefik, and it worked well for what I needed at the time. Instead of using Traefik again, I stumbled upon Caddy. I was intrigued by the minimal configuration required, so I decided to give it a try.
Using Caddy
To use Caddy, you will need the following:
- A
Caddyfilewhere you define how you’d like your reverse proxy to function - Additional config within your
docker-compose.ymlfile
The project directory is as follows:
- root
- client/
Dockerfile
Dockerfile(forserver— Node.js) — thinking about this more, I should have created a separate directory to house all Node.js/server codeCaddyfiledocker-compose.yml
- client/
Here’s an example of the Caddyfile I used:
# Back-end API (Node.js)
api.example.com {
reverse_proxy server:5000 { # server - refers to the docker service name
header_down Strict-Transport-Security "max-age=31536000"
}
}
# Front-end app (React)
example.com {
root * /srv
try_files {path} /index.html
file_server
}
This docker-compose.yml file has the following services:
server— the Node.js serverclient— the React front-endcaddy— Caddy, used for the reverse proxy
version: "3.7"
services:
client:
container_name: client
build:
context: ./client
dockerfile: Dockerfile
networks:
- network
volumes:
- app-dist:/app/client/dist # Mount the app-dist volume to the dist directory
server:
container_name: server
build:
context: .
dockerfile: Dockerfile
ports:
- "5000:5000"
restart: always
environment:
"NODE_ENV": "production"
networks:
- network
# Run the caddy server
caddy:
image: caddy/caddy:2.7.5-alpine
container_name: caddy-service
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- app-dist:/srv # Mount the app-dist volume to the Caddy service
- caddy_data:/data
- caddy_config:/config
networks:
- network
depends_on:
- server
- client
volumes:
app-dist:
caddy_data:
caddy_config:
networks:
network:
driver: bridge
Here is the Dockerfile used for the client (React) front-end:
# Use an official Node.js image as the base image
FROM node:20 as builder
# Set the working directory
WORKDIR /app/client
# Copy package.json and package-lock.json to the working directory
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the entire client directory to the working directory
COPY . ./
# Build the production version of the React app
RUN npm run build
# This is important as it's referenced in the docker-file
VOLUME /app/client/dist
All that’s left for you to do is run docker-compose up -d, and if all goes well, you’ll have both your React and Node.js app with automatic SSL certificates hosted on a single VPS.
Troubleshooting Caddy
Caddy SSL not working?
Did you delete caddy_data?
caddy_datacontains your SSL certificates. To avoid rate limits from Let’s Encrypt, avoid deleting this volume frequently.
Reverse proxy isn’t working?
Check which port your service is running on
- Make sure you’re using the Docker internal service port (e.g. for my
serverI’m using port 5000) in yourCaddyfile.
Ensure you’re using the service name
- Make sure you’re referring to the Docker service name in your
Caddyfile. Notice I’m usingserver:5000.
Conclusion
Using Caddy as a reverse proxy for a React and Node.js app, all hosted on a single server, took minimal effort and it works! The automatic SSL that Caddy provides is invaluable for anyone self-hosting secure apps.