Skip to content

BLOG

How to Deploy a Single Page Application to Google Cloud

Avatar photo

Sam McDavid

July 24, 2018
3 min read

While they are easy enough to build, I have yet to determine my favorite way to serve them in production. I’ve used static site hosting services like Surge, I’ve built a lightweight server with express, and served them from Heroku. I’ve even uploaded my build output to AWS S3 for hosting. Recently, I worked with a client who already had infrastructure on the Google Cloud Platform (GCP) and therefore needed a way to host my SPA there.

There is information online for how to serve an SPA from a GCP Bucket, but I felt the information for my exact use case was incomplete and scattered across several blog posts and wikis. In this article, I will talk about my use case where I built a React SPA with webpack 4 and deployed it to a GCP Bucket.

Please note, you could use create react app for building your SPA, but it is important to know how your underlying tools work so you can debug them when you run into errors, which is inevitable in development.

The Technical Requirements

In order to follow along, you will need to make sure that you have a domain, a GCP Account with admin permission to buckets, and an SPA built with webpack to deploy. If you don’t have an SPA already setup don’t worry, we have a sample repo for you to use on GitHub.

Build Process

I am going to use webpack 4 to build my sample SPA. To keep the setup as simple as possible, but also development and production friendly, I have broken the webpack configuration into three files: common, dev, and prod. This allows for the configuration of the necessary entry, output, loaders, resolvers, and basic plugins in the common file along with environment specific plugins in their own webpack configurations. The entire source can be found in the spa-deploy-demo repo, but you will find the contents of the wepack files that I am using below.

webpack.common.js

const path = require('path')
const HtmlWebPackPlugin = require('html-webpack-plugin')

module.exports = {
entry: {
index: './src/index.tsx'
},
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/'
},
module: {
rules: [
{
test: /.tsx?$/,
use: 'awesome-typescript-loader',
exclude: /node_modules/
},
{
test: /.html$/,
use: 'html-loader'
}
]
},
resolve: {
extensions: ['.css', '.tsx', '.ts', '.js'],
modules: [path.resolve(__dirname, 'src'), 'node_modules']
},
plugins: [
new HtmlWebPackPlugin({
template: './src/index.html',
filename: './index.html'
})
]
}

webpack.dev.js

const webpack = require('webpack')
const merge = require('webpack-merge')
const common = require('./webpack.common.js')
const BrowserSyncPlugin = require('browser-sync-webpack-plugin')

module.exports = merge(common, {
devtool: 'inline-source-map',
devServer: {
contentBase: './dist',
historyApiFallback: true,
port: 3003
},
plugins: [
new BrowserSyncPlugin({
host: 'localhost',
port: '8080',
proxy: 'http://localhost:3003',
reload: false
})
]
})

webpack.prod.js

const webpack = require('webpack')
const merge = require('webpack-merge')
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
const common = require('./webpack.common.js')

module.exports = merge(common, {
devtool: 'source-map',
plugins: [
new UglifyJSPlugin({
sourceMap: true
}),
]
})

The common webpack configuration is as minimal as possible for this demo. I prefer to write TypeScript as I do not want to configure Babel and awesome-typescript-loader is my goto loader for TypeScript. I am also using the html-loader and html-webpack-plugin for loading html so my index.html is included in the build process so the output directory can be added to the gitignore list.