A Better Way to Run an SPA locally and in Google Cloud Platform
In a previous post I mentioned that I wasn't running my Vue application in Docker locally. Since then I've figured out a better way to run the Vue app in Google Cloud Platform (GCP) and locally.
The issue was that I had it running in a Docker container behind Nginx in GCP Run, but I wanted to run it with the Vite dev server locally to get all the benfits such as debugging and rebuilding on file changes.
I've now ditched the Docker container in GCP completely and I am serving my Vue files from a GCP Storage bucket, which is perfectly fine since they are just static files. This also has the added benefit of them being behind a CDN.
This means locally I can now run my Vue app inside a Nodejs Docker image, and include it within my Docker Compose configuration. Allowing me to spin up a complete dev environment (API, SPA, database) with just docker compose up.
My compose.yaml has the follwing additions to accomodate the Vue app:
ui-build:
image: node:21-alpine
container_name: mindful-drinking-ui-build
working_dir: /usr/src/ui
command: npm install
volumes:
- type: bind
source: ../mindful-drinking-ui
target: /usr/src/ui
ui:
image: node:21-alpine
container_name: mindful-drinking-ui
working_dir: /usr/src/ui
command: npm run dev
ports:
- 5000:5000
depends_on:
- ui-build
- api
volumes:
- type: bind
source: ../mindful-drinking-ui
target: /usr/src/ui
networks:
- mindful-net
When I run docker compose up it will also now pull down the node:21-alpine image from docker hub, create a bind mount in the running container, and run npm install. It will then run the same image again, mount the code again, and run npm run dev which will start the Vite dev server on port 5000 (this is defined in vite.config.js)
This seems to work great, the only caveat being that it would expect the Vue app source code to be in a specific location on the host relative to the Flask app source code directory, where the compose.yaml is stored. It's not perfect but at the same time I don't think it's too outrageous.
Now, to serve my Vue app from a GCP Storage bucket I had to create the bucket via the GCP UI, and also configure a load balancer in front of it so I could use my custom domain and have an HTTPS endpoint. I followed the documentation to do this but one extra thing I needed to figure out was how to get my build to store the built files in the bucket.
Well, there's also some documentation on how to do that, but it didn't work for my use case because I needed my dist/index.html file in the root of the bucket, and the Javascript and CSS files in a sub directory named assets. There's no current way to do that with the artifacts key in cloudbuild.yaml. The directory structure of dist (where Vite places the built files) is:
dist/
index.html
assets/
built-js-file.js
another-built-js-file.js
a-css-file.css
And the following example won't work:
artifacts:
objects:
location: 'gs://mindful-drinking/'
paths: ['dist/**']
It's also not possible to specify multiple artifacts objects in cloudbuild.yaml.
It turned out the solution is to omit the artifacts key in cloudbuild.yaml and instead add a build step to manually invoke the gcloud tool and copy the files to storage. The clue as to what was going wrong was in the build log:
Artifacts will be uploaded to gs://mindful-drinking using gsutil cp
dist/*: Uploading path....
Omitting directory "file://dist/assets". (Did you mean to do cp -r?)
Copying file://dist/index.html [Content-Type=text/html]...
/ [0/1 files][ 0.0 B/ 563.0 B] 0% Done
/ [1/1 files][ 563.0 B/ 563.0 B] 100% Done
Operation completed over 1 objects/563.0 B.
dist/*: 1 matching files uploaded
1 total artifacts uploaded to gs://mindful-drinking/
When including the artifacts key, behind the scenes the build is using a tool called gsutil to cp the files, but there's no way to specify the -r option. So using the gsutil command was my initial plan, as per this StackOverflow post. But it turns out the preferred approach now is to call gcloud storage.
So my cloudbuild.yaml for the Vue app now looks like this:
steps:
- name: node:21-alpine
entrypoint: 'npm'
args: ['install']
- name: node:21-alpine
entrypoint: 'npm'
args: ['run', 'build']
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
args: ['gcloud', 'storage', 'cp', '-r', 'dist/*', 'gs://mindful-drinking/']