Starting with a React app
React is a great library for building websites, and really shines when building single-page applications. Create React App makes it really easy to get started.
Many services can host static websites. HOPS' also manages the build process and connects to GitHub, and makes the release process smooth and fast.
This example uses create-react-app@^5.0
with TypeScript and the pnpm package manager.
We use Caddy to serve the packaged app.
Creating our application
First, initialize a new project directory, my-hops-app
, with the create-react-app
tool.
$ npx "create-react-app@~5.0" my-hops-app --template typescript
# Output (partial):
# Success! Created my-hops-app at /home/mirg/projects/my-hops-app (...)
$ cd my-hops-app
$ pnpm import && rm -rf node_modules package-lock.json && pnpm install
Unfortunately, there's a bug in one of the 11891 transitive dependencies used by create-react-app
.
We'll update .npmrc
to resolve this before proceeding:
# https://github.com/pnpm/pnpm/issues/4920#issuecomment-1226724790
cat <<EOF > .npmrc
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
public-hoist-pattern[]=@types*
EOF
That's it, we're good to go!
Creating a Dockerfile
A Dockerfile
is a recipe for how to create a container image, which is what we use to run applications in HOPS.
In this example, we use a multi-stage build to create a very small image containing just a web server and the packaged code.
Save the following as Dockerfile
in the base your project folder.
HOPS finds this and automatically builds it whenever you commit to your repository.
# syntax=docker/dockerfile:1
# This line describes the capabilities of our builder. Do not remove it, it has
# to be the first line in our Dockerfile.
# This is a multi-stage build. A container image is a stack of layers. Each
# action creates a layer. For projects like this, we can end up with images
# that contain a lot of files that are not necessary, like the Node runtime,
# the entire node_modules directory, etc. We can work around this by building
# in one "stage", and then moving only the required files for running the
# application into a new, clean and minimal image.
# See: https://docs.docker.com/build/building/multi-stage/
# You can use variables in Dockerfiles!
ARG app_name="my-react-app"
# Build in the Node 18 runtime environment.
FROM node:18.15.0-alpine as build
ARG app_name
# Install the pnpm package manager.
# Set pnpm's home directory explicitly to get consistent behaviour in case we
# change something, like the build OS or user.
ENV PNPM_HOME /var/lib/hops/pnpm
# See: https://pnpm.io/installation#using-npm
RUN npm install -g pnpm@7.33.1
# Copy in our source code.
COPY . /opt/${app_name}
# Fetches our dependencies.
# - The --mount=type=cache flag makes our builders store cached files
# (downloaded packages) between builds. The location is configured by the
# PNPM_HOME variable above.
# - The --prefer-offline flag prefers packages already in PNPN_HOME.
# - The --package-import-method=copy flag fixes a filesystem issue with caches.
# - The --frozen-lockfile flag ensures no surprise package updates occur.
RUN --mount=type=cache,id=hops_cra,target=/var/lib/hops/pnpm/store \
pnpm install \
--dir /opt/${app_name} \
--package-import-method copy \
--prefer-offline \
--frozen-lockfile
# Run the create-react-app build script. This outputs the finished application
# into the /opt/${app_name}/build directory. We don't specify NODE_ENV here,
# because create-react-app does this for us automatically.
RUN pnpm run --dir /opt/${app_name} build
# create-react-app's output only requires JavaScript in the browser, not on the
# server, so we can serve the project with a regular web server, like Caddy.
# We only need to copy in our build directory and update the configuration.
# See: https://caddyserver.com/
FROM caddy:2.6.4-alpine
ARG app_name
# Copy our build directory from the build step.
COPY --from=build /opt/${app_name}/build /usr/share/${app_name}
# Write our custom Caddy config to /etc/caddy/Caddyfile
COPY <<EOF /etc/caddy/Caddyfile
{
# We don't need the admin endpoint.
admin off
servers {
# Enable metrics
metrics
}
}
:{\$PORT:3000} {
# https://caddyserver.com/docs/caddyfile/patterns#single-page-apps-spas
encode gzip
root * /usr/share/${app_name}
# HOPS requires an endpoint for health checks.
handle {\$HOPS_READINESS_PATH:/health} {
respond OK 200
}
handle {
# If there's no file at {path}, use /index.html.
try_files {path} /index.html
file_server
}
}
:{\$HOPS_METRICS_PORT:3001} {
# Expose metrics on this internal port and path
metrics {\$HOPS_METRICS_PATH:/metrics}
}
EOF
Creating the iterapp.toml
file
The iterapp.toml
file describes how to run our app, and which resources we need to do that.
Because the app is configured to listen to the PORT
and HOPS_READINESS_CHECK
variables, we can manage this with our environment variables.
We prefer to release our apps straight to production when the main
branch is updated, so we'll specify that.
In addition, we need to opt in to collecting metrics. We'll expose them on an internal port, 3001
.
This is our iterapp.toml
file for this project:
# iterapp.toml
default_environment="prod"
[metrics]
port = 3001
path = "/metrics"
Deploying our app
Create a repository on GitHub under the iterate
organization, and push your code to it.
V2 | ✨ V3 ✨ |
---|---|
RegisterIn Slack, go to DeployTo deploy your app for the first time, go to |
RegisterThe primary means of interacting with HOPS is through the CLI. To register your application from step 1:
DeployTo deploy your app for the first time, run
This should deploy your application. Check it out at |
🚀 Happy hacking!
Resources
-
You can speed up your builds slightly by adding
node_modules
to.dockerignore
.# .dockerignore node_modules
-
If you're using GitHub, you should add an entry to
.github/dependabot.yml
to keep your docker images up to date:# .github/dependabot.yml version: 2 updates: - package-ecosystem: "docker" directory: "/" schedule: interval: "weekly"
As of Thursday, March 16th 2023.