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

jeffpertersonHOBBY

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

jeffpertersonHOBBY

a year ago

N/A


jeffpertersonHOBBY

a year 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.


a year ago

so you got everything working how you want it?


jeffpertersonHOBBY

a year 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?


jeffpertersonHOBBY

a year 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
    }
}

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


jeffpertersonHOBBY

a year 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?


a year ago

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


jeffpertersonHOBBY

a year ago

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


jeffpertersonHOBBY

a year 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?


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


jeffpertersonHOBBY

a year ago

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


jeffpertersonHOBBY

a year ago

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


jeffpertersonHOBBY

a year ago

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?


jeffpertersonHOBBY

a year ago

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?


jeffpertersonHOBBY

a year 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.


a year ago

I assume in separate sub folders right?


jeffpertersonHOBBY

a year ago

Oh and yes they are all in Docker containers


jeffpertersonHOBBY

a year ago

Yes


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?


jeffpertersonHOBBY

a year ago

Sure if you don't mind!


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?


jeffpertersonHOBBY

a year ago

  • node:21

  • yarn

  • yes

  • yes: VITE_SERVER_URL


jeffpertersonHOBBY

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

a year ago

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


jeffpertersonHOBBY

a year ago

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


a year ago

with caddy


jeffpertersonHOBBY

a year ago

Awesome, sounds like a plan


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


jeffpertersonHOBBY

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


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


jeffpertersonHOBBY

a year ago

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


jeffpertersonHOBBY

a year ago

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


jeffpertersonHOBBY

a year 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?


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


jeffpertersonHOBBY

a year ago

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


jeffpertersonHOBBY

a year 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.


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


jeffpertersonHOBBY

a year 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)


a year ago

yep that's correct


jeffpertersonHOBBY

a year ago

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


jeffpertersonHOBBY

a year ago

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


jeffpertersonHOBBY

a year 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?


a year ago

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


jeffpertersonHOBBY

a year ago

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


jeffpertersonHOBBY

a year 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}}

a year ago

502 on the / (root) route?


jeffpertersonHOBBY

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


a year ago

does it no longer show as empty?


jeffpertersonHOBBY

a year ago

Okay I may have mistyped something but it is rebuilding now


jeffpertersonHOBBY

a year ago

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

1217737637335666700


a year ago

is this to the route route?


jeffpertersonHOBBY

a year ago

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


a year ago

what is supposed to be returned by that route?


jeffpertersonHOBBY

a year ago

It should just return a JSON response from the backend


a year ago

what method did you call it with?


jeffpertersonHOBBY

a year ago

POST


a year ago

it says it only accepts GET and HEAD


jeffpertersonHOBBY

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


a year ago

thats just railway's proxy overwriting the server header


a year ago

the allow header is coming from the backend


jeffpertersonHOBBY

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


a year ago

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


jeffpertersonHOBBY

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


a year ago

have you tried using GET? thats what it allows


jeffpertersonHOBBY

a year ago

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


jeffpertersonHOBBY

a year ago

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


jeffpertersonHOBBY

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


a year ago

these would be stuff your backend is setting


a year ago

but are you getting the correct json back?


jeffpertersonHOBBY

a year ago

Actually I don't get anything in my response:

1217741717311328300


a year ago

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


jeffpertersonHOBBY

a year ago

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


jeffpertersonHOBBY

a year ago

backend-django.railway.internal


a year ago

the public one


a year ago

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


jeffpertersonHOBBY

a year ago

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


jeffpertersonHOBBY

a year ago

Oh okay that makes sense


jeffpertersonHOBBY

a year ago

I will generate one then



a year ago

show me your caddyfile for the proxy service please


jeffpertersonHOBBY

a year 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}
}

jeffpertersonHOBBY

a year ago

Oops let me re format tat


a year ago

in a code block if you dont mind


jeffpertersonHOBBY

a year ago

Okay I edited it


a year ago

i think i have an idea of whats going on


a year ago

let me test some things


jeffpertersonHOBBY

a year ago

Awesome!


a year ago

what is BACKEND_HOST set to?


jeffpertersonHOBBY

a year ago

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

jeffpertersonHOBBY

a year ago

I hardcoded these values because I was getting some parsing issue


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


jeffpertersonHOBBY

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

a year ago

what is BACKEND_DJANGO_PORT set to


jeffpertersonHOBBY

a year ago

BACKEND_DJANGO_PORT=8000


a year ago

also you should use a double && there


jeffpertersonHOBBY

a year ago

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

1217765024907526100


jeffpertersonHOBBY

a year ago

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


jeffpertersonHOBBY

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

a year ago

thats it?


jeffpertersonHOBBY

a year ago

This one right?:

1217765537572978700


a year ago

yeah


a year ago

do you have a start command for django set elsewhere?


jeffpertersonHOBBY

a year ago

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


jeffpertersonHOBBY

a year ago

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


jeffpertersonHOBBY

a year ago

Okay sure thing!


jeffpertersonHOBBY

a year 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.

a year ago

show me this please


jeffpertersonHOBBY

a year 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"]

jeffpertersonHOBBY

a year ago

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


jeffpertersonHOBBY

a year ago

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

a year ago

send its logs with this


jeffpertersonHOBBY

a year ago


jeffpertersonHOBBY

a year ago

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


a year ago

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


jeffpertersonHOBBY

a year ago

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?


jeffpertersonHOBBY

a year ago

I scraped by without making one


a year ago

well then something real odd is happening here


jeffpertersonHOBBY

a year ago

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


jeffpertersonHOBBY

a year ago

I am not sure if it is relevant though



jeffpertersonHOBBY

a year ago

1217770949483565000


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)


jeffpertersonHOBBY

a year ago

Sure thing


a year ago

what's the current state of the django deployment


jeffpertersonHOBBY

a year ago

1217772128737165300


a year ago

I'm talking about the deployment state


a year ago

like active or completed


jeffpertersonHOBBY

a year ago

Completed


jeffpertersonHOBBY

a year ago

1217772282362204200


a year ago

so django never ran


jeffpertersonHOBBY

a year ago

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


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


jeffpertersonHOBBY

a year ago

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


jeffpertersonHOBBY

a year 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} 
}

jeffpertersonHOBBY

a year ago

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


a year ago

haha i did this locally


a year ago

1217920638954770700


a year ago

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


jeffpertersonHOBBY

a year 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.


a year ago

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


jeffpertersonHOBBY

a year ago

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


a year ago

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


jeffpertersonHOBBY

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

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 ?


jeffpertersonHOBBY

a year ago

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


a year ago

they where in the original caddyfile


jeffpertersonHOBBY

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


a year ago

ah gotcha


jeffpertersonHOBBY

a year ago

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!