How to Use Caddy To Serve Static Vite React Frontend and a Gunicorn Django Backend With Docker

jeffperterson
HOBBY

2 years ago

Hello! I am trying to test Caddy locally before deploying to Railway. I would like to have Caddy serve statically built files from Vite React while "forwarding" REST API requests such as /api/v1/* requests to my Django Backend that is served with Gunicorn. For local development I am using Docker compose but I am not using volumes because I know this is not supported on Railway.

I am getting an error in the networking tab of my Firefox browser that says "CORS Missing Allow Origin" and for a POST request that says "NSERRORDOMBADURI" (see image). I also get a 502 Bad Gateway.

I believe my error is in the Caddyfile because of these errors as well as the fact that I am not getting any information in the Django Docker container.

I am a noob at proxing and tools such as Caddy and Ngnix but do you think you could give me some pointers?

Here are the relevant files:

Caddyfile

:{$PORT} {
    root * /var/www/html

    header {
        Access-Control-Allow-Origin *
        Access-Control-Allow-Credentials true
        Access-Control-Allow-Methods *
        Access-Control-Allow-Headers *
        defer
    }
    reverse_proxy /api/* localhost:8000
    file_server
}

Dockerfile for Caddy:

# Build React Image
FROM node:latest AS frontend

WORKDIR /frontend

# React / Yarn / Node stuff
ARG VITE_SERVER_URL

COPY ./frontend/package*.json ./frontend/yarn.lock ./

RUN yarn && yarn install

COPY ./frontend .

RUN yarn build


# Caddy Build Image
FROM caddy:2.7.6-alpine

ARG PORT
ARG CADDY_BACKEND_HOST

# Copy Caddyfile
COPY /caddy-server/Caddyfile /etc/caddy/Caddyfile

# Copy the built application from the previous stage
COPY --from=frontend /frontend/dist /srv
COPY --from=frontend /frontend/dist /var/www/html

RUN caddy fmt --overwrite /etc/caddy/Caddyfile

0 Replies

jeffperterson
HOBBY

2 years ago

N/A


jeffperterson
HOBBY

2 years ago

I had realized that I had forgot to add the Docker service with the other containers in my Docker compose file and therefore I could not use the name of the Docker container in my file (for instance backend-django). I updated my code accordingly and now Caddy is acting as a proxy to Django.


2 years ago

so you got everything working how you want it?


jeffperterson
HOBBY

2 years ago

I would say to some degree, at least locally, although now when attempting to move to railway I get a a "Mixed Block" error so maybe it is something to do with the url I am passing which has http or https?


jeffperterson
HOBBY

2 years ago

Also I had updated my Caddyfile among other files:

:{$PORT} {
    root * /var/www/html

    encode gzip

    handle /api* {
        reverse_proxy {$CADDY_BACKEND_HOST}:{$CADDY_BACKEND_PORT}
    }

    handle {
        root * /var/www/html
        try_files {path} /index.html
        file_server
    }
}

2 years ago

I think you should do three services, frontend, backend, and a proxy. currently you are integrating the proxy into the frontend service, I've seen great success with the 3 service method, and I have ready made solutions for that


jeffperterson
HOBBY

2 years ago

Ahh I did see that solution here actually: https://github.com/railwayapp-templates/caddy-reverse-proxy/blob/main/Caddyfile. However, do you know if it is possible to have Caddy statically host the frontend files and reverse proxy the backend?


2 years ago

I'm sure it's possible, but the three service method would be far more optimal


jeffperterson
HOBBY

2 years ago

Wouldn't having caddy serve files directly to clients be faster than proxying another frontend service?


2 years ago

it would be extremely negligible when proxying through the private network


2 years ago

you linked the Caddyfile for the reverse proxy, but I'm not sure if you've seen it's example project


jeffperterson
HOBBY

2 years ago

oh yes I have seen that actually. The main concern with me doing this is whether or not I can use call my [http://backend/api/v1/*](http://backend/api/v1/*) routes from my frontend -- but only through the proxy. Would I be able to do this with your example?


2 years ago

yes absolutely, you would end up with only a single custom domain in use (on the proxy service only)

you access your frontend from the proxy service and then you make calls with paths (not domains) like fetch('/api/users') and the proxy will route the /api/* calls to the backend service through the private network


jeffperterson
HOBBY

2 years ago

Okay another concern is how do you serve the dist folder of the frontend? In your example's case it is Vue.


2 years ago

yeah Vue that is built with vite, I have a ready made drop in solution for vite with react though


2 years ago

though, not that anything actually changes Caddyfile wise


jeffperterson
HOBBY

2 years ago

In that case do you have an example of the dist folder being served with a Dockerfile? For instance CMD yarn?


2 years ago

my examples for serving frontend apps all use nixpacks, but if you are absolutely stuck on using a dockerfile I can whip you up a dockerfile to do that, the CMD command would start caddy though


jeffperterson
HOBBY

2 years ago

Okay so I was thinking about using more like 3 Dockerfiles for the: frontend, backend, and caddy


2 years ago

here's the vite and react example repo, the magic happens in the nixpacks.toml and Caddyfile


2 years ago

any reason not to use nixpacks?


jeffperterson
HOBBY

2 years ago

I am mainly more comfortable with Docker as well as I like to develop locally with Docker and Compose


2 years ago

fair enough, and forgive me if it's already been discussed, but does that mean your backend already deploys from a Dockerfile?


jeffperterson
HOBBY

2 years ago

That is correct. So the way I have my monorepo set up is I have a React frontend, a Django Backend, and a Caddyfile.


2 years ago

I assume in separate sub folders right?


jeffperterson
HOBBY

2 years ago

Oh and yes they are all in Docker containers


jeffperterson
HOBBY

2 years ago

Yes


2 years ago

okay cool, well the reverse proxy is already using a dockerfile, so would you like me to whip you up a Dockerfile for the frontend that uses caddy?


jeffperterson
HOBBY

2 years ago

Sure if you don't mind!


2 years ago

will do, can you tell me more about how you want the frontend built?

what node version?
yarn, npm, pnpm?
do you have the accompanying lock file?
is there any environment variables you use in your frontend that I need to be aware about?


jeffperterson
HOBBY

2 years ago

  • node:21

  • yarn

  • yes

  • yes: VITE_SERVER_URL


jeffperterson
HOBBY

2 years ago

I have this for a start:

FROM node:latest

WORKDIR /app

ARG VITE_SERVER_URL

COPY package.json yarn.lock ./ 
RUN yarn && yarn install 

COPY . .

# Build the application
RUN yarn build

2 years ago

good start, I'll finish the rest when I'm back at my computer


jeffperterson
HOBBY

2 years ago

Okay thank you. I am curious to see how you will serve the dist directory.


2 years ago

with caddy


jeffperterson
HOBBY

2 years ago

Awesome, sounds like a plan


2 years ago

theres so many other options, but i really like caddy and strongly believe is it far more user friendly than something like nginx


jeffperterson
HOBBY

2 years ago

I am coming to believe that myself -- mainly because I think the documentation is more organized than Nginx. Although I wish there was a larger community base / community help


2 years ago

and due to a bunch of sane defaults that it comes with, it works great on railway, nginx on the other hand, not so much


jeffperterson
HOBBY

2 years ago

Oh true. I did try Nginx without success. I am hoping that with Caddy this time around it will go more smoothly


2 years ago

you can definitely write a caddyfile that isnt going to work on railway, but its much easier to write one that works well than it is to write a nginx.conf that works well


jeffperterson
HOBBY

2 years ago

Ahh I see. Caddy seems simple enough but I am still new to it to do anything.


2 years ago

here's the branch for a react app built with vite that uses yarn and deploys from a dockerfile


jeffperterson
HOBBY

2 years ago

Ahh so I see that you made the Dockerfile. One thing that interests me is line 10-13 in your Caddyfile where you do:

    # server options
    servers {
        trusted_proxies static private_ranges # trust railway's proxy
    }

Does this allow Caddy to talk to Railway's private IPv6 network?


2 years ago

nope, everything you expose publicly on railway will be behind a proxy (in this case its both railway's proxy and your own caddy proxy), so that just allows caddy to trust the proxy so you will be able to see the actual client ip instead of just the proxy's ip that would be something like 10.0.0.124


jeffperterson
HOBBY

2 years ago

Hmm interesting. So does this mean I do not need the:

    handle /api* {
        reverse_proxy {$CADDY_BACKEND_HOST}:{$CADDY_BACKEND_PORT}
    }

lines?


2 years ago

nope no need, you would use the caddyfile that comes in that repo i just linked


jeffperterson
HOBBY

2 years ago

Okay I will give it a try thanks! I would just like to know however, how the Caddyfile is able to get to the Django backend I have. I may have missed something.


2 years ago

that would be a job for the caddyfile in the reverse proxy repo you linked, it makes a call through the private network


2 years ago

give the overview a read


2 years ago

might wanna just deploy that template into your project


jeffperterson
HOBBY

2 years ago

Hmm this sounds interesting. So I want to see if I have this right? :

  • The frontend's static files are served by Caddy (via its Caddyfile)

  • The "MySite -Caddy Proxy" is also using Caddy but it is proxing (also via a Caddyfile)

  • The Backend basically has no Caddy file (in my case I would use Django in Gunicorn)


2 years ago

yep that's correct


jeffperterson
HOBBY

2 years ago

Interesting I never though about using two Docker containers using Caddy.


jeffperterson
HOBBY

2 years ago

Thanks Brody I will give this a shot! Perhaps I will come back here if all goes well


2 years ago

caddy is a web server, but it is also a very good reverse proxy


2 years ago

sounds good, let me know if you run into any troubles or even if it all goes well


jeffperterson
HOBBY

2 years ago

Hello Brody, I have attempted to deploy the Caddy frontend, Django backend, and Caddy reverse proxy using much of the code you had provided. I have a question concerning the $PORT variable. Does the $PORT need to be the same for all three of these services to work?


2 years ago

it doesnt need to be, it can be if you want to though, but i would do something like 3000, 3001


jeffperterson
HOBBY

2 years ago

And the service in which caddy acts as a proxy doesn't need a $PORT right?


2 years ago

correct, railway assigns a random port and the proxy service will use that, but we only need to define fixed ports of the back and frontend since we need to talk to those over the private network and that wouldnt work with random ports that change every deployment


jeffperterson
HOBBY

2 years ago

Hmmm I am getting a 502 error although I have set up the FRONTEND_HOST and BACKEND_HOST variables:

FRONTEND_HOST=${{frontend.RAILWAY_PRIVATE_DOMAIN}}:${{frontend.PORT}}
BACKEND_HOST=${{django.RAILWAY_PRIVATE_DOMAIN}}:${{django.PORT}}

2 years ago

502 on the / (root) route?


jeffperterson
HOBBY

2 years ago

Yes although I just found that for some reason the FRONTEND_HOST variable was an empty string so I am now redeploying it to see if that helps


2 years ago

does it no longer show as empty?


jeffperterson
HOBBY

2 years ago

Okay I may have mistyped something but it is rebuilding now


jeffperterson
HOBBY

2 years ago

Okay I got it up sort of but I get a 405 error

1217737637335666700


2 years ago

is this to the route route?


jeffperterson
HOBBY

2 years ago

Yes this one I believe a /api/v1/ route


2 years ago

what is supposed to be returned by that route?


jeffperterson
HOBBY

2 years ago

It should just return a JSON response from the backend


2 years ago

what method did you call it with?


jeffperterson
HOBBY

2 years ago

POST


2 years ago

it says it only accepts GET and HEAD


jeffperterson
HOBBY

2 years ago

Hmmm so is there a way to configure this in railway? It does say the server is railway and not Caddy. I'm not really sure actually


2 years ago

thats just railway's proxy overwriting the server header


2 years ago

the allow header is coming from the backend


jeffperterson
HOBBY

2 years ago

That's strange I thought if I left the ALLOWED_HOSTS variable alone in the [settings.py](settings.py) file in Django all methods would be allowed


2 years ago

im not too sure what that setting has to do with request methods?


jeffperterson
HOBBY

2 years ago

It probably doesn't, I am actually stumped on this one. I thought it could be CORs thing but I believe I already fixed that


2 years ago

have you tried using GET? thats what it allows


jeffperterson
HOBBY

2 years ago

I mean I can directly hit Django with a REST API request if it doesn't go through the proxy


jeffperterson
HOBBY

2 years ago

That is a good point, I should try that real quick


jeffperterson
HOBBY

2 years ago

Seems like I am getting an unsafe-url Referrer Policy when I do a GET request. I am not sure if that is really relevant to the issue though:

1217741209351622700


2 years ago

these would be stuff your backend is setting


2 years ago

but are you getting the correct json back?


jeffperterson
HOBBY

2 years ago

Actually I don't get anything in my response:

1217741717311328300


2 years ago

can you send me the link so i can see this stuff for myself?


jeffperterson
HOBBY

2 years ago

Yes, please fill out the form to trigger the POST request:
https://apricot.up.railway.app/login


2 years ago

will do when back at my computer!


2 years ago

send me the backend domain please


jeffperterson
HOBBY

2 years ago

backend-django.railway.internal


2 years ago

the public one


2 years ago

wouldnt be much of a private network if i could call that internal domain


jeffperterson
HOBBY

2 years ago

I didn't know I needed public one if caddy can proxy Django via the internal one?


2 years ago

you dont, this is just for testing


jeffperterson
HOBBY

2 years ago

Oh okay that makes sense


jeffperterson
HOBBY

2 years ago

I will generate one then



2 years ago

show me your caddyfile for the proxy service please


jeffperterson
HOBBY

2 years ago

# Caddy configuration file
{
    # debug

    # server options
    servers {
        # trust railway's proxy
        trusted_proxies static private_ranges 
    }
}


# Comment out for development
# :{$CADDY_BACKEND_PORT} {
:{$PORT} {

    reverse_proxy {$FRONTEND_HOST} # proxy all requests for /* to the frontend, configure this variable in the service settings

    # # the handle_path directive will strip /api/ from the path before proxying
    # # this is needed if your backend's api routes don't start with /api/
    # # change paths as needed
    # handle_path /api/* {
    #     # this strips the /api/ prefix from the uri sent to the proxy address
    #     # proxy all requests for /api/* to the backend, configure this variable in the service settings
    #     reverse_proxy {$BACKEND_HOST} 
    # }

    # configure this variable in the service settings
    reverse_proxy {$BACKEND_HOST}
}

jeffperterson
HOBBY

2 years ago

Oops let me re format tat


2 years ago

in a code block if you dont mind


jeffperterson
HOBBY

2 years ago

Okay I edited it


2 years ago

i think i have an idea of whats going on


2 years ago

let me test some things


jeffperterson
HOBBY

2 years ago

Awesome!


2 years ago

what is BACKEND_HOST set to?


jeffperterson
HOBBY

2 years ago

FRONTEND_HOST=frontend.railway.internal:4173
BACKEND_HOST=backend-django.railway.internal:8000

jeffperterson
HOBBY

2 years ago

I hardcoded these values because I was getting some parsing issue


2 years ago

okay i was wrong on my first idea, i have a new idea


2 years ago

what is the start command for django


2 years ago

or CMD command in your case


jeffperterson
HOBBY

2 years ago

So basically at the end of the Django's Dockerfile there is a CMD command that calls a shell script that looks like this:

exec python manage.py migrate & gunicorn django_project.wsgi:application --bind [::]:${BACKEND_DJANGO_PORT} --timeout 0

2 years ago

what is BACKEND_DJANGO_PORT set to


jeffperterson
HOBBY

2 years ago

BACKEND_DJANGO_PORT=8000


2 years ago

also you should use a double && there


jeffperterson
HOBBY

2 years ago

Hmm do you think that is what is causing the issue?:

1217765024907526100


jeffperterson
HOBBY

2 years ago

Oh it seems that you had hit the register endpoint a few times so apparently Django is getting something


2 years ago

no but you still should use &&


2 years ago

send me the logs for your proxy service


jeffperterson
HOBBY

2 years ago

using provided configuration

admin endpoint started

started background certificate maintenance

server running

autosaved config (load with --resume flag)

serving initial configuration

cleaning storage unit

finished cleaning storage units

2 years ago

thats it?


jeffperterson
HOBBY

2 years ago

This one right?:

1217765537572978700


2 years ago

yeah


2 years ago

do you have a start command for django set elsewhere?


jeffperterson
HOBBY

2 years ago

Such as python [manage.py](manage.py) runserver? I don't include that in the container I am pretty sure


2 years ago

railway service settings, a procfile, a railway.json, anything


jeffperterson
HOBBY

2 years ago

Hmm I haven't used anything like that. I quit using a procfile after moving away from Heroku


2 years ago

make this change, and then send me the new logs from django


jeffperterson
HOBBY

2 years ago

Okay sure thing!


jeffperterson
HOBBY

2 years ago

Okay new logs:

/usr/local/lib/python3.11/site-packages/langchain/chat_models/__init__.py:31: LangChainDeprecationWarning: Importing chat models from langchain is deprecated. Importing from langchain will no longer be supported as of langchain==0.2.0. Please import from langchain-community instead:

`from langchain_community.chat_models import ChatOpenAI`.

To install langchain-community run `pip install -U langchain-community`.

warnings.warn(

Operations to perform:

Apply all migrations: Core, admin, auth, authtoken, contenttypes, sessions, token_blacklist

Running migrations:

No migrations to apply.

2 years ago

show me this please


jeffperterson
HOBBY

2 years ago

FROM python:3.11.6

WORKDIR /app/django_project

COPY ./backend-django/requirements.txt ./

ADD ./rabbitmq_immersio_utils ./rabbitmq_immersio_utils

RUN pip install --no-cache-dir -r requirements.txt

COPY ./backend-django /app

# RUN apt-get install systemd && systemctl enable rabbitmq-server
WORKDIR /app/django_project

EXPOSE 8000
EXPOSE 5672
EXPOSE 15672

# We need environment variables during build time (during RUN commands)
# Therefore we use ARG

# 👇 Defined in compose
ARG RABBITMQ_HOST
ARG RABBITMQ_DEFAULT_USER
ARG RABBITMQ_DEFAULT_PASS
ARG RABBITMQ_PORT
ARG RABBITMQ_GUI_PORT

# RUN python manage.py makemigrations && python manage.py migrate

# CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
CMD ["sh", "set_up.sh"]

jeffperterson
HOBBY

2 years ago

That is the Dockerfile and the set_[up.sh](up.sh) file is a one liner basically


2 years ago

send the set_up.sh please


jeffperterson
HOBBY

2 years ago

exec python manage.py migrate && gunicorn django_project.wsgi:application --bind [::]:${BACKEND_DJANGO_PORT} --timeout 0

2 years ago

send its logs with this


jeffperterson
HOBBY

2 years ago


jeffperterson
HOBBY

2 years ago

I'm not sure if I used that tool you sent correctly


2 years ago

there's no logs from gunicorn itself, something real fishy is going on here


jeffperterson
HOBBY

2 years ago

Oh well isn't that because I am not running gunicorn with the --debug flag or something?


2 years ago

no, gunicorn by default will print logs like what it's listening on


2 years ago

do you have a gunicorn config?


jeffperterson
HOBBY

2 years ago

I scraped by without making one


2 years ago

well then something real odd is happening here


jeffperterson
HOBBY

2 years ago

I found a link to a "Heroku related deploy issue": https://help.heroku.com/HX4L23I4/debugging-deploy-issues-with-gunicorn


jeffperterson
HOBBY

2 years ago

I am not sure if it is relevant though



jeffperterson
HOBBY

2 years ago

1217770949483565000


2 years ago

reads like an AI wrote it, all high level help without any actual real solutions


2 years ago

this is for access logs, not applicable here because we don't even see boot logs


2 years ago

set BACKEND_HOST to [https://backend-django-production.up.railway.app](https://backend-django-production.up.railway.app)


jeffperterson
HOBBY

2 years ago

Sure thing


2 years ago

what's the current state of the django deployment


jeffperterson
HOBBY

2 years ago

1217772128737165300


2 years ago

I'm talking about the deployment state


2 years ago

like active or completed


jeffperterson
HOBBY

2 years ago

Completed


jeffperterson
HOBBY

2 years ago

1217772282362204200


2 years ago

so django never ran


jeffperterson
HOBBY

2 years ago

That makes sense I don't see the 0.0.0.0:8000 port information on there


2 years ago

alright well it's 5:50am and I haven't slept yet, so I wish you good luck in finding out why django isn't running! we can pick this back up tomorrow


jeffperterson
HOBBY

2 years ago

Okay thanks for the help Brody! I will an eye out for it and see what I have for you tomorrow!


2 years ago

sounds good


jeffperterson
HOBBY

2 years ago

Hello Brody I had determined that issue preventing my frontend from accessing backend via the caddy proxy was due to how I had my Caddyfile configured where handle_path should have been handle as the url path of the REST API request was being stripped by caddy as it made its way to the backend. I have now fixed this with the updated Caddyfile:

# Caddy configuration file
{
    # server options
    servers {
        # trust railway's proxy
        trusted_proxies static private_ranges 
    }
}

:{$PORT} {


    handle /api/* {
        # this strips the /api/ prefix from the uri sent to the proxy address
        # proxy all requests for /api/* to the backend, configure this variable in the service settings
        reverse_proxy {$BACKEND_HOST} 
    }

    # proxy all requests for /* to the frontend, configure this variable in the service settings
    reverse_proxy {$FRONTEND_HOST} 
}

jeffperterson
HOBBY

2 years ago

Thank you again Brody for your help! It works good now!


2 years ago

haha i did this locally


2 years ago

1217920638954770700


2 years ago

but wanted to figure out why we weren't seeing gunicorns logs first


jeffperterson
HOBBY

2 years ago

Lol. Yeah I so what I found out about the Gunicorn logs is that when I temporarily commented the migrate command in:

exec python manage.py migrate & gunicorn django_project.wsgi:application --bind [::]:${BACKEND_DJANGO_PORT} --timeout 0

Where it became:

exec gunicorn django_project.wsgi:application --bind [::]:${BACKEND_DJANGO_PORT} --timeout 0

The logs appeared. Although I think the real reason why they appeared is because the && used to be & or something. Once I had used handle the backend (Gunicorn) was able to get logs.


2 years ago

the proxy using handle has nothing to do with gunicorn's boot logs


jeffperterson
HOBBY

2 years ago

Well I was never hitting Gunicorn so I had never received logs for it.


2 years ago

im not talking about access logs, im talking about boot logs


jeffperterson
HOBBY

2 years ago

Right so those were because of the .sh file. Now it shows:

[INFO] Listening at: http://[::]:8000 (7)
[7] [INFO] Using worker: sync
[8] [INFO] Booting worker with pid: 8

2 years ago

those are the boot logs


2 years ago

may i ask why you have omitted admin off, persist_config off, auto_https off, and log ?


jeffperterson
HOBBY

2 years ago

Oh. To be honest with you I was not aware of those settings.


2 years ago

they where in the original caddyfile


jeffperterson
HOBBY

2 years ago

Oh I see what you are saying. I accidently thought they were comments because my IDE did not have Caddyfile support at that time. I could probably re-add them


2 years ago

ah gotcha


jeffperterson
HOBBY

2 years ago

Alright thanks for the help. I think your example repo (https://github.com/railwayapp-templates/caddy-reverse-proxy/tree/main) does the job!


2 years ago

that do be my repo


2 years ago

glad i could help you get this all working!


How to Use Caddy To Serve Static Vite React Frontend and a Gunicorn Django Backend With Docker - Railway Help Station