diff --git a/examples/aws-remix-container/.dockerignore b/examples/aws-remix-container/.dockerignore
new file mode 100644
index 000000000..25316b7aa
--- /dev/null
+++ b/examples/aws-remix-container/.dockerignore
@@ -0,0 +1,8 @@
+node_modules
+.cache
+build
+public/build
+
+
+# sst
+.sst
\ No newline at end of file
diff --git a/examples/aws-remix-container/.eslintrc.cjs b/examples/aws-remix-container/.eslintrc.cjs
new file mode 100644
index 000000000..4f6f59eee
--- /dev/null
+++ b/examples/aws-remix-container/.eslintrc.cjs
@@ -0,0 +1,84 @@
+/**
+ * This is intended to be a basic starting point for linting in your app.
+ * It relies on recommended configs out of the box for simplicity, but you can
+ * and should modify this configuration to best suit your team's needs.
+ */
+
+/** @type {import('eslint').Linter.Config} */
+module.exports = {
+ root: true,
+ parserOptions: {
+ ecmaVersion: "latest",
+ sourceType: "module",
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ env: {
+ browser: true,
+ commonjs: true,
+ es6: true,
+ },
+ ignorePatterns: ["!**/.server", "!**/.client"],
+
+ // Base config
+ extends: ["eslint:recommended"],
+
+ overrides: [
+ // React
+ {
+ files: ["**/*.{js,jsx,ts,tsx}"],
+ plugins: ["react", "jsx-a11y"],
+ extends: [
+ "plugin:react/recommended",
+ "plugin:react/jsx-runtime",
+ "plugin:react-hooks/recommended",
+ "plugin:jsx-a11y/recommended",
+ ],
+ settings: {
+ react: {
+ version: "detect",
+ },
+ formComponents: ["Form"],
+ linkComponents: [
+ { name: "Link", linkAttribute: "to" },
+ { name: "NavLink", linkAttribute: "to" },
+ ],
+ "import/resolver": {
+ typescript: {},
+ },
+ },
+ },
+
+ // Typescript
+ {
+ files: ["**/*.{ts,tsx}"],
+ plugins: ["@typescript-eslint", "import"],
+ parser: "@typescript-eslint/parser",
+ settings: {
+ "import/internal-regex": "^~/",
+ "import/resolver": {
+ node: {
+ extensions: [".ts", ".tsx"],
+ },
+ typescript: {
+ alwaysTryTypes: true,
+ },
+ },
+ },
+ extends: [
+ "plugin:@typescript-eslint/recommended",
+ "plugin:import/recommended",
+ "plugin:import/typescript",
+ ],
+ },
+
+ // Node
+ {
+ files: [".eslintrc.cjs"],
+ env: {
+ node: true,
+ },
+ },
+ ],
+};
diff --git a/examples/aws-remix-container/.gitignore b/examples/aws-remix-container/.gitignore
new file mode 100644
index 000000000..6a1b35d13
--- /dev/null
+++ b/examples/aws-remix-container/.gitignore
@@ -0,0 +1,8 @@
+node_modules
+
+/.cache
+/build
+.env
+
+# sst
+.sst
diff --git a/examples/aws-remix-container/Dockerfile b/examples/aws-remix-container/Dockerfile
new file mode 100644
index 000000000..026c03329
--- /dev/null
+++ b/examples/aws-remix-container/Dockerfile
@@ -0,0 +1,45 @@
+# base node image
+FROM node:18-bullseye-slim as base
+
+# set for base and all layer that inherit from it
+ENV NODE_ENV production
+
+# Install all node_modules, including dev dependencies
+FROM base as deps
+
+WORKDIR /myapp
+
+ADD package.json ./
+RUN npm install --include=dev
+
+# Setup production node_modules
+FROM base as production-deps
+
+WORKDIR /myapp
+
+COPY --from=deps /myapp/node_modules /myapp/node_modules
+ADD package.json ./
+RUN npm prune --omit=dev
+
+# Build the app
+FROM base as build
+
+WORKDIR /myapp
+
+COPY --from=deps /myapp/node_modules /myapp/node_modules
+
+ADD . .
+RUN npm run build
+
+# Finally, build the production image with minimal footprint
+FROM base
+
+WORKDIR /myapp
+
+COPY --from=production-deps /myapp/node_modules /myapp/node_modules
+
+COPY --from=build /myapp/build /myapp/build
+COPY --from=build /myapp/public /myapp/public
+ADD . .
+
+CMD ["npm", "start"]
diff --git a/examples/aws-remix-container/README.md b/examples/aws-remix-container/README.md
new file mode 100644
index 000000000..6c4d2168f
--- /dev/null
+++ b/examples/aws-remix-container/README.md
@@ -0,0 +1,40 @@
+# Welcome to Remix!
+
+- 📖 [Remix docs](https://remix.run/docs)
+
+## Development
+
+Run the dev server:
+
+```shellscript
+npm run dev
+```
+
+## Deployment
+
+First, build your app for production:
+
+```sh
+npm run build
+```
+
+Then run the app in production mode:
+
+```sh
+npm start
+```
+
+Now you'll need to pick a host to deploy it to.
+
+### DIY
+
+If you're familiar with deploying Node applications, the built-in Remix app server is production-ready.
+
+Make sure to deploy the output of `npm run build`
+
+- `build/server`
+- `build/client`
+
+## Styling
+
+This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever css framework you prefer. See the [Vite docs on css](https://vitejs.dev/guide/features.html#css) for more information.
diff --git a/examples/aws-remix-container/app/entry.client.tsx b/examples/aws-remix-container/app/entry.client.tsx
new file mode 100644
index 000000000..94d5dc0de
--- /dev/null
+++ b/examples/aws-remix-container/app/entry.client.tsx
@@ -0,0 +1,18 @@
+/**
+ * By default, Remix will handle hydrating your app on the client for you.
+ * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
+ * For more information, see https://remix.run/file-conventions/entry.client
+ */
+
+import { RemixBrowser } from "@remix-run/react";
+import { startTransition, StrictMode } from "react";
+import { hydrateRoot } from "react-dom/client";
+
+startTransition(() => {
+ hydrateRoot(
+ document,
+
+
+
+ );
+});
diff --git a/examples/aws-remix-container/app/entry.server.tsx b/examples/aws-remix-container/app/entry.server.tsx
new file mode 100644
index 000000000..45db3229c
--- /dev/null
+++ b/examples/aws-remix-container/app/entry.server.tsx
@@ -0,0 +1,140 @@
+/**
+ * By default, Remix will handle generating the HTTP Response for you.
+ * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
+ * For more information, see https://remix.run/file-conventions/entry.server
+ */
+
+import { PassThrough } from "node:stream";
+
+import type { AppLoadContext, EntryContext } from "@remix-run/node";
+import { createReadableStreamFromReadable } from "@remix-run/node";
+import { RemixServer } from "@remix-run/react";
+import { isbot } from "isbot";
+import { renderToPipeableStream } from "react-dom/server";
+
+const ABORT_DELAY = 5_000;
+
+export default function handleRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext,
+ // This is ignored so we can keep it in the template for visibility. Feel
+ // free to delete this parameter in your app if you're not using it!
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ loadContext: AppLoadContext
+) {
+ return isbot(request.headers.get("user-agent") || "")
+ ? handleBotRequest(
+ request,
+ responseStatusCode,
+ responseHeaders,
+ remixContext
+ )
+ : handleBrowserRequest(
+ request,
+ responseStatusCode,
+ responseHeaders,
+ remixContext
+ );
+}
+
+function handleBotRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext
+) {
+ return new Promise((resolve, reject) => {
+ let shellRendered = false;
+ const { pipe, abort } = renderToPipeableStream(
+ ,
+ {
+ onAllReady() {
+ shellRendered = true;
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
+
+ responseHeaders.set("Content-Type", "text/html");
+
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ })
+ );
+
+ pipe(body);
+ },
+ onShellError(error: unknown) {
+ reject(error);
+ },
+ onError(error: unknown) {
+ responseStatusCode = 500;
+ // Log streaming rendering errors from inside the shell. Don't log
+ // errors encountered during initial shell rendering since they'll
+ // reject and get logged in handleDocumentRequest.
+ if (shellRendered) {
+ console.error(error);
+ }
+ },
+ }
+ );
+
+ setTimeout(abort, ABORT_DELAY);
+ });
+}
+
+function handleBrowserRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext
+) {
+ return new Promise((resolve, reject) => {
+ let shellRendered = false;
+ const { pipe, abort } = renderToPipeableStream(
+ ,
+ {
+ onShellReady() {
+ shellRendered = true;
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
+
+ responseHeaders.set("Content-Type", "text/html");
+
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ })
+ );
+
+ pipe(body);
+ },
+ onShellError(error: unknown) {
+ reject(error);
+ },
+ onError(error: unknown) {
+ responseStatusCode = 500;
+ // Log streaming rendering errors from inside the shell. Don't log
+ // errors encountered during initial shell rendering since they'll
+ // reject and get logged in handleDocumentRequest.
+ if (shellRendered) {
+ console.error(error);
+ }
+ },
+ }
+ );
+
+ setTimeout(abort, ABORT_DELAY);
+ });
+}
diff --git a/examples/aws-remix-container/app/root.tsx b/examples/aws-remix-container/app/root.tsx
new file mode 100644
index 000000000..61c8b983d
--- /dev/null
+++ b/examples/aws-remix-container/app/root.tsx
@@ -0,0 +1,45 @@
+import {
+ Links,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
+} from "@remix-run/react";
+import type { LinksFunction } from "@remix-run/node";
+
+import "./tailwind.css";
+
+export const links: LinksFunction = () => [
+ { rel: "preconnect", href: "https://fonts.googleapis.com" },
+ {
+ rel: "preconnect",
+ href: "https://fonts.gstatic.com",
+ crossOrigin: "anonymous",
+ },
+ {
+ rel: "stylesheet",
+ href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
+ },
+];
+
+export function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
+
+export default function App() {
+ return ;
+}
diff --git a/examples/aws-remix-container/app/routes/_index.tsx b/examples/aws-remix-container/app/routes/_index.tsx
new file mode 100644
index 000000000..b5be85818
--- /dev/null
+++ b/examples/aws-remix-container/app/routes/_index.tsx
@@ -0,0 +1,39 @@
+import { Resource } from "sst";
+import { Cluster } from "ioredis";
+import { json } from "@remix-run/node";
+import type { MetaFunction } from "@remix-run/node";
+import { useLoaderData } from "@remix-run/react";
+
+const redis = new Cluster(
+ [{ host: Resource.MyRedis.host, port: Resource.MyRedis.port }],
+ {
+ dnsLookup: (address, callback) => callback(null, address),
+ redisOptions: {
+ tls: {},
+ username: Resource.MyRedis.username,
+ password: Resource.MyRedis.password,
+ },
+ }
+);
+
+export const meta: MetaFunction = () => {
+ return [
+ { title: "New Remix App" },
+ { name: "description", content: "Welcome to Remix!" },
+ ];
+};
+
+export async function loader() {
+ const counter = await redis.incr("counter");
+
+ return json({ counter });
+}
+
+export default function Index() {
+ const data = useLoaderData();
+ return (
+
+ Hit counter: {data.counter}
+
+ );
+}
diff --git a/examples/aws-remix-container/app/tailwind.css b/examples/aws-remix-container/app/tailwind.css
new file mode 100644
index 000000000..303fe158f
--- /dev/null
+++ b/examples/aws-remix-container/app/tailwind.css
@@ -0,0 +1,12 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+html,
+body {
+ @apply bg-white dark:bg-gray-950;
+
+ @media (prefers-color-scheme: dark) {
+ color-scheme: dark;
+ }
+}
diff --git a/examples/aws-remix-container/package.json b/examples/aws-remix-container/package.json
new file mode 100644
index 000000000..732ab3f46
--- /dev/null
+++ b/examples/aws-remix-container/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "aws-remix-container",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "build": "remix vite:build",
+ "dev": "remix vite:dev",
+ "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
+ "start": "remix-serve ./build/server/index.js",
+ "typecheck": "tsc"
+ },
+ "dependencies": {
+ "@remix-run/node": "^2.12.1",
+ "@remix-run/react": "^2.12.1",
+ "@remix-run/serve": "^2.12.1",
+ "ioredis": "^5.4.1",
+ "isbot": "^4.1.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "sst": "latest"
+ },
+ "devDependencies": {
+ "@remix-run/dev": "^2.12.1",
+ "@types/react": "^18.2.20",
+ "@types/react-dom": "^18.2.7",
+ "@typescript-eslint/eslint-plugin": "^6.7.4",
+ "@typescript-eslint/parser": "^6.7.4",
+ "autoprefixer": "^10.4.19",
+ "eslint": "^8.38.0",
+ "eslint-import-resolver-typescript": "^3.6.1",
+ "eslint-plugin-import": "^2.28.1",
+ "eslint-plugin-jsx-a11y": "^6.7.1",
+ "eslint-plugin-react": "^7.33.2",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "postcss": "^8.4.38",
+ "tailwindcss": "^3.4.4",
+ "typescript": "^5.1.6",
+ "vite": "^5.1.0",
+ "vite-tsconfig-paths": "^4.2.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+}
diff --git a/examples/aws-remix-container/postcss.config.js b/examples/aws-remix-container/postcss.config.js
new file mode 100644
index 000000000..2aa7205d4
--- /dev/null
+++ b/examples/aws-remix-container/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/examples/aws-remix-container/public/favicon.ico b/examples/aws-remix-container/public/favicon.ico
new file mode 100644
index 000000000..8830cf682
Binary files /dev/null and b/examples/aws-remix-container/public/favicon.ico differ
diff --git a/examples/aws-remix-container/public/logo-dark.png b/examples/aws-remix-container/public/logo-dark.png
new file mode 100644
index 000000000..b24c7aee3
Binary files /dev/null and b/examples/aws-remix-container/public/logo-dark.png differ
diff --git a/examples/aws-remix-container/public/logo-light.png b/examples/aws-remix-container/public/logo-light.png
new file mode 100644
index 000000000..4490ae793
Binary files /dev/null and b/examples/aws-remix-container/public/logo-light.png differ
diff --git a/examples/aws-remix-container/sst-env.d.ts b/examples/aws-remix-container/sst-env.d.ts
new file mode 100644
index 000000000..261f14f5b
--- /dev/null
+++ b/examples/aws-remix-container/sst-env.d.ts
@@ -0,0 +1,25 @@
+/* This file is auto-generated by SST. Do not edit. */
+/* tslint:disable */
+/* eslint-disable */
+import "sst"
+export {}
+declare module "sst" {
+ export interface Resource {
+ "MyRedis": {
+ "host": string
+ "password": string
+ "port": number
+ "type": "sst.aws.Redis"
+ "username": string
+ }
+ "MyService": {
+ "service": string
+ "type": "sst.aws.Service"
+ "url": string
+ }
+ "MyVpc": {
+ "bastion": string
+ "type": "sst.aws.Vpc"
+ }
+ }
+}
diff --git a/examples/aws-remix-container/sst.config.ts b/examples/aws-remix-container/sst.config.ts
new file mode 100644
index 000000000..cca260899
--- /dev/null
+++ b/examples/aws-remix-container/sst.config.ts
@@ -0,0 +1,26 @@
+///
+
+export default $config({
+ app(input) {
+ return {
+ name: "aws-remix-container",
+ removal: input?.stage === "production" ? "retain" : "remove",
+ home: "aws",
+ };
+ },
+ async run() {
+ const vpc = new sst.aws.Vpc("MyVpc", { bastion: true });
+ const redis = new sst.aws.Redis("MyRedis", { vpc });
+ const cluster = new sst.aws.Cluster("MyCluster", { vpc });
+
+ cluster.addService("MyService", {
+ link: [redis],
+ public: {
+ ports: [{ listen: "80/http", forward: "3000/http" }],
+ },
+ dev: {
+ command: "npm run dev",
+ },
+ });
+ }
+});
diff --git a/examples/aws-remix-container/tailwind.config.ts b/examples/aws-remix-container/tailwind.config.ts
new file mode 100644
index 000000000..14d0f00ce
--- /dev/null
+++ b/examples/aws-remix-container/tailwind.config.ts
@@ -0,0 +1,22 @@
+import type { Config } from "tailwindcss";
+
+export default {
+ content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"],
+ theme: {
+ extend: {
+ fontFamily: {
+ sans: [
+ '"Inter"',
+ "ui-sans-serif",
+ "system-ui",
+ "sans-serif",
+ '"Apple Color Emoji"',
+ '"Segoe UI Emoji"',
+ '"Segoe UI Symbol"',
+ '"Noto Color Emoji"',
+ ],
+ },
+ },
+ },
+ plugins: [],
+} satisfies Config;
diff --git a/examples/aws-remix-container/tsconfig.json b/examples/aws-remix-container/tsconfig.json
new file mode 100644
index 000000000..9d87dd378
--- /dev/null
+++ b/examples/aws-remix-container/tsconfig.json
@@ -0,0 +1,32 @@
+{
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ "**/.server/**/*.ts",
+ "**/.server/**/*.tsx",
+ "**/.client/**/*.ts",
+ "**/.client/**/*.tsx"
+ ],
+ "compilerOptions": {
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "types": ["@remix-run/node", "vite/client"],
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "target": "ES2022",
+ "strict": true,
+ "allowJs": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["./app/*"]
+ },
+
+ // Vite takes care of building everything, not tsc.
+ "noEmit": true
+ }
+}
diff --git a/examples/aws-remix-container/vite.config.ts b/examples/aws-remix-container/vite.config.ts
new file mode 100644
index 000000000..54066fb7a
--- /dev/null
+++ b/examples/aws-remix-container/vite.config.ts
@@ -0,0 +1,16 @@
+import { vitePlugin as remix } from "@remix-run/dev";
+import { defineConfig } from "vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+export default defineConfig({
+ plugins: [
+ remix({
+ future: {
+ v3_fetcherPersist: true,
+ v3_relativeSplatPath: true,
+ v3_throwAbortReason: true,
+ },
+ }),
+ tsconfigPaths(),
+ ],
+});
diff --git a/www/src/assets/docs/start/start-remix-container.png b/www/src/assets/docs/start/start-remix-container.png
new file mode 100644
index 000000000..17ce75914
Binary files /dev/null and b/www/src/assets/docs/start/start-remix-container.png differ
diff --git a/www/src/content/docs/docs/start/aws/nextjs.mdx b/www/src/content/docs/docs/start/aws/nextjs.mdx
index 3ba92bb80..1ad917ec2 100644
--- a/www/src/content/docs/docs/start/aws/nextjs.mdx
+++ b/www/src/content/docs/docs/start/aws/nextjs.mdx
@@ -542,7 +542,7 @@ You can use any stage name here but it's good to create a new stage for producti
Congrats! Your app should now be live!
-![SST Next.js app](../../../../../assets/docs/start/start-nextjs-container.png)
+![SST Next.js container app](../../../../../assets/docs/start/start-nextjs-container.png)
---
diff --git a/www/src/content/docs/docs/start/aws/remix.mdx b/www/src/content/docs/docs/start/aws/remix.mdx
index 125122663..7f5676451 100644
--- a/www/src/content/docs/docs/start/aws/remix.mdx
+++ b/www/src/content/docs/docs/start/aws/remix.mdx
@@ -3,7 +3,26 @@ title: Remix on AWS with SST
description: Create and deploy a Remix app to AWS with SST.
---
-We are going to create a Remix app, add an S3 Bucket for file uploads, and deploy it to AWS using SST.
+There are two ways to deploy a Remix app to AWS with SST.
+
+1. [Serverless](#serverless)
+2. [Containers](#containers)
+
+We'll use both to build a couple of simple apps below.
+
+---
+
+#### Examples
+
+We also have a few other Remix examples that you can refer to.
+
+- [Enabling streaming in your Remix app](/docs/examples/#aws-remix-streaming)
+
+---
+
+## Serverless
+
+We are going to create a Remix app, add an S3 Bucket for file uploads, and deploy it to using the `Remix` component.
:::tip[View source]
You can [view the source](https://github.com/sst/ion/tree/dev/examples/aws-remix) of this example in our repo.
@@ -13,12 +32,12 @@ Before you get started, make sure to [configure your AWS credentials](/docs/iam-
---
-## 1. Create a project
+### 1. Create a project
Let's start by creating our project.
```bash
-npx create-remix@latest
+npx create-remix@latest aws-remix
cd aws-remix
```
@@ -26,7 +45,7 @@ We are picking all the default options.
---
-#### Init SST
+##### Init SST
Now let's initialize SST in our app.
@@ -39,7 +58,7 @@ Select the defaults and pick **AWS**. This'll create a `sst.config.ts` file in y
---
-#### Start dev mode
+##### Start dev mode
Run the following to start dev mode. This'll start SST and your Remix app.
@@ -51,7 +70,7 @@ Once complete, click on **MyWeb** in the sidebar and open your Remix app in your
---
-## 2. Add an S3 Bucket
+### 2. Add an S3 Bucket
Let's allow public `access` to our S3 Bucket for file uploads. Update your `sst.config.ts`.
@@ -63,7 +82,7 @@ const bucket = new sst.aws.Bucket("MyBucket", {
Add this above the `Remix` component.
-#### Link the bucket
+##### Link the bucket
Now, link the bucket to our Remix app.
@@ -75,7 +94,7 @@ new sst.aws.Remix("MyWeb", {
---
-## 3. Create an upload form
+### 3. Create an upload form
Add the upload form client in `app/routes/_index.tsx`. Replace the `Index` component with:
@@ -113,7 +132,7 @@ export default function Index() {
---
-## 4. Generate a pre-signed URL
+### 4. Generate a pre-signed URL
When our app loads, we'll generate a pre-signed URL for the file upload and use it in the form.
@@ -153,7 +172,7 @@ Head over to the local Remix app in your browser, `http://localhost:5173` and tr
---
-## 5. Deploy your app
+### 5. Deploy your app
Now let's deploy your app to AWS.
@@ -169,6 +188,272 @@ Congrats! Your site should now be live!
---
+## Containers
+
+We are going to build a hit counter Remix app with Redis. We’ll the deploy it to AWS in a container using the `Cluster` component.
+
+:::tip[View source]
+You can [view the source](https://github.com/sst/ion/tree/dev/examples/aws-remix-container) of this example in our repo.
+:::
+
+Before you get started, make sure to [configure your AWS credentials](/docs/iam-credentials#credentials).
+
+---
+
+### 1. Create a project
+
+Let's start by creating our app.
+
+```bash
+npx create-remix@latest aws-remix-container
+cd aws-remix-container
+```
+
+We are picking all the default options.
+
+---
+
+##### Init SST
+
+Now let's initialize SST in our app.
+
+```bash
+npx sst@latest init
+```
+
+Select the defaults and pick **AWS**. This'll create a `sst.config.ts` file in your project root.
+
+---
+
+### 2. Add a Cluster
+
+To deploy our Remix app in a container, we'll use [AWS Fargate](https://aws.amazon.com/fargate/) with [Amazon ECS](https://aws.amazon.com/ecs/). Replace the `run` function in your `sst.config.ts`.
+
+```js title="sst.config.ts" {9-11}
+async run() {
+ const vpc = new sst.aws.Vpc("MyVpc", { bastion: true });
+ const cluster = new sst.aws.Cluster("MyCluster", { vpc });
+
+ cluster.addService("MyService", {
+ public: {
+ ports: [{ listen: "80/http", forward: "3000/http" }],
+ },
+ dev: {
+ command: "npm run dev",
+ },
+ });
+}
+```
+
+This creates a VPC with a bastion host, an ECS Cluster, and adds a Fargate service to it.
+
+The `dev.command` tells SST to run our Remix app locally in dev mode.
+
+---
+
+### 3. Add Redis
+
+Let's add an [Amazon ElastiCache](https://aws.amazon.com/elasticache/) Redis cluster. Add this below the `Vpc` component in your `sst.config.ts`.
+
+```js title="sst.config.ts"
+const redis = new sst.aws.Redis("MyRedis", { vpc });
+```
+
+This shares the same VPC as our ECS cluster.
+
+---
+
+#### Link Redis
+
+Now, link the Redis cluster to the container.
+
+```ts title="sst.config.ts" {3}
+cluster.addService("MyService", {
+ // ...
+ link: [redis],
+});
+```
+
+This will allow us to reference the Redis cluster in our Remix app.
+
+---
+
+#### Install a tunnel
+
+Since our Redis cluster is in a VPC, we'll need a tunnel to connect to it from our local machine.
+
+```bash "sudo"
+sudo npx sst tunnel install
+```
+
+This needs _sudo_ to create a network interface on your machine. You'll only need to do this once on your machine.
+
+---
+
+#### Start dev mode
+
+Start your app in dev mode.
+
+```bash
+npx sst dev
+```
+
+This will deploy your app, start a tunnel in the **Tunnel** tab, and run your Remix app locally in the **MyServiceDev** tab.
+
+---
+
+### 4. Connect to Redis
+
+We want the `/` route to increment a counter in our Redis cluster. Let's start by installing the npm package we'll use.
+
+```bash
+npm install ioredis
+```
+
+We'll add a `loader` to increment the counter.
+
+```ts title="app/routes/_index.tsx" {2}
+const redis = new Cluster(
+ [{ host: Resource.MyRedis.host, port: Resource.MyRedis.port }],
+ {
+ dnsLookup: (address, callback) => callback(null, address),
+ redisOptions: {
+ tls: {},
+ username: Resource.MyRedis.username,
+ password: Resource.MyRedis.password,
+ },
+ }
+);
+
+export async function loader() {
+ const counter = await redis.incr("counter");
+
+ return json({ counter });
+}
+```
+
+:::tip
+We are directly accessing our Redis cluster with `Resource.MyRedis.*`.
+:::
+
+Add the relevant imports.
+
+```ts title="app/routes/_index.tsx"
+import { Resource } from "sst";
+import { Cluster } from "ioredis";
+import { json } from "@remix-run/node";
+import { useLoaderData } from "@remix-run/react";
+```
+
+Let's now use this in our `Index` component.
+
+```ts title="app/routes/_index.tsx"
+export default function Index() {
+ const data = useLoaderData();
+
+ return (
+
+ Hit counter: {data.counter}
+
+ );
+}
+```
+
+---
+
+#### Test your app
+
+Let's head over to `http://localhost:5173` in your browser and it'll show the current hit counter.
+
+You should see it increment every time you **refresh the page**.
+
+---
+
+### 5. Deploy your app
+
+To deploy our app we'll add a `Dockerfile`.
+
+
+View Dockerfile
+
+```diff lang="dockerfile" title="Dockerfile"
+# Based on https://github.com/remix-run/blues-stack/blob/main/Dockerfile
+
+# base node image
+FROM node:18-bullseye-slim as base
+
+# set for base and all layer that inherit from it
+ENV NODE_ENV production
+
+# Install all node_modules, including dev dependencies
+FROM base as deps
+
+WORKDIR /myapp
+
+ADD package.json ./
+RUN npm install --include=dev
+
+# Setup production node_modules
+FROM base as production-deps
+
+WORKDIR /myapp
+
+COPY --from=deps /myapp/node_modules /myapp/node_modules
+ADD package.json ./
+RUN npm prune --omit=dev
+
+# Build the app
+FROM base as build
+
+WORKDIR /myapp
+
+COPY --from=deps /myapp/node_modules /myapp/node_modules
+
+ADD . .
+RUN npm run build
+
+# Finally, build the production image with minimal footprint
+FROM base
+
+WORKDIR /myapp
+
+COPY --from=production-deps /myapp/node_modules /myapp/node_modules
+
+COPY --from=build /myapp/build /myapp/build
+COPY --from=build /myapp/public /myapp/public
+ADD . .
+
+CMD ["npm", "start"]
+```
+
+
+:::tip
+You need to be running [Docker Desktop](https://www.docker.com/products/docker-desktop/) to deploy your app.
+:::
+
+Let's also add a `.dockerignore` file in the root.
+
+```bash title=".dockerignore"
+node_modules
+.cache
+build
+public/build
+```
+
+Now to build our Docker image and deploy we run:
+
+```bash
+npx sst deploy --stage production
+```
+
+You can use any stage name here but it's good to create a new stage for production.
+
+Congrats! Your app should now be live!
+
+![SST Remix container app](../../../../../assets/docs/start/start-remix-container.png)
+
+---
+
## Connect the console
As a next step, you can setup the [SST Console](/docs/console/) to _**git push to deploy**_ your app and monitor it for any issues.