Deno is a modern take on the JavaScript/TypeScript runtime. It's a simple and lightweight runtime that uses the V8 Engine, is written in Rust, and it's creation was spearheaded by Ryan Dahl of NodeJS fame. Deno hit its 1.0 milestone back in 2020 and has since moved on to the 2.x line, with Deno 2.9.5 current as of August 2026, so it is a good time to start playing around with shipping Deno apps. Since Convox is language agnostic and multi-cloud we are all about shipping any app, written in any language, to any cloud. So let's get started and ship our first Deno app!
Updated for Deno 2: this post originally targeted Deno 1.0. The example now uses the built-in Deno.serve API instead of the old standard library HTTP server, dependencies are managed through deno.json and JSR or npm instead of pinned deno.land URLs, and the Dockerfile uses the official denoland/deno image.
Follow the instructions here for your operating system. I am on a Mac so I chose to use Homebrew.
$ brew install deno
Deno.serve({ port: 8080, hostname: "0.0.0.0" }, (_req) => {
return new Response("Hello World\n");
});
const port = 8080;
const s = serve({ port: port});
const body = new TextEncoder().encode("Hello World\n");
console.log(`Listening on ${hostname}:${port}`);
for await ( const req of s) {
req.respond({ body });
}
Deno 2 ships its HTTP server as the built-in Deno.serve API, so there is nothing to import. This very simple app creates a web server listening on port 8080 and responds to all requests with "Hello World".
We can test our app by running (Deno 2 also accepts -N as shorthand for --allow-net):
$ deno run --allow-net hello.ts
and then opening another terminal window and running:
$ curl http://localhost:8080
Hello World
All apps deployed on Convox run in containers and therefore need a Dockerfile.
All apps deployed on Convox run in containers and therefore need a Dockerfile. Deno now ships an official Docker image, denoland/deno, published in Debian (default), Ubuntu, Alpine, and distroless variants, so we will use that as our base.
Our app has no imports at all because Deno.serve is built into the runtime. If your app does have dependencies, Deno 2 manages them with deno add and deno install, records them in deno.json or package.json (both are supported natively), and pulls packages from JSR or npm. The Deno 1.x pattern of pinning versioned deno.land/std URLs in a deps.ts file is obsolete; the standard library is now stable and published on JSR.
The first thing we will do is cleanup our dependencies for better maintainability and caching. Let's create a dependencies file deps.ts and put our single dependency in it.
export { serve } from "https://deno.land/std@0.54.0/http/server.ts";
Then we can tweak our hello.ts file a little bit
import { serve } from "./deps.ts";
const hostname = "0.0.0.0";
const port = 8080;
const s = serve({ port: port});
const body = new TextEncoder().encode("Hello World\n");
console.log(`Listening on ${hostname}:${port}`);
for await ( const req of s) {
req.respond({ body });
}
Following the example in the official image repository we can create our Dockerfile.
FROM denoland/deno:2.9.5
EXPOSE 8080
WORKDIR /app
COPY . .
RUN deno cache hello.ts
USER deno
CMD ["run", "--allow-net", "hello.ts"]
As in the official example, the image runs the deno binary, so the CMD arguments start with run. Pinning the tag to a specific release such as 2.9.5 keeps builds reproducible. With no external dependencies the deno cache step is quick, but it keeps the build layer pattern in place for when you add packages.
USER deno
COPY deps.ts .
RUN deno cache deps.ts
ADD . .
RUN deno cache hello.ts
CMD ["run", "--allow-net", "hello.ts"]
In order to deploy our new app to a Convox Rack we need to first create a convox.yml manifest file. In the case of this very simple app we have a very simple manifest.
services:
app:
build: .
port: 8080
We define a service called app which is built from the Dockerfile in the current directory and exposes the internal port of 8080.
If you have a local Rack installed you can test your app by running convox start and then opening a browser and going to https://app.deno.dev.convox/ where you should see your "Hello World" message.
If you have not yet installed a production Rack you can do so easily for the cloud of your choice in just a few minutes using either the Convox web console or the Convox CLI
Once you have a production Rack up and running all you need to do is create an empty app and deploy. For this example I have a Rack running on Digital Ocean called do-test.
$ convox racks
NAME PROVIDER STATUS
do-test do running
First I switch to my Rack and create an empty app
$ convox switch do-test
Switched to do-test
$ convox apps create
Creating deno... OK
My current directory is called deno and Convox automatically assumes your app name matches the current directory name unless you pass the --app argument to app related commands.
Now that I have an empty app created I can deploy my app with convox deploy
$ convox deploy
Packaging source... OK
Uploading source... OK
Starting build... OK
Building: .
Step 1/7 : FROM denoland/deno:2.9.5
...
Step 7/7 : CMD ["run", "--allow-net", "hello.ts"]
Successfully built
Promoting RLCBFEFTRIP... OK
$
Authenticating registry.05456db021737ab6.convox.cloud/deno: Login Succeeded
Building: .
Sending build context to Docker daemon 5.12kB
Step 1/9 : FROM hayd/ubuntu-deno
---> 46fa88c6e582
Step 2/9 : EXPOSE 8080
---> Using cache
---> 8558dbd2cc9a
Step 3/9 : WORKDIR /app
---> Using cache
---> 9b090a2faa85
Step 4/9 : USER deno
---> Using cache
---> e89528aa5d35
Step 5/9 : COPY deps.ts .
---> Using cache
---> 3d81ce45b513
Step 6/9 : RUN deno cache deps.ts
---> Using cache
---> e8d9b94708ad
Step 7/9 : ADD . .
---> Using cache
---> 0d9374c31e7e
Step 8/9 : RUN deno cache hello.ts
---> Using cache
---> 3aad783eeb21
Step 9/9 : CMD ["run", "--allow-net", "hello.ts"]
---> Using cache
---> 4721697b19de
Successfully built 4721697b19de
Successfully tagged adc456a75a24f6940a7c04f27deca0a69aa82ce436aac812f5dc784c:latest
Running: docker tag adc456a75a24f6940a7c04f27deca0a69aa82ce436aac812f5dc784c do-test/deno:app.BMPHETIBKJY
Running: docker tag do-test/deno:app.BMPHETIBKJY registry.05456db021737ab6.convox.cloud/deno:app.BMPHETIBKJY
Running: docker push registry.05456db021737ab6.convox.cloud/deno:app.BMPHETIBKJY
Promoting RLCBFEFTRIP...
2020-05-29T23:12:52Z system/k8s/atom/app Status: Running => Pending
2020-05-29T23:12:53Z system/k8s/app Scaled up replica set app-bd9774756 to 1
2020-05-29T23:12:53Z system/k8s/app-bd9774756 Created pod: app-bd9774756-gpnnj
2020-05-29T23:12:53Z system/k8s/app-bd9774756-gpnnj Successfully assigned do-test-deno/app-bd9774756-gpnnj to do-test-node-3n6m6
2020-05-29T23:12:55Z system/k8s/app-bd9774756-gpnnj Created container main
2020-05-29T23:12:55Z system/k8s/app-bd9774756-gpnnj Started container main
2020-05-29T23:12:55Z system/k8s/app-bd9774756-gpnnj Container image "registry.05456db021737ab6.convox.cloud/deno:app.BMPHETIBKJY" already present on machine
2020-05-29T23:12:56Z system/k8s/atom/app Status: Pending => Updating
2020-05-29T23:13:04Z system/k8s/atom/app Status: Updating => Running
OK
$
The deployment time depends on the cloud your are deploying to and few other factors, such as your upload bandwidth, but typically for an app this small a first time deployment can take 1-5 minutes and subsequent deployments can take less than a minute.
Now that we have deployed we can test our app by grabbing the auto-generated URL with convox services
$ convox services
SERVICE DOMAIN PORTS
app app.deno.05456db021737ab6.convox.cloud 443:8080
And we can test with either curl or our browser
$ curl https://app.deno.05456db021737ab6.convox.cloud
Hello World
And just like that we have a Deno app up and running on Digital Ocean with a valid SSL certificate and we can make updates and redeploy with a single command! You can find the example app for this post in the Convox-Examples Github Repository.
Yes. The example uses the built-in Deno.serve API and the official denoland/deno Docker image, so it runs on any Deno 2.x release. The current release is Deno 2.9.5, published August 6, 2026.
No. Deno 2 includes Deno.serve as a built-in API, so a web server needs no imports at all. You pass an options object for port and hostname; the defaults are port 8000 on hostname 0.0.0.0.
Yes. Deno publishes denoland/deno on Docker Hub in Debian (default), Ubuntu, Alpine, and distroless variants. It replaces the community hayd/deno-docker image the original post used.
Deno 2 shipped in October 2024 with Node.js and npm compatibility, native package.json support, the deno add and deno install commands, and a stabilized standard library on JSR. The old pattern of pinning deno.land/std URLs in a deps.ts file is obsolete, and Deno.serve replaced the standard library HTTP server loop.