a year 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
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.
a year ago
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:
:{$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
}
}
a year 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
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?
a year ago
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?
a year ago
it would be extremely negligible when proxying through the private network
a year ago
you linked the Caddyfile for the reverse proxy, but I'm not sure if you've seen it's example project
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?
a year 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
Okay another concern is how do you serve the dist folder of the frontend? In your example's case it is Vue.
a year ago
yeah Vue that is built with vite, I have a ready made drop in solution for vite with react though
a year ago
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
?
a year 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
Okay so I was thinking about using more like 3 Dockerfiles for the: frontend, backend, and caddy
a year ago
here's the vite and react example repo, the magic happens in the nixpacks.toml and Caddyfile
a year ago
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
a year ago
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.
a year ago
I assume in separate sub folders right?
a year 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?
a year 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?
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
a year ago
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.
a year ago
with caddy
a year ago
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
a year 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
Oh true. I did try Nginx without success. I am hoping that with Caddy this time around it will go more smoothly
a year 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
Ahh I see. Caddy seems simple enough but I am still new to it to do anything.
a year ago
here's the branch for a react app built with vite that uses yarn and deploys from a 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:
# server options
servers {
trusted_proxies static private_ranges # trust railway's proxy
}
Does this allow Caddy to talk to Railway's private IPv6 network?
a year 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
Hmm interesting. So does this mean I do not need the:
handle /api* {
reverse_proxy {$CADDY_BACKEND_HOST}:{$CADDY_BACKEND_PORT}
}
lines?
a year ago
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.
a year ago
that would be a job for the caddyfile in the reverse proxy repo you linked, it makes a call through the private network
a year ago
give the overview a read
a year ago
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)
a year ago
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
a year ago
caddy is a web server, but it is also a very good reverse proxy
a year ago
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?
a year ago
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?
a year 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
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}}
a year ago
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 helps
a year ago
does it no longer show as empty?
a year ago
is this to the route route?
a year ago
what is supposed to be returned by that route?
a year ago
what method did you call it with?
a year ago
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
a year ago
thats just railway's proxy overwriting the server header
a year ago
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](settings.py)
file in Django all methods would be allowed
a year ago
im 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
a year ago
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
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:
a year ago
these would be stuff your backend is setting
a year ago
but are you getting the correct json back?
a year ago
can you send me the link so i can see this stuff for myself?
Yes, please fill out the form to trigger the POST
request:
https://apricot.up.railway.app/login
a year ago
will do when back at my computer!
a year ago
send me the backend domain please
a year ago
the public one
a year ago
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?
a year ago
you dont, this is just for testing
a year ago
show me your caddyfile for the proxy service please
# 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}
}
a year ago
in a code block if you dont mind
a year ago
i think i have an idea of whats going on
a year ago
let me test some things
a year ago
what is BACKEND_HOST
set to?
FRONTEND_HOST=frontend.railway.internal:4173
BACKEND_HOST=backend-django.railway.internal:8000
a year ago
okay i was wrong on my first idea, i have a new idea
a year ago
what is the start command for django
a year ago
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:
exec python manage.py migrate & gunicorn django_project.wsgi:application --bind [::]:${BACKEND_DJANGO_PORT} --timeout 0
a year ago
what is BACKEND_DJANGO_PORT
set to
a year ago
also you should use a double &&
there
Oh it seems that you had hit the register endpoint a few times so apparently Django is getting something
a year ago
no but you still should use &&
a year ago
send me the logs for your proxy service
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
a year ago
thats it?
a year ago
yeah
a year ago
do you have a start command for django set elsewhere?
Such as python [manage.py](manage.py) runserver
? I don't include that in the container I am pretty sure
a year ago
railway 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 Heroku
a year ago
make this change, and then send me the new logs from django
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.
a year ago
show me this please
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"]
That is the Dockerfile and the set_[up.sh](up.sh)
file is a one liner basically
a year ago
send the set_up.sh please
exec python manage.py migrate && gunicorn django_project.wsgi:application --bind [::]:${BACKEND_DJANGO_PORT} --timeout 0
a year ago
send its logs with this
a year ago
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?
a year ago
no, gunicorn by default will print logs like what it's listening on
a year ago
do you have a gunicorn config?
a year ago
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
a year ago
reads like an AI wrote it, all high level help without any actual real solutions
a year ago
this is for access logs, not applicable here because we don't even see boot logs
a year ago
set BACKEND_HOST
to [https://backend-django-production.up.railway.app](https://backend-django-production.up.railway.app)
a year ago
what's the current state of the django deployment
a year ago
I'm talking about the deployment state
a year ago
like active or completed
a year ago
so django never ran
a year 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
Okay thanks for the help Brody! I will an eye out for it and see what I have for you tomorrow!
a year ago
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:
# 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}
}
a year ago
haha i did this locally
a year ago
a year ago
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:
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.
a year ago
the proxy using handle has nothing to do with gunicorn's boot logs
a year ago
im not talking about access logs, im talking about boot logs
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
a year ago
those are the boot logs
a year ago
may i ask why you have omitted admin off
, persist_config off
, auto_https off
, and log
?
a year ago
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
a year ago
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!
a year ago
that do be my repo
a year ago
glad i could help you get this all working!