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 ✨

Register

In Slack, go to #iterapp-logs. Type /iterapp register {repository}, where {repository} is the part after iterate/. This configures HOPS to listen to your repository.

Deploy

To deploy your app for the first time, go to #iterapp-logs. Type /iterapp deploy {repository} prod main. This should deploy your application. Check it out at https://{repository}.app.iterate.no!

Register

The primary means of interacting with HOPS is through the CLI. To register your application from step 1:

  1. Install the CLI: (see CLI documentation for more)

    curl -SsLf https://cli.headless-operations.no/install.sh | sh
    
  2. Log in:

    hops v3 login
    
  3. Register:

    hops v3 register --cluster iterapp {repository}
    

    (Change {repository} to the the repository name including the organization, for example: iterate/dogs)

Deploy

To deploy your app for the first time, run

hops v3 deploy -a {repository} -e prod -r main`.

This should deploy your application. Check it out at https://{repository}.app.iterate.no!

🚀 Happy hacking!

Resources

1

As of Thursday, March 16th 2023.