Building a Drink Tracking Website

I’ve been working on a small website to help people (me) track their alcohol consumption and to see whether it is in line with the CDC’s guidelines on what constitutes healthy (well, rather a not too unhealthy) drinking.

Really it was simply to give me an opportunity to learn some new technology (VueJS and Google Cloud, mostly) and to brush up on existing skills (Python, Flask, Docker).

I thought I’d write a blog post on what I found to be particularly interesting and as a future reference for myself on any problems I had to solve.

VueJS

The last time I started a new project in React, Create React App was the go to tool for creating a project boiler plate. Now days if I want to start a React project the most popular choices require javascript on the server too, using a framework like Next.js. Well since this project is for fun and personal learning I’d rather use Python for my API, so I needed to find a client side only javascript framework.

So that confirmed my choice to use Vue. What I like about Vue is it is still absolutely a front end framework and if you want it to be, a complete package. The Vite build tool can set up your project boilerplate with everything you need, including routing and state management.

I think one of my favorite things about Vue is the template syntax. It feels like I am writing HTML and is easier to immediately make sense of than a complex React component where bits of JSX might be getting defined and returned from various functions. For example, with Vue I could render a table body using a list of objects like so:

<tbody v-for='thing in listOfObjects'>
    <tr>
        <td>{{ thing.property1 }}</td>
        <td>{{ thing.property2 }}</td>
    </tr>
</tbody>

Notice the v-for which is a Vue directive. There are others such as v-if and v-else which again make templates a bit cleaner.

With React it would look something like this:

<tbody>
    { listOfObjects.map(thing => 
        (
            <tr>
                <td>{{ thing.property1 }}</td>
                <td>{{ thing.property2 }}</td>
            </tr>
        )
    )}
</tbody>

Docker

It took me a while to figure out the best way to containerize my UI and API for running in Cloud Run. Initially I was going to have both Nginx and Gunicorn in the same container, running my Flask API. Then after some research I found out it is better to have the container be a single process, partly because if the process crashes then the container will stop and allow the cloud host to start another container. Also I wasn’t sure what the point would be of running my own Nginx server, since the container is already behind whatever Cloud Run is using to serve web requests.

As per the Cloud Run docs:

A Cloud Run service provides you with the infrastructure required to run a reliable HTTPS endpoint. Your responsibility is to make sure your code listens on a TCP port and handles HTTP requests.

That sounds like what Gunicorn does to me. Perfect.

For the Vue app, I used an Nginx container because I needed a process to listen for an HTTPS connection and serve my static files, and Nginx is pretty good at that.

One problem I encountered was how to have my Vue app use the appropriate host for the API depending on whether it was running on my local machine or in the cloud. The solution was pretty simple- I could define an environment variable in .env.development and .env.production files in the project root and Vite (the Vue CLI and build tool) would set the appropriate value on import.meta.env during the build for use in the code like import.meta.env.VITE_API_HOST. (Env var must be prefixed with VITE_). More info.

Another Problem I encountered was getting my API to connect from inside a Docker container (when running on my local machine) to MySQL that was not inside a container. There seem to be different solutions depening on whether the host machine is running Windows, Mac OS, or Linux. On Windows and Mac it seems you can use the connection string host.docker.internal from inside a container to route traffic to the host machine. But on Linux I couldn’t get this to work even with the suggested addition of adding --add-host host.docker.internal:host-gateway to my Docker run command. I opted to run the containers on the host network using --network="host" in my Docker run command. It’s not great because any listening ports inside the container are now open and listening on the host machine too, rather than needing to be mapped explicitely.

Google Cloud

There was a bit of trial and error getting things up and running in Google Cloud, mostly due to me needing to figure out what products I should be using and how best to use them.

It turned out that these were the products I needed: Cloud Build, Artifact Registry, Cloud Run, Cloud SQL, IAM and Admin.

I think I’ll save the details of getting everthing running in Google Cloud for another post as it’s quite long winded, but I will mention one issue I encountered- good old CORS.

Everytime I think I have CORS figured out it always gets me. I tend to forget that firstly, a CORS origin is the scheme, the domain and subdomain, and the port. Secondly, if the client has a script making the HTTP request (e.g. using Fetch) and it is sending auth credentials using credentials: "include", the Access-Control-Allow-Origin header must be specified with an origin, not just "*". Since I had my Vue app running under https://mindful-drinking.jonhudson.me and the API under https://mindful-drinking-api.jonhudson.me CORS did come into play. But because I had to also include credentials in the client request, I needed to specify Access-Control-Allow-Origin: https://mindful-drinking.jonhudson.me in the response header. It wasn’t quite as simple as that of course, and there were other CORS headers I needed to include. So I settled on using the Flask CORS package to make my life easier.