Skip to main content
Deno 2 is finally here 🎉️
Learn more

Static Files on Deno Deploy

Deno Deploy has always been great at dynamically generating content at the edge. We can run JavaScript code close to users, which can significantly reduce the response time latency. Many applications are not completely dynamic though: they have static assets like CSS files, client side JS, and images.

Until now Deno Deploy has not had a great way to deal with static assets. You had the choice of encoding them into the JavaScript code, hand rolling a CDN, or pulling the files from your GitHub repository. None of these options are ideal.

A new primitive

Deno Deploy now has first class support for static files. Your static files are stored on our network when you deploy your code, and are then distributed around the world close to where your users are. You can use the Deno file system APIs, and fetch to access these files from your JavaScript code running at the edge.

Because the actual serving of the files is still controlled by your code running at the edge, you have full control over all responses, even those to static files. For example, this can be used to:

  • serve files to only to signed-in users
  • add CORS headers to your files
  • modify files with some dynamic content at the edge before they are served
  • serve different files depending on the user’s browser

In Deno Deploy static files are not a completely separate system.

The most basic thing you can do, is read the entire file into memory and serve it to the user:

import { serve } from "https://deno.land/[email protected]/http/server.ts";

const HTML = await Deno.readFile("./index.html");

serve(async () => {
  return new Response(HTML, {
    headers: {
      "content-type": "text/html",
    },
  });
});

This works great for small files. For larger files, you can stream the file directly to the user instead of buffering it in memory:

import { serve } from "https://deno.land/[email protected]/http/server.ts";

const FILE_URL = new URL("/movie.webm", import.meta.url).href;

serve(async () => {
  const resp = await fetch(FILE_URL);
  return new Response(resp.body, {
    headers: {
      "content-type": "video/webm",
    },
  });
});

Want a directory listing of all the available files? Easy enough with Deno.readDir:

import { serve } from "https://deno.land/[email protected]/http/server.ts";

serve(async () => {
  const entries = [];
  for await (const entry of Deno.readDir(".")) {
    entries.push(entry);
  }

  const list = entries.map((entry) => {
    return `<li>${entry.name}</li>`;
  }).join("");

  return new Response(`<ul>${list}</ul>`, {
    headers: {
      "content-type": "text/html",
    },
  });
});

The standard library’s file serving utilities can be utilized to serve static files. These will set appropriate Content-Type headers and support more complex features like Range requests out of the box:

import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { serveFile } from "https://deno.land/[email protected]/http/file_server.ts";

serve(async (req) => {
  return await serveFile(req, `${Deno.cwd()}/static/index.html`);
});

You can also use Deno Deploy if you’re using a full-fledged HTTP framework like oak to serve your static content:

import { Application } from "https://deno.land/x/oak/mod.ts";

const app = new Application();
app.use(async (ctx) => {
  try {
    await ctx.send({
      root: `${Deno.cwd()}/static`,
      index: "index.html",
    });
  } catch {
    ctx.response.status = 404;
    ctx.response.body = "404 File not found";
  }
});

await app.listen({ port: 8000 });

A full list of the file system APIs Deno Deploy currently supports:

  • Deno.readFile to read a file into memory
  • Deno.readTextFile to read a file into memory as a UTF-8 string
  • Deno.readDir to get a list of files and folders in a folder
  • Deno.open to open a file for reading in chunks (for streaming)
  • Deno.stat to get information about a file or folder (get size or type)
  • Deno.lstat same as above, but doesn’t follow symlinks
  • Deno.realPath to get the path of a file or folder, after resolving symlinks
  • Deno.readLink to get the target for a symlink

Github Integration

Now you may ask yourself: how do I add these newfangled static files to my deployments?

By default, if you’ve linked a GitHub repository to Deploy, all of the files in your repository will available as static files. No changes needed. This is great if you use static files for a few assets stored in your repository, like images or the markdown files for your blog.

However, sometimes you want to generate static files at deploy time. For example when using a framework like Remix.run, or when you use a static site generator. For this scenario we now let you deploy your code and static assets with the deployctl tool. Serving your current working directory at the edge is as simple as:

deployctl deploy --project my-project --prod https://deno.land/[email protected]/http/file_server.ts

This works great when you just want to deploy once or if you don’t have your code in version control and always deploy from your local machine, but that isn’t the case for most projects.

Projects hosted on Github will want to use Github Actions to run a build step to generate HTML, or other static content, and then upload to Deno Deploy. We’ve provided a special Github Action step for exactly this purpose:

- name: Upload to Deno Deploy
  uses: denoland/deployctl@v1
  with:
    project: my-project
    entrypoint: main.js
    root: dist

One doesn’t even need to configure any access tokens or secrets for this to work. Just link your GitHub repository in the Deno Deploy dashboard and set the project to “GitHub Actions” deployment mode. Authentication is handled transparently by GitHub Actions.

Why GitHub Actions instead of a custom CI system? GitHub Actions is the de-facto standard for continuous integration now, and many many developers are already familiar with it. Why re-invent something that is already awesome?

Example: A statically generated site

To close out the blog post, here is a real example of a server running on deploy that serves a static site that is built in GitHub Actions. The site is built by a static site generator (in this case the awesome https://lumeland.github.io). Next to the static files, the site also includes a /api/time endpoint that dynamically returns the current time.

Try out the example at https://lume-example.deno.dev/.

Here is the GitHub Actions workflow file the project uses:

name: ci
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  deploy:
    name: deploy
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - name: Clone repository
        uses: actions/checkout@v2

      - name: Install Deno
        uses: denoland/setup-deno@main
        with:
          deno-version: 1.18.2

      - name: Build site
        run: deno run -A https://deno.land/x/lume/ci.ts

      - name: Upload to Deno Deploy
        uses: denoland/deployctl@v1
        with:
          project: lume-example
          entrypoint: server/main.ts

And the actual code that serves the site and the API endpoint:

import { Application, Router } from "https://deno.land/x/[email protected]/mod.ts";

const app = new Application();

// First we try to serve static files from the _site folder. If that fails, we
// fall through to the router below.
app.use(async (ctx, next) => {
  try {
    await ctx.send({
      root: `${Deno.cwd()}/_site`,
      index: "index.html",
    });
  } catch {
    next();
  }
});

const router = new Router();

// The /api/time endpoint returns the current time in ISO format.
router.get("/api/time", (ctx) => {
  ctx.response.body = { time: new Date().toISOString() };
});

// After creating the router, we can add it to the app.
app.use(router.routes());
app.use(router.allowedMethods());

await app.listen({ port: 8000 });

How fast are deployments? The time from git push to changes being live around the globe is around 25 seconds on this repo. 15 of those seconds is waiting for the GitHub Actions runner to get ready. For “automatic” mode deployments that don’t involve GitHub Actions, we average between 1-10s deploy times.

If you have any questions, comments, or suggestions, please let us know by opening an issue on feedback repository, or by sending us an email. Happy Denoing!


You can read more about the file system APIs in our docs: https://deno.com/deploy/docs/runtime-fs. More info about the GitHub integration can be found here: https://deno.com/deploy/docs/projects#git-integration.