How to Use Caddy To Serve Static Vite React Frontend and a Gunicorn Django Backend With Docker
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 "NS_ERROR_DOM_BAD_URI" (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
Dockerfile for Caddy:
149 Replies
Project ID:
N/A
N/A
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.so you got everything working how you want it?
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?
Also I had updated my Caddyfile among other files:
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
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?
I'm sure it's possible, but the three service method would be far more optimal
Wouldn't having caddy serve files directly to clients be faster than proxying another frontend service?
it would be extremely negligible when proxying through the private network
you linked the Caddyfile for the reverse proxy, but I'm not sure if you've seen it's example project
https://railway.app/project/35d8d571-4313-4049-9699-4e7db7f02a2f
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/*
routes from my frontend -- but only through the proxy. Would I be able to do this with your example?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 networkOkay another concern is how do you serve the dist folder of the frontend? In your example's case it is Vue.
yeah Vue that is built with vite, I have a ready made drop in solution for vite with react though
though, not that anything actually changes Caddyfile wise
In that case do you have an example of the dist folder being served with a Dockerfile? For instance
CMD yarn <some command to serve dist folder>
?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
Okay so I was thinking about using more like 3 Dockerfiles for the: frontend, backend, and caddy
here's the vite and react example repo, the magic happens in the nixpacks.toml and Caddyfile
https://github.com/brody192/vite-react-template
any reason not to use nixpacks?
I am mainly more comfortable with Docker as well as I like to develop locally with Docker and Compose
fair enough, and forgive me if it's already been discussed, but does that mean your backend already deploys from a Dockerfile?
That is correct. So the way I have my monorepo set up is I have a React frontend, a Django Backend, and a Caddyfile.
I assume in separate sub folders right?
Oh and yes they are all in Docker containers
Yes
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?
Sure if you don't mind!
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?
- node:21
- yarn
- yes
- yes:
VITE_SERVER_URL
I have this for a start:
good start, I'll finish the rest when I'm back at my computer
Okay thank you. I am curious to see how you will serve the dist directory.
with caddy
Awesome, sounds like a plan
theres so many other options, but i really like caddy and strongly believe is it far more user friendly than something like nginx
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
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
Oh true. I did try Nginx without success. I am hoping that with Caddy this time around it will go more smoothly
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
Ahh I see. Caddy seems simple enough but I am still new to it to do anything.
here's the branch for a react app built with vite that uses yarn and deploys from a dockerfile
https://github.com/brody192/vite-react-template/tree/yarn-dockerfile
Ahh so I see that you made the Dockerfile. One thing that interests me is line 10-13 in your Caddyfile where you do:
Does this allow Caddy to talk to Railway's private IPv6 network?
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
Hmm interesting. So does this mean I do not need the:
lines?
nope no need, you would use the caddyfile that comes in that repo i just linked
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.
that would be a job for the caddyfile in the reverse proxy repo you linked, it makes a call through the private network
give the overview a read https://railway.app/template/7uDSyj
might wanna just deploy that template into your project
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)
yep that's correct
Interesting I never though about using two Docker containers using Caddy.
Thanks Brody I will give this a shot! Perhaps I will come back here if all goes well
caddy is a web server, but it is also a very good reverse proxy
sounds good, let me know if you run into any troubles or even if it all goes well
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?it doesnt need to be, it can be if you want to though, but i would do something like 3000, 3001
And the service in which caddy acts as a proxy doesn't need a
$PORT
right?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
Hmmm I am getting a 502 error although I have set up the
FRONTEND_HOST
and BACKEND_HOST
variables:
502 on the
/
(root) route?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 helpsdoes it no longer show as empty?
Okay I may have mistyped something but it is rebuilding now
Okay I got it up sort of but I get a 405 error
is this to the route route?
Yes this one I believe a
/api/v1/
routewhat is supposed to be returned by that route?
It should just return a JSON response from the backend
what method did you call it with?
POST
it says it only accepts GET and HEAD
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
thats just railway's proxy overwriting the server header
the allow header is coming from the backend
That's strange I thought if I left the
ALLOWED_HOSTS
variable alone in the settings.py
file in Django all methods would be allowedim not too sure what that setting has to do with request methods?
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
have you tried using GET? thats what it allows
I mean I can directly hit Django with a REST API request if it doesn't go through the proxy
That is a good point, I should try that real quick
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:these would be stuff your backend is setting
but are you getting the correct json back?
Actually I don't get anything in my response:
can you send me the link so i can see this stuff for myself?
will do when back at my computer!
send me the backend domain please
backend-django.railway.internal
the public one
wouldnt be much of a private network if i could call that internal domain
I didn't know I needed public one if caddy can proxy Django via the internal one?
you dont, this is just for testing
Oh okay that makes sense
I will generate one then
https://backend-django-production.up.railway.app/
show me your caddyfile for the proxy service please
in a code block if you dont mind
Okay I edited it
i think i have an idea of whats going on
let me test some things
Awesome!
what is
BACKEND_HOST
set to?
I hardcoded these values because I was getting some parsing issue
okay i was wrong on my first idea, i have a new idea
what is the start command for django
or CMD command in your case
So basically at the end of the Django's Dockerfile there is a
CMD
command that calls a shell script that looks like this:
what is
BACKEND_DJANGO_PORT
set toBACKEND_DJANGO_PORT=8000
also you should use a double
&&
thereHmm do you think that is what is causing the issue?:
Oh it seems that you had hit the register endpoint a few times so apparently Django is getting something
no but you still should use &&
send me the logs for your proxy service
thats it?
This one right?:
yeah
do you have a start command for django set elsewhere?
Such as
python manage.py runserver
? I don't include that in the container I am pretty surerailway service settings, a procfile, a railway.json, anything
Hmm I haven't used anything like that. I quit using a
procfile
after moving away from Herokumake this change, and then send me the new logs from django
Okay sure thing!
Okay new logs:
show me this please
That is the Dockerfile and the
set_up.sh
file is a one liner basicallysend the set_up.sh please
send its logs with this https://bookmarklets.up.railway.app/log-downloader/
I'm not sure if I used that tool you sent correctly
there's no logs from gunicorn itself, something real fishy is going on here
Oh well isn't that because I am not running gunicorn with the
--debug
flag or something?no, gunicorn by default will print logs like what it's listening on
do you have a gunicorn config?
I scraped by without making one
well then something real odd is happening here
I found a link to a "Heroku related deploy issue": https://help.heroku.com/HX4L23I4/debugging-deploy-issues-with-gunicorn
I am not sure if it is relevant though
I found this in the docs: https://docs.gunicorn.org/en/stable/faq.html#:~:text=In%20version%2019.0%2C%20Gunicorn%20doesn,the%20console%20by%20default%20again.
reads like an AI wrote it, all high level help without any actual real solutions
this is for access logs, not applicable here because we don't even see boot logs
set
BACKEND_HOST
to https://backend-django-production.up.railway.app
Sure thing
what's the current state of the django deployment
I'm talking about the deployment state
like active or completed
Completed
so django never ran
That makes sense I don't see the 0.0.0.0:8000 port information on there
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
Okay thanks for the help Brody! I will an eye out for it and see what I have for you tomorrow!
sounds good
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:
Thank you again Brody for your help! It works good now!haha i did this locally
but wanted to figure out why we weren't seeing gunicorns logs first
Lol. Yeah I so what I found out about the Gunicorn logs is that when I temporarily commented the
migrate
command in:
Where it became:
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.the proxy using handle has nothing to do with gunicorn's boot logs
Well I was never hitting Gunicorn so I had never received logs for it.
im not talking about access logs, im talking about boot logs
Right so those were because of the
.sh
file. Now it shows:
those are the boot logs
may i ask why you have omitted
admin off
, persist_config off
, auto_https off
, and log
?Oh. To be honest with you I was not aware of those settings.
they where in the original caddyfile
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
ah gotcha
Alright thanks for the help. I think your example repo (https://github.com/railwayapp-templates/caddy-reverse-proxy/tree/main) does the job!
GitHub
GitHub - railwayapp-templates/caddy-reverse-proxy
Contribute to railwayapp-templates/caddy-reverse-proxy development by creating an account on GitHub.
that do be my repo
glad i could help you get this all working!