---
title: "Deploy to Deno Deploy"
description: "Run a Remix 3 app on Deno Deploy: the built fetch handler behind a three-line Deno.serve entrypoint, built and deployed by Deploy's own infrastructure."
url: "https://pitlane.tools/deploy/deno-deploy"
buildMode: "vite"
counterpart: "https://pitlane.tools/deploy/deno-deploy-no-build.md"
---

# Deploy to Deno Deploy

This guide for: **Vite** · [No Build](https://pitlane.tools/deploy/deno-deploy-no-build.md)

Deploy a Remix 3 app to [Deno Deploy](https://deno.com/deploy), the new platform at [console.deno.com](https://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.

> [!TIP]
>
> **Start from the template**
>
> `npx giget github:pitlane-tools/templates/deno-deploy my-app` scaffolds a working guest book app wired for this guide. See [pitlane-tools/templates](https://github.com/pitlane-tools/templates).

> [!WARNING]
>
> **New Deploy only**
>
> This guide targets the current Deno Deploy. Deploy Classic (`dash.deno.com`) shut down on July 20, 2026. If you're coming from it, see the [migration guide](https://docs.deno.com/deploy/migration_guide/).

## Configuration

[`remix()`](https://pitlane.tools/package/vite-plugin-remix/function/remix.md) needs no Deploy-specific options. The defaults apply. `deno.json` and `main.ts` adapt the project to Deno:

```jsonc title="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:

```ts title="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

```sh
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:

```sh
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:

```sh
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.

```sh
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:

```sh
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](https://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:

```yaml title=".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](https://pitlane.tools/guides/spa.md): `server: false` builds no server environment, so there is no wrapper entrypoint to write. The plugin's remaining job is [component HMR](https://pitlane.tools/guides/hmr.md#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.

```html title="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>
```

```tsx title="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 />);
```

```jsonc title="tsconfig.json"
{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "remix/component" } }
```

```ts title="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](https://pitlane.tools/guides/umbrella.md) 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:

```sh
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](#deploy-with-github-actions) above.
