How to deploy Remix apps with SSR to AWS Amplify

How to deploy Remix apps with SSR to AWS Amplify

·

3 min read

Featured on Hashnode

The AWS Amplify team recently announced the Amplify Hosting deployment specification. This is a way to deploy applications to AWS Amplify with server side rendering (SSR) support. While there are guides or support for many frameworks including Astro, NextJS and Nuxt I couldn't find one for Remix.

As I'm about to start working on a Shopify app (Shopify provides templates for Remix) and I didn't want to switch infrastructure providers I decided to investigate using how difficult it would be to use Amplify to host a Remix app with SSR.

The first step is to create a build file named amplify.yml which Amplify uses to build the project. I was deploying a new project and Amplify detected the file automatically during the initial deployment. Our build script handles four important tasks:

  1. Runs npm run build to build the application. Remix puts the output in into the build folder.

  2. Moves and restuctures the build folder output to meet the Amplify hosting specification by

    1. Renaming build to .amplify-hosting

    2. Renaming the client subfolder to static

    3. Moving the server subfolder to compute/default

  3. Reduce the size of our node-modules folder by running npm ci --omit dev to create a node-modules that only includes production dependencies. This is important for larger project as there is a limit on the maximum size of this folder. It also helps reduce cold start times. The folder is moved into the compute/default folder so the modules are available at runtime.

  4. Finally there are two files that we will create shortly that need to be copied into place.

    1. The server.js goes into .amplify-hosting/compute/default

    2. The deploy-manifest.json goes into .amplify-hosting/deploy-manifest.json

The complete amplify.yml is below:

version: 1
baseDirectory: .amplify-hosting
frontend:
  phases:
    preBuild:
      commands:
        - npm ci
    build:
      commands:
        - npm run build
        - mv build .amplify-hosting
        - mv .amplify-hosting/client .amplify-hosting/static
        - mkdir -p .amplify-hosting/compute
        - mv .amplify-hosting/server .amplify-hosting/compute/default
        - npm ci --omit dev
        - cp package.json .amplify-hosting/compute/default
        - cp -r node_modules .amplify-hosting/compute/default
        - cp server.js .amplify-hosting/compute/default
        - cp deploy-manifest.json .amplify-hosting/deploy-manifest.json
  artifacts:
    files:
      - "**/*"
    baseDirectory: .amplify-hosting

AWS Amplify will now package and deploy our .amplify-hosting folder.

The deploy-mainfest.json has two main tasks:

  1. Tell Amplify how to route traffic

  2. Tell Amplify how to configure and start the compute resources

This routing should not be confused with rewriting and redirecting traffic. It's purpose is to indicate if traffic should be handled as static or compute. Here I'm using the less than perfect approach that files with a . should be treated as static first and fall back to compute if they don't exist. Everything else should be treated as compute. This means that static files must have a . in the filename.

For the compute configuration I've set the runtime to Node 20 and the project should be started by using node server.js

The full deploly-mainfest.json is:

{
  "version": 1,
  "framework": {
    "name": "remix",
    "version": "2.8.1"
  },
  "routes": [
    {
      "path": "/*.*",
      "target": {
        "kind": "Static",
        "cacheControl": "public, max-age=2"
      },
      "fallback": {
        "kind": "Compute",
        "src": "default"
      }
    },
    {
      "path": "/*",
      "target": {
        "kind": "Compute",
        "src": "default"
      }
    }
  ],
  "computeResources": [
    {
      "name": "default",
      "runtime": "nodejs20.x",
      "entrypoint": "server.js"
    }
  ]
}

Finally I need to create a small Javascript file called server.js. This file launches an Express server on port 3000 which listens for requests and passes them to Remix. Remember to add express as a production dependency so this will work.

npm i express --save

The complete server.js is:

import remix from "@remix-run/express";
import express from "express";
import * as build from "./index.js";

const app = express();
const port = 3000;

app.all(
  "*",
  remix.createRequestHandler({
    build,
  })
);

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

That's it! You should be able to deploy the project to AWS Amplify and when you go to the URL provided your request will be execute on the server.