Dockerize RedwoodJS

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"
    }
  }
}
2 Likes