Hi, this workshop will show you how to transform your local project onto the world wide web. We have an existing careers website where users can apply to a job and upload their resume to which is connected to a local postgres instance and saves files to the local hard drive and want anybody in the world to be able to apply to our job.
Firstly, let's create a Fly account. Fly is a cloud platform for deploying serverless applications. It is entirely free for small services and scales affordably.
We use it for all of our ten new WDCC projects and are yet to pay a dime. In the future, we are going to transition to using the same technology that powers capstone projects so that we can easily set budgets and monitor the projects while also enabling developers and leads to easily provision their own infrastructure.
Follow the instructions here to create an account. It will ask for a payment method, which will be required to use the platform unfortunately. If you select the hobby plan it will be free as long as you don't create an organisation.
You can now install the Command Line Interface which is how we interact with the platform. Follow the instructions for your platform here and when you've installed run this in a terminal to log in
Let's get our app onto your machine. Run
to download our repository to your computer and open it in your code editor.
Part 1: Frontend
We'll start with the easy part, let's connect the frontend to the backend. If you open /web/src/main.tsx
, you'll see the code for our react-based frontend. Inspect our handleSubmit
function which is called when the user presses our Submit button.
Inside we write an asynchronous function that calls an endpoint on our backend. We add the data to an instance of FormData
, including a file called 'resume'. Notice, that our fetch requests the URL http://localhost:3000
. This isn't going to work when we deploy, so we'll want to change it. We'll change it in three ways:
- Protocol: When our API is running on the internet we want it to be secure, so we change our local http (hyper-text transfer protcol) to https (hyper text transfer protcol secure 🔒) which means that all of our servers traffic is encrypted so outsiders can't read it.
- Localhost. Our server will be running somewhere completely different, so we'll want to change the domain to that
- Port. We can remove the port because we know that it will be the main application running on our machine. When we remove the port, the port is still there, but it is assumed based on the protocol. For HTTP programmes, they are typically run on port 80 and for HTTPS, they are typically run on 443.
In theory our API url will look more like https://api.wdcc.co.nz
for example. Though instead of just hard-coding this in the fetch again, we'll use environment variables. We use environment variables to easily change information about the app from our cloud platform without having to recompile our code. Change the URL in the submitData
function to:
This uses our build tool Vite to manage our environment variables. At build time (when you run yarn run build
or npm run build
) this will replace the variable with the link. It is required that any environment variable starts with VITE on the frontend, otherwise it won't be accessible in your application unfortunately.
Create a .env
file inside the web folder and write:
VITE_API_URL="http://localhost:3000"
if you want to continue using your app locally for testing.
Part 2: Database
We can now move on to the difficult part, changing our API. Our project is already setup to use an environment variable. If you take a look at api/prisma/schema.prisma
you can see our database schema which outlines a datasource
which is described to accept our database url through an environment variable.
Prisma is an ORM (Object-Relational Mapper) for interfacing with a database, it's not important for this workshop, but if you aren't familiar with it and are interested, check out Prisma.
We don't have a database at the moment, so let's create one using Fly. Open up a terminal and run
Choose a globally unique name or let it randomly generate one, then select Sydney (SYD) as your region, then select Development as your configuration (important if you don't want to be charged) as well as scaling the node to zero after an hour.
This will print out your Connection String which you can use to connect to the database from your code, it should look something like this:
postgres://postgres:[email protected]:5432
Awesome! You now have a remote postgres database. There is one problem though, this is not accessible to your machine. It is hidden away behind a Virtual Private Cloud. This means it is only accessible to deployed applications and not any machine. This is great for security because it means that other people can't access your database remotely. If we want to use the same database to test locally, we can proxy the database, which means open up a connection for us to use. Remember the name of your app (it should say Postgres cluster powerful-wildflower-6985 created
, that is your app name) and run this command
If it won't let you proxy, you may need to start the database if you chose the development configuration:
If that doesn't work, you may have a database already running on your machine, you can simply change the port like this, just make sure your remember your new port.
You are now welcome to create an .env
file in the api
folder and jot down your connection string, though as we are proxying it, we need to adjust it by changing the powerful-wildflower.flycast
part to localhost:5432
(or with whatever port you are using) and you are good to go:
DATABASE_URL=postgres://postgres:EeRuCGInlfTMibD@localhost:5432
Inside your api
folder run
(if you don't have yarn installed run npm i -g yarn
)
Open a new terminal and, in your web folder, run
Then navigate to localhost:5173
in your browser and try the app. It should be fully functional and let you successfully apply to a hypothetical job.
Part 3: Storage Bucket
We need to somewhere to store the resumes people are uploading on the cloud. At the moment, it is being stored in the /uploads
folder on our local machine, but we want it to be cloud native and globally accessible. Let's create a bucket. We'll use Tigris which is a Amazon S3 compatible bucket that is integrated with Fly.
fly storage create --public
This command creates us a public storage bucket that anybody can view the contents of, but only we can upload to. This command should output a list of security variables that we can use to access it from our service. They should look like this:
AWS_ACCESS_KEY_ID=tid_OVzeLGJpfkgkhkhbtCVHmTIqnzTAcDcINEPPGPGPYhCyr
AWS_ENDPOINT_URL_S3=https://fly.storage.tigris.dev
AWS_REGION=auto
AWS_SECRET_ACCESS_KEY=tsec_pFKfkrekb9BVTAqVBEq-+b-ugkgkk4k4fjdEy6b9hwgkgk4iZzXvMjKU
BUCKET_NAME=wonderful-blueberry-3932
Let's save these in our .env
file.
We now need to change our code so that it uses our global system instead of just our local file storage. Run
yarn install @aws-sdk/client-s3
to install AWS's client for interacting with S3 and add this to the top of your index.ts
file:
Then create your client like this:
Then inside our apply endpoint, add the following code:
You should be all set to use our remote storage. If you run this locally, it will upload the data to the remote database and file storage.
### Part 4: Deployment
Let's complete the process by deploying your app to the cloud. Firstly, using your Fly CLI, we'll create the apps for both frontend and backend.
```
fly apps create App-Name
fly apps create App-Name-Api
```
Then inside both folders create a fly.toml
, this is where we will store the configuration information for each service.
Let's document the config for the API first. We'll have four different sections.
We then want to define how to build the app, and we'll use a Dockerfile for this. In your fly.toml
add:
this specifies the path for the Dockerfile. We then want to outline how the service runs:
Finally, let's describe the specs of the machine we want our app running on. We'll go with the most basic machine possible
Your fly.toml
should, in its entirety, look like this:
We now need our Dockerfile, so inside your api
folder create a file called Dockerfile
with no file extension and paste this in:
# syntax = docker/dockerfile:1
# Adjust NODE_VERSION as desired
ARG NODE_VERSION=20.3.0
FROM node:${NODE_VERSION}-slim as base
LABEL fly_launch_runtime="Node.js/Prisma"
# Node.js/Prisma app lives here
WORKDIR /app
# Set production environment
ENV NODE_ENV="production"
ARG YARN_VERSION=1.22.19
RUN npm install -g yarn@$YARN_VERSION --force
# Throw-away build stage to reduce size of final image
FROM base as build
# Install packages needed to build node modules
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential node-gyp openssl pkg-config python-is-python3
# Install node modules
COPY --link package.json yarn.lock ./
RUN yarn install --frozen-lockfile --production=false
# Generate Prisma Client
COPY --link prisma .
RUN yarn prisma generate
# Copy application code
COPY --link . .
# Build application
RUN yarn run build
# Remove development dependencies
RUN yarn install --production=true
# Final stage for app image
FROM base
# Install packages needed for deployment
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y openssl && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Copy built application
COPY --from=build /app /app
# Start the server by default, this can be overwritten at runtime
EXPOSE 3000
CMD [ "yarn", "run", "start" ]
This shows the container manager, step by step, how to install and run our application. You can generally get GPT to write this, but to teach the exact way to write this is a little more complicated and outside of the scope of this workshop.
Now let's set our secret environment variables. You can set a secret for your fly project with:
So go through all of the environment variables inside your env file and save them as secrets using this command. Note: for your database URL make sure you are not using the localhost one, and also change flycast
to internal
as this is likely to cause problems otherwise.
Now to deploy, cd into api if you haven't already then run
For the web, create a fly.toml
:
and the Dockerfile:
# syntax = docker/dockerfile:1
# Adjust NODE_VERSION as desired
ARG NODE_VERSION=20.3.0
FROM node:${NODE_VERSION}-slim as base
LABEL fly_launch_runtime="Vite"
# Vite app lives here
WORKDIR /app
# Set production environment
ENV NODE_ENV="production"
ARG YARN_VERSION=1.22.19
RUN npm install -g yarn@$YARN_VERSION --force
# Throw-away build stage to reduce size of final image
FROM base as build
# Install packages needed to build node modules
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3
# Install node modules
COPY --link package.json yarn.lock ./
RUN yarn install --frozen-lockfile --production=false
# Copy application code
COPY --link . .
ENV VITE_API_URL="https://software-arch-workshop-api.fly.dev"
# Build application
RUN yarn run build
# Remove development dependencies
RUN yarn install --production=true
# Final stage for app image
FROM nginx
# Copy built application
COPY --from=build /app/dist /usr/share/nginx/html
# Start the server by default, this can be overwritten at runtime
EXPOSE 80
CMD [ "/usr/sbin/nginx", "-g", "daemon off;" ]
Notice that we've added our build time environment variables in here such as VITE_API_URL just before we ask Docker to run the build command.