
Overview – Caddy, Docker, React, Node.js
In this blog post, I’m going to share how, you can setup 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 embark on hosting this publicly and ensure you have SSL certificates – which are automatically renewed.
This means, you’d need to solve for a few aspects:
- 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 AppPlatform, Netlify, Vercel, etc. These are convenient and provide a relatively quick way to host your services, however, there is 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 a big enough blocker. Hence, if 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 that are 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 setup 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 setup 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 internal React app IP address
- Back-end (Node.js): api.myapp.com->172.14.1.129-> Reverse Proxy -> Redirect to internal Node.js app IP address
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 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(for- server– Nodejs) – thinking about this more, I should have created a separate directory to house all Nodejs/server code
- Caddyfile
- docker-compose.yml
 
- client/
Here’s an example of the Caddyfile I used:Caddyfile:
// Back-end API (Node.js)
api.example.com {
  reverse_proxy server:5000 { // server - referes 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 that has the following services:
- server– the Node.js server
- client– the React front-end
- caddy– Caddy used for 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 directoryWORKDIR /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-fileVOLUME /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 Lets 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 IP (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.



