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 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


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:

  1. Hosting the front-end application
  2. Hosting the back-end server
  3. 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.


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 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): ->
  • Back-end (Node.js): ->

Then, the reverse proxy will do the following:

  • Front-end (React): -> -> Reverse Proxy -> Redirect to internal React app IP address
  • Back-end (Node.js): -> -> 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 Caddyfile where you define how you’d like your reverse proxy to function
  • Additional config within your docker-compose.yml file

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

Here’s an example of the Caddyfile I used:

// Back-end API (Node.js) {
  reverse_proxy server:5000 { // server - referes to the docker service name
    header_down Strict-Transport-Security "max-age=31536000"

// Front-end app (React) {
  root * /srv
  try_files {path} /index.html

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"
    container_name: client
      context: ./client
      dockerfile: Dockerfile
      - network
      - app-dist:/app/client/dist # Mount the app-dist volume to the dist directory

    container_name: server
      context: .
      dockerfile: Dockerfile
      - "5000:5000"
    restart: always
      "NODE_ENV": "production"
      - network
    # Run the caddy server
    image: caddy/caddy:2.7.5-alpine
    container_name: caddy-service
    restart: unless-stopped
      - "80:80"
      - "443:443"
      - ./Caddyfile:/etc/caddy/Caddyfile
      - app-dist:/srv  # Mount the app-dist volume to the Caddy service
      - caddy_data:/data
      - caddy_config:/config
      - network
      - server
      - client


    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_data contains 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 server I’m using port 5000) in your Caddyfile

Ensure you’re using the service name

  • Make sure you’re referring to the docker service name in your Caddyfile. Notice I’m using server:5000


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.