Docker Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services. You can create and start all your services with a single command.
This example containerizes Redwood’s Web and API sides into individual containers that can be run with Docker Compose. The code for this example can be found on my GitHub.
Outline
Create Project
This example will start with a new Redwood project.
yarn create redwood-app redwood-docker-compose
cd redwood-docker-compose
Create the following files:
- Dockerfiles inside the
web
andapi
directories - Nginx configuration file in
web
directory -
docker-compose.yml
and.dockerignore
files in the root of the project
touch web/Dockerfile api/Dockerfile web/nginx.conf \
docker-compose.yml .dockerignore
Set up API Side
To set up the API side we need to have CORS configured, an apiUrl
specified, and a database migration applied to a production database.
Configure CORS
Our backend and frontend will each be in their own containers, and possibly on entirely separate domains. To ensure the frontend can query the backend, we will set origin
to *
and credentials
to true
in the cors
option of our GraphQL handler.
// api/src/functions/graphql.js
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import services from 'src/services/**/*.{js,ts}'
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'
export const handler = createGraphQLHandler({
loggerConfig: { logger, options: {} },
directives,
sdls,
services,
cors: {
origin: '*',
credentials: true,
},
onException: () => {
db.$disconnect()
},
})
Set apiUrl
Inside redwood.toml
set the apiUrl
to http://localhost:8911/api
. If you are deploying this to a service like Fly or Qovery, you will need to set this to the endpoint of your deployed GraphQL handler.
[web]
title = "Redwood App"
port = 8910
apiUrl = "http://localhost:8911/api"
[api]
port = 8911
[browser]
open = true
Prisma Schema
Our schema has the same Post
model used in the Redwood tutorial.
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Post {
id Int @id @default(autoincrement())
title String
body String
createdAt DateTime @default(now())
}
Add Database Environment Variables
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 api/db/.env
rm -rf .env .env.defaults
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
Apply Database Migration
yarn rw prisma migrate dev --name posts
Set up Web Side
Create a home page and generate a cell called BlogPostsCell
to perform our data fetching.
yarn rw g page home /
yarn rw g cell BlogPosts
BlogPostsCell
The query returns an array of posts
, each of which has an id
, title
, body
, and createdAt
date.
// 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
Import the BlogPostsCell
into HomePage
and return a <BlogPostsCell />
component.
// 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+Docker 🐳</h1>
<BlogPostsCell />
</>
)
}
export default HomePage
Scaffold Admin Dashboard
yarn rw g scaffold post
Set up Docker
We will have two Dockerfiles, a .dockerignore
file, an nginx.conf
configuration file, and a docker-compose.yml
file to stitch it all together.
dockerignore
node_modules
API Dockerfile
Our Dockerfile
is using the node:14-alpine
image. This may cause issues if you are on an M1 and want to build the image locally. Change node:14-alpine
to node:14
if you encounter this issue.
We set our working directory to app
and copy either the api
side or web
side along with .nvmrc
, graphql.config.js
, package.json
, redwood.toml
, and yarn.lock
.
FROM node:14-alpine
WORKDIR /app
COPY api api
COPY .nvmrc .
COPY graphql.config.js .
COPY package.json .
COPY redwood.toml .
COPY yarn.lock .
RUN yarn install --frozen-lockfile
RUN yarn add react react-dom --ignore-workspace-root-check
RUN yarn rw build api
RUN rm -rf ./api/src
WORKDIR /app/api
EXPOSE 8911
ENTRYPOINT [ "yarn", "rw", "serve", "api", "--port", "8911", "--rootPath", "/api" ]
Web Dockerfile
FROM node:14-alpine as builder
WORKDIR /app
COPY web web
COPY .nvmrc .
COPY graphql.config.js .
COPY package.json .
COPY redwood.toml .
COPY yarn.lock .
RUN yarn install --frozen-lockfile
RUN yarn rw build web
RUN rm -rf ./web/src
FROM nginx as runner
COPY --from=builder /app/web/dist /usr/share/nginx/html
COPY web/nginx.conf /etc/nginx/conf.d/default.conf
RUN ls -lA /usr/share/nginx/html
EXPOSE 8910
Nginx Configuration
Nginx is a web server that can also be used as a reverse proxy, load balancer, mail proxy or HTTP cache. Don’t ask me to explain this code, but I promise it works.
server {
listen 8910 default_server;
root /usr/share/nginx/html;
location ~* \.(?:css|js)$ {
expires 1h;
add_header Pragma public;
add_header Cache-Control "public";
access_log off;
}
location ~* \.(?:ico|gif|jpe?g|png)$ {
expires 7d;
add_header Pragma public;
add_header Cache-Control "public";
access_log off;
}
location / {
try_files $uri $uri/ /index.html;
}
}
Docker Compose File
We will have two services in our docker-compose.yml
file: web
and api
. Each will have ports
exposed and a build
that is set to the root directory for the context
along with the corresponding location for the Dockerfiles.
version: "3.9"
services:
web:
build:
context: .
dockerfile: ./web/Dockerfile
ports:
- "8910:8910"
api:
build:
context: .
dockerfile: ./api/Dockerfile
ports:
- "8911:8911"
Build Images
The docker compose up
command aggregates the output of each container and builds, (re)creates, starts, and attaches to containers for a service.
docker compose up
Open http://localhost:8910/posts
to create a test post and return to http://localhost:8910/
to see the result.
Check the image information with docker images
.
docker images
Keep in mind that I am on an M1 so this image is much larger than it would be with the Alpine version of Node. To see different approaches to optimizing your container, see Dockerize RedwoodJS and redwoodjs-docker
.
REPOSITORY TAG IMAGE ID CREATED SIZE
redwood-docker_api latest 243369952fa0 57 seconds ago 2.96GB
redwood-docker_web latest c1610495648c 42 minutes ago 137MB
See the specific running containers with docker ps
.
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a4d2a221278f redwood-docker_web "/docker-entrypoint.…" 35 seconds ago Up 34 seconds 80/tcp, 0.0.0.0:8910->8910/tcp redwood-docker-web-1
f5ab7bf289a9 redwood-docker_api "yarn rw serve api -…" 35 seconds ago Up 34 seconds 0.0.0.0:8911->8911/tcp redwood-docker-api-1
Test GraphQL Endpoint
Hit localhost:8911/api/graphql with your favorite API tool or curl. Send a query to the root schema asking for the current version.
curl \
--request POST \
--header 'content-type: application/json' \
--url 'http://localhost:8911/api/graphql' \
--data '{"query":"{ redwood { version } }"}'
{
"data":{
"redwood":{
"version":"0.41.0"
}
}
}
Send another query for the title
and body
of the posts
in the database.
curl \
--request POST \
--header 'content-type: application/json' \
--url 'http://localhost:8911/api/graphql' \
--data '{"query":"{ posts { title body } }"}'
{
"data":{
"posts":[
{
"title":"Docker Compose",
"body":"How to compose a Redwood app"
}
]
}
}
Publish to GitHub Container Registry
GitHub Packages is a platform for hosting and managing packages that combines your source code and packages in one place including containers and other dependencies. You can integrate GitHub Packages with GitHub APIs, GitHub Actions, and webhooks to create an end-to-end DevOps workflow that includes your code, CI, and deployment solutions.
GitHub Packages offers different package registries for commonly used package managers, such as npm, RubyGems, Maven, Gradle, and Docker. GitHub’s Container registry is optimized for containers and supports Docker and OCI images. To publish our images to the GitHub Container Registry, we need to first push our project to a GitHub repository.
Initialize Git
git init
git add .
git commit -m "I can barely contain my excitement"
Create a New Repository
You can create a blank repository by visiting repo.new or using the gh repo create
command with the GitHub CLI. Enter the following command to create a new repository, set the remote name from the current directory, and push the project to the newly created repository.
gh repo create redwood-docker-compose \
--public \
--source=. \
--remote=upstream \
--push
If you created a repository from the GitHub website instead of the CLI then you will need to set the remote and push the project with the following commands.
git remote add origin https://github.com/YOUR_USERNAME/redwood-docker-compose.git
git push -u origin main
Login to ghcr
To login, create a PAT (personal access token) and include it instead of xxxx
.
export CR_PAT=xxxx
Login with your own username in place of YOUR_USERNAME
.
echo $CR_PAT | docker login ghcr.io -u YOUR_USERNAME --password-stdin
Tag Images
Docker tags are mutable named references for pulling and running images, similar to branch refs in Git.
docker tag redwood-docker-compose_web ghcr.io/YOUR_USERNAME/redwood-docker-compose_web
docker tag redwood-docker-compose_api ghcr.io/YOUR_USERNAME/redwood-docker-compose_api
Push to Registry
Once you have tagged your image, you can push and pull the images much like you would push or pull a Git repository.
docker push ghcr.io/YOUR_USERNAME/redwood-docker-compose_web:latest
docker push ghcr.io/YOUR_USERNAME/redwood-docker-compose_api:latest
Pull from Registry
To test that our project has a docker image published to a public registry, pull it from your local development environment.
docker pull ghcr.io/YOUR_USERNAME/redwood-docker-compose_web:latest
docker pull ghcr.io/YOUR_USERNAME/redwood-docker-compose_api:latest