Skip to content

Search the guides, deployment pages, and API reference.

Deno Deploy

Deploy a Remix 3 app to Deno Deploy, the new platform at console.deno.com, which runs full Deno 2 apps with CDN caching and built-in logs, traces, and metrics. No platform plugin is involved: the Vite build runs in your CI, and Deploy runs the uploaded fetch handler under real Deno.

Configuration

remix() needs no Deploy-specific options. The defaults apply. deno.json and main.ts adapt the project to Deno:

deno.json
{
"nodeModulesDir": "auto",
"tasks": {
"build": "vite build",
},
"deploy": {
// Uploads respect .gitignore; un-exclude the built output so the
// deploy tarball ships dist/.
"exclude": ["!dist"],
},
}

Deploy's Dynamic Entrypoint is executed the way deno run executes a file, so it must start a server. Wrap the built fetch handler in three lines:

main.ts
import server from "./dist/ssr/index.js";
Deno.serve({ port: Number(Deno.env.get("PORT") ?? 8000) }, request => server.fetch(request));

Static assets keep flowing through the staticFiles("./dist/client") middleware in your router, same as every other server target.

The app's build configuration (dashboard Edit build config, or CLI flags):

Setting Value
Framework preset No Preset
Install command deno install --allow-scripts
Build command (empty: the Vite build runs before deploying)
Runtime configuration Dynamic
Dynamic Entrypoint main.ts

The Vite build runs before deno deploy, and the deploy.exclude entry above un-excludes the gitignored dist/ from the upload, so the tarball already contains the fingerprinted assets the running app serves. The install command only restores node_modules for the runtime. Leave the build command empty so Deploy doesn't build a second time.

Local development

Terminal window
vp dev # dev server through Pitlane's Vite plugin
vp build # production build
vp preview # serve the production build

To exercise the production bundle under Deno itself before deploying:

Terminal window
deno serve --port 3000 dist/ssr/index.js

Deploy with the CLI

The deno deploy command comes with the Deno CLI. Build first, then deploy. The CLI tarballs the directory (respecting .gitignore, so the deploy.exclude un-exclusion above) and uploads it:

Terminal window
vp build
deno deploy --app my-remix-app --prod

(The first bare deno deploy run authenticates and prompts for an app name.)

Create the app non-interactively (flags switch the wizard off):

Run it after vp build: --source local uploads this directory and publishes a first revision straight away.

Terminal window
deno deploy create \
--org my-org \
--app my-remix-app \
--source local \
--do-not-use-detected-build-config \
--install-command "deno install --allow-scripts" \
--runtime-mode dynamic \
--entrypoint main.ts \
--region global

Without --do-not-use-detected-build-config, a build configuration Deploy detects in the directory takes precedence over the flags. --region also takes us or eu.

Useful companions:

Terminal window
deno deploy logs # stream runtime logs
deno deploy env load .env # push env vars; secret-looking keys are auto-marked secret

Deploy with GitHub Actions

Create an access token at console.deno.com/account/access-tokens and store it as the DENO_DEPLOY_TOKEN repository secret. The workflow builds the Vite app (producing the fingerprinted assets in dist/) and then uploads it:

.github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: voidzero-dev/setup-vp@v1.21.1
with:
cache: true
- run: vp install --frozen-lockfile
- run: vp build
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- run: deno deploy --app my-remix-app --prod
env:
DENO_DEPLOY_TOKEN: ${{ secrets.DENO_DEPLOY_TOKEN }}

Environment variables

Set them per context (Production for production domains, Development for preview/branch URLs) in the dashboard's environment-variables drawer, or push a local file with deno deploy env load .env. They reach the app through Deno.env.get() (and process.env under Node compatibility).

Client-only apps

A client-only Remix 3 app runs remix() in SPA mode: server: false builds no server environment, so there is no wrapper entrypoint to write. The plugin's remaining job is component HMR, so it is worth keeping in a static build. Deno Deploy hosts static sites first-class, including a Single Page App mode that serves index.html for unmatched paths.

index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My Remix App</title>
<script type="module" src="/app/main.tsx"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
app/main.tsx
import { createRoot, on, type Handle } from "remix/component";
function App(handle: Handle) {
let count = 0;
return () => (
<button
mix={[
on("click", () => {
count++;
handle.update();
}),
]}
>
Count: {count}
</button>
);
}
createRoot(document.getElementById("app")!).render(<App />);
tsconfig.json
{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "remix/component" } }
vite.config.ts
import { remix } from "pitlane/vite-plugin-remix";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [remix({ server: false })],
});

pitlane/vite-plugin-remix comes from the pitlane package. The deno-deploy template maps the scoped packages in its deno.jsonc, such as "@pitlane/vite-plugin-remix": "npm:@pitlane/vite-plugin-remix@…", so add pitlane before switching the import. Run deno add npm:pitlane, which records a "pitlane": "npm:pitlane@…" entry in the same imports map. Deno resolves pitlane/vite-plugin-remix and the other subpaths through that entry. The Pitlane package guide lists the remix and vite versions it requires.

Configure the app as Static with directory dist and Single Page App mode on, either in the dashboard or in one non-interactive command:

Terminal window
deno deploy create \
--org my-org \
--app my-remix-spa \
--source local \
--do-not-use-detected-build-config \
--runtime-mode static \
--static-dir dist \
--single-page-app \
--region global

Deploys are unchanged: deno deploy --prod from the CLI, or the same GitHub Actions workflow above.