Alright, we’ve got a working implementation. Check out the code here for the barebones version, or here for the finished tutorial blog. Huge thanks to Joshua Sierles for doing the majority of the heavy lifting here.
NOTE: This will not be the end state of deploying Redwood apps to Fly. We are still optimizing the build and there will be a yarn rw deploy fly
command to set this up automatically. However, if you want to start spiking something out now, you can follow these steps manually.
Creating and Deploying a Redwood App on Fly from Scratch
yarn create redwood-app redwood-fly
cd redwood-fly
Create Dockerfile
, .dockerignore
, fly.toml
, and .env
Normally .env
is contained in the root of your project, but as of now it will need to be contained inside your api/db
folder due to Docker weirdness.
touch fly.toml Dockerfile .dockerignore api/db/.env
rm -rf .env .env.defaults
prisma.schema
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
binaryTargets = "native"
}
model Post {
id Int @id @default(autoincrement())
title String
body String
createdAt DateTime @default(now())
}
Setup home page and BlogPosts
cell
yarn rw g page home /
yarn rw g cell BlogPosts
yarn rw g scaffold post
BlogPostsCell.js
// web/src/components/BlogPostsCell/BlogPostsCell.js
export const QUERY = gql`
query POSTS {
posts {
id
title
body
createdAt
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => <div>Empty</div>
export const Failure = ({ error }) => (
<div style={{ color: 'red' }}>Error: {error.message}</div>
)
export const Success = ({ posts }) => {
return posts.map((post) => (
<article key={post.id}>
<header>
<h2>{post.title}</h2>
</header>
<p>{post.body}</p>
<time>{post.createdAt}</time>
</article>
))
}
HomePage.js
// web/src/pages/HomePage/HomePage.js
import BlogPostsCell from 'src/components/BlogPostsCell'
import { MetaTags } from '@redwoodjs/web'
const HomePage = () => {
return (
<>
<MetaTags
title="Home"
description="This is the home page"
/>
<h1>Redwood+Fly 🦅</h1>
<BlogPostsCell />
</>
)
}
export default HomePage
DATABASE_URL
Include DATABASE_URL
in api/db/.env
. See this post for instructions on quickly setting up a remote database on Railway.
DATABASE_URL=postgresql://postgres:password@containers-us-west-10.railway.app:5513/railway
yarn rw prisma migrate dev --name fly-away
yarn rw dev
Open http://localhost:8910/posts
to create a test post and return to http://localhost:8910/
to see the result.
redwood.toml
Inside redwood.toml
set the apiUrl
to the following with the name of your project instead of redwood-fly
.
[web]
title = "Redwood App"
port = 8910
apiUrl = "https://redwood-fly.fly.dev/api/graphql"
includeEnvironmentVariables = []
[api]
port = 8911
[browser]
open = true
Deploy to Fly
fly launch
will configure your project.
fly launch --name redwood-fly
Dockerfile
This generates the following Dockerfile
.
FROM node:14-alpine as base
WORKDIR /app
COPY package.json package.json
COPY web/package.json web/package.json
COPY api/package.json api/package.json
COPY yarn.lock yarn.lock
RUN yarn install --frozen-lockfile
COPY redwood.toml .
COPY graphql.config.js .
FROM base as web_build
COPY web web
RUN yarn rw build web
FROM base as api_build
COPY api api
RUN yarn rw build api
FROM node:14-alpine
WORKDIR /app
# Only install API packages to keep image small
COPY api/package.json .
RUN yarn install && yarn add react react-dom @redwoodjs/api-server @redwoodjs/internal prisma
COPY graphql.config.js .
COPY redwood.toml .
COPY api api
COPY --from=web_build /app/web/dist /app/web/dist
COPY --from=api_build /app/api/dist /app/api/dist
COPY --from=api_build /app/api/db /app/api/db
COPY --from=api_build /app/node_modules/.prisma /app/node_modules/.prisma
# Entrypoint to @redwoodjs/api-server binary
CMD [ "yarn", "rw-server", "--port", "8910" ]
fly.toml
It will also generate the following fly.toml
file.
app = "redwood-fly"
kill_signal = "SIGINT"
kill_timeout = 5
processes = []
[deploy]
release_command = "npx prisma migrate deploy --schema '/app/api/db/schema.prisma'"
[env]
PORT = "8910"
[experimental]
allowed_public_ports = []
auto_rollback = true
[[services]]
http_checks = []
internal_port = 8910
processes = ["app"]
protocol = "tcp"
script_checks = []
[services.concurrency]
hard_limit = 25
soft_limit = 20
type = "connections"
[[services.ports]]
handlers = ["http"]
port = 80
[[services.ports]]
handlers = ["tls", "http"]
port = 443
[[services.tcp_checks]]
grace_period = "1s"
interval = "15s"
restart_limit = 0
timeout = "2s"
If you are on an M1 you will likely run into issues. You can add the --remote-only
flag to build the Docker image with Fly’s remote builder to avoid this issue and also speed up your build time.
fly deploy --remote-only
If all went according to plan you will see the following message:
Monitoring Deployment
1 desired, 1 placed, 1 healthy, 0 unhealthy [health checks: 1 total, 1 passing]
--> v1 deployed successfully
Live example - https://redwood-fly.fly.dev/
Test your endpoint
Warning: As of Redwood v0.36.x, Redwood’s API is open by default unless you specify an environment variable for secure services. This will be changing very soon in one of the upcoming minor releases before the v1 release candidate. If you follow this tutorial as is, your endpoint will be trollable.
Hit https://redwood-fly.fly.dev/api/graphql with your favorite API tool or curl.
Check Redwood Version
query REDWOOD_VERSION {
redwood {
version
}
}
Output:
{
"data": {
"redwood": {
"version": "0.36.4"
}
}
}
Query for all posts
query POSTS {
posts {
id
title
body
createdAt
}
}
Output:
{
"data": {
"posts": [
{
"id": 1,
"title": "This is a post",
"body": "Yeah it is",
"createdAt": "2021-09-09T20:10:58.985Z"
}
]
}
}
Create a post
mutation CREATE_POST_MUTATION {
createPost(
input: {
title:"this is a title",
body:"this is a body"
}
) {
id
title
body
createdAt
}
}
Output:
{
"data": {
"createPost": {
"id": 2,
"title": "this is a title",
"body": "this is a body",
"createdAt": "2021-09-20T02:00:24.899Z"
}
}
}
Delete a post
mutation DELETE_POST_MUTATION {
deletePost(
id: 2
) {
id
title
body
createdAt
}
}
Output:
{
"data": {
"deletePost": {
"id": 2,
"title": "this is a title",
"body": "this is a body",
"createdAt": "2021-09-20T02:00:24.899Z"
}
}
}