Running Deno in Docker

Mayank Choubey
Tech Tonic
Published in
13 min readJan 2, 2022

--

This article is about running Deno runtime inside a container. This article is divided in three parts:

  • Introduction: Why Deno needs to run in container
  • Running Deno in Docker: Running Deno in Docker containers through REPL, shell, and Dockerfile
  • Running Deno in K8s cluster: Running Deno container images through K8s container orchestration

Introduction

Deno is a secure & strictly sandboxed runtime that doesn’t have default access to anything the owning user has. All the access like network, file system, child process can be controlled with granularity.

Docker is an open source containerization platform. It enables developers to package applications into containers — standardized executable components combining application source code with the operating system (OS) libraries and dependencies required to run that code in any environment. Containers have become increasingly popular as organizations shift to cloud-native development and hybrid multi-cloud environments.

Why would there be a need to run an already sandboxed runtime like Deno in a containerization platform like Docker? There could be many reasons to do it. We’ll see a couple of important reasons.

OS incompatibility

Deno can’t run on some older operating systems like CentOS 7. The reason being that CentOS 7’s default GLIBC version could be lesser than the minimum needed by Deno. Let’s try it out on a CentOS 7:

$ cat /etc/os-release
NAME="CentOS Linux"
VERSION="7 (Core)"
ID="centos"
ID_LIKE="rhel fedora"
VERSION_ID="7"
$ curl -fsSL https://deno.land/x/install/install.sh | sh
############################################################# 100.0%
Archive: /Users/mayankc/.deno/bin/deno.zip
inflating: /Users/mayankc/.deno/bin/deno
Deno was installed successfully to /Users/mayankc/.deno/bin/deno
Run 'deno --help' to get started
$ ls .deno/bin/
deno
$ .deno/bin/deno
.deno/bin/deno: /lib64/libc.so.6: version `GLIBC_2.18' not found (required by .deno/bin/deno)

The Deno runtime fails to start on CentOS 7 as it needs at least GLIBC v2.18, but on CentOS 7 it finds v2.17:

$ ldd --version
ldd (GNU libc) 2.17

Upgrading GLIBC is a big headache and is not recommended for reliability of OS services and other applications. This could become a blocker.

Docker containers can run Deno runtime on CentOS 7 inside a compatible container. We’ll see it in detail a bit later.

Container orchestration

The most popular container orchestration platform is Kubernetes. Kubernetes is a portable, extensible, open-source platform for managing containerized workloads and services, that facilitates both declarative configuration and automation. It has a large, rapidly growing ecosystem.

Kubernetes has a lot of advantages, especially when managing large scale services. Some of them are:

  • Autoscaling
  • Service discovery and load balancing
  • Storage orchestration
  • Automated rollouts and rollbacks
  • Self-healing
  • Secret and configuration management

Organizations running large to very large scale services might have already invested a lot in building their infrastructure using Kubernetes’s container orchestration. If all the other services (could be hundreds/thousands) are deployed using container orchestration, it won’t be possible to run a Deno service independently.

It’s easy to conclude that, even if Deno is strictly sandboxed, it might be required to run it in inside a container. Failing to do so might lead to replacement of Deno with other technology.

Now that we’ve gone through a short motivation, let’s see how to run Deno inside Docker containers.

Pre-requisites

Image

The Deno docker repo contains a number of pre-built images officially supported by Deno:

  • Alpine
  • Centos
  • Debian
  • Ubuntu
  • Distroless
  • Only binary

All the supported images are also listed on docker hub.

Alpine is one of the smallest image and is generally good enough to run Deno executable. Another easier way is to use Deno binary directly. We’ll not use any distro for examples used in this article. We’ll only use an image containing only the Deno binary.

Note: A Deno binary image contains just the Deno binary

Installing docker

The required docker packages & services need to be installed before working with Deno. It is possible that docker’s services might come preinstalled on an organization’s instance running containers. If not, here are the instructions to install & enable required services.

First install the pre-requisites:

$ sudo yum install -y yum-utils
....
Installed:
yum-utils.noarch 0:1.1.31-54.el7_8
Complete!$ sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
Loaded plugins: fastestmirror
adding repo from: https://download.docker.com/linux/centos/docker-ce.repo
grabbing file https://download.docker.com/linux/centos/docker-ce.repo to /etc/yum.repos.d/docker-ce.repo
repo saved to /etc/yum.repos.d/docker-ce.repo

Next, install the main Docker engine, CLI, daemon, etc:

$ sudo yum install docker-ce docker-ce-cli containerd.io
....
Installed:
containerd.io.x86_64 0:1.4.12-3.1.el7 docker-ce.x86_64 3:20.10.12-3.el7 docker-ce-cli.x86_64 1:20.10.12-3.el7
Dependency Installed:
docker-ce-rootless-extras.x86_64 0:20.10.12-3.el7 docker-scan-plugin.x86_64 0:0.12.0-3.el7
Complete!

Finally, the Docker service needs to be started:

$ sudo systemctl start docker
$ sudo systemctl status docker
● docker.service - Docker Application Container Engine
Loaded: loaded (/usr/lib/systemd/system/docker.service; disabled; vendor preset: disabled)
Active: active (running) since Fri 2021-12-31 02:51:01 UTC; 17s ago
Docs: https://docs.docker.com
....

To verify the Docker installation, the Docker’s default hello world container can be executed:

$ sudo docker run hello-worldHello from Docker!
This message shows that your installation appears to be working correctly.

The Docker is installed and working fine. Let’s move on to running Deno inside a Docker container.

Deno in Docker

In real world applications, a Dockerfile will be prepared that contains details about the image, exposed ports, application code, dependency installation, and starting the application. As we’ve just installed Docker, we can start playing with it using command-line by pulling an image directly from Docker hub.

We’ll see three ways of using Deno in Docker:

  • Directly run REPL
  • Login to container shell & use deno command
  • Write Dockerfile to run an application (example content server)

Directly running REPL

The simplest way to run Deno in Docker is by pulling an image directly and starting a REPL shell. As mentioned earlier, we’ll be using a binary only image for all the examples.

In the first run, Docker will download the requested image from Docker hub. For subsequent runs, it’ll directly start a REPL. In other words, the first run would take time, while the subsequent runs would be faster.

The basic command to run REPL looks like this:

docker run -it denoland/deno:1.17.1 repl

There are three command-line arguments to the docker run command:

  • it: Creates an attached container
  • denoland/deno:1.17.1: The image to use for container
  • repl: The command to execute after starting the container

Here is a sample run of REPL:

$ sudo docker run -it denoland/deno:1.17.1 repl
Deno 1.17.1
exit using ctrl+d or close()
> const rsp=await fetch("https://deno.land/x");
undefined
> rsp.status;
200
> rsp.statusText
"OK"
> rsp.headers
Headers {
"cache-control": "public,max-age=0,must-revalidate",
"content-length": "42058",
"content-type": "text/html; charset=utf-8",
date: "Fri, 31 Dec 2021 03:02:28 GMT",
etag: "0de231dff66c850499da8d63791e00ee9f38123d",
server: "deploy/us-west1-a"
}
>

This is still CentOS 7, but we’re now able to run Deno inside a container! The familiar REPL prompt comes. We can run any Deno code in the REPL.

Run from shell

The next way to run Deno in Docker is by logging into the container shell and running deno commands there. A simple shell prompt will be presented to us. We can run Deno normally.

The command-line arguments are the same as before except that the REPL command is replaced with sh command.

$ docker run -it denoland/deno:1.17.1 sh
# deno -V
deno 1.17.1
# deno run --allow-net https://deno.land/std@0.119.0/examples/welcome.ts
Download https://deno.land/std@0.119.0/examples/welcome.ts
Check https://deno.land/std@0.119.0/examples/welcome.ts
Welcome to Deno!
# deno install https://deno.land/std@0.119.0/examples/welcome.ts
✅ Successfully installed welcome
/usr/local/bin/welcome
# /usr/local/bin/welcome
Welcome to Deno!
#

The shell experience is the same as running Deno directly. We’ve tried running a simple Deno program. We’ve also tried to install & run an application.

Using Dockerfile

The first two ways are great for playing with Docker, Deno, containers, etc. However, in real world applications, there will be a Dockerfile containing a list of instructions that would be used to build and run the container. In this subsection, we’ll prepare, build, and run a Deno application inside a Docker container using a Dockerfile.

Application

We need a test application to run in the container. We’ll be using the following content server application cloned into /var/tmp:

$ git clone https://github.com/mayankchoubey/deno-content-server.git
Cloning into 'deno-content-server'...
remote: Enumerating objects: 120, done.
remote: Counting objects: 100% (120/120), done.
remote: Compressing objects: 100% (99/99), done.
remote: Total 120 (delta 55), reused 53 (delta 17), pack-reused 0
Receiving objects: 100% (120/120), 88.36 KiB | 0 bytes/s, done.
Resolving deltas: 100% (55/55), done.
$ ls /var/tmp/deno-content-server/
authService.ts cfg.json consts.ts controller.ts deploy deps.ts fileService.ts logger.ts main.ts README.md router.ts test utils.ts

There is a deps.ts file containing all the dependencies. To know more dependency management in Deno, a relevant article can be visited here.

Writing Dockerfile

Here is the simplest Dockerfile for the content server application:

FROM denoland/deno:1.17.1
EXPOSE 8080
WORKDIR /app
USER deno
COPY deps.ts .
RUN deno cache deps.ts
COPY . .
RUN deno cache main.ts
RUN mkdir -p /var/tmp/log
CMD ["run", "--allow-all", "main.ts"]

This Dockerfile is used to build a container application image (using docker build) that will get executed later (using docker run).

Let’s go through each line:

  • FROM denoland/deno:1.17.1: The base image to use for the container
  • EXPOSE 8080: The port to expose from the container (aka application listening port)
  • WORKDIR /app: The working directory inside the container
  • USER deno: The user account used to run the services in container
  • COPY deps.ts .: First deps.ts is copied into the container
  • RUN deno cache deps.ts: The cache command downloads & compiles all the dependencies recursively
  • COPY . .: Copy the application source into the container
  • RUN deno cache main.ts: The cache command downloads & compiles the application
  • RUN mkdir -p /var/tmp/log: Creates a directory in the container where logs will be written by the application
  • CMD [“run”, “ — allow-all”, “main.ts”]: Starts the application inside the conatiner

Note that we’ve used — allow-all instead of selective sandboxing. As Deno is running inside a container, it’s already sandboxed. There is no need to add another layer of sandboxing.

Build a container image

Once the Dockerfile is ready, the next step is to use docker build command to create a container image that can be deployed using docker run. Each step specified in the Dockerfile is executed. This ensures that errors can be caught at the time to building rather than deploying.

$ sudo docker build --no-cache -t app .
Sending build context to Docker daemon 331.8kB
Step 1/10 : FROM denoland/deno:1.17.1
---> 0dd28b85d386
Step 2/10 : EXPOSE 8080
---> Running in 92fdc2c76466
Removing intermediate container 92fdc2c76466
---> e950e16ccb8f
Step 3/10 : WORKDIR /app
---> Running in 42ef8ed53199
Removing intermediate container 42ef8ed53199
---> 7948be527821
Step 4/10 : USER deno
---> Running in 3c6a57a9ea39
Removing intermediate container 3c6a57a9ea39
---> 6de3d77cceae
Step 5/10 : COPY deps.ts .
---> af91952f3f27
Step 6/10 : RUN deno cache deps.ts
---> Running in 8631c7d179c9
Download https://deno.land/std@0.119.0/http/http_status.ts
Download https://deno.land/std@0.119.0/http/mod.ts
Download https://deno.land/std@0.119.0/log/mod.ts
Download https://deno.land/std@0.119.0/path/mod.ts
Download https://deno.land/std@0.119.0/streams/mod.ts
Download https://deno.land/std@0.119.0/_util/os.ts
Download https://deno.land/std@0.119.0/path/_interface.ts
Download https://deno.land/std@0.119.0/path/common.ts
Download https://deno.land/std@0.119.0/path/glob.ts
Download https://deno.land/std@0.119.0/path/posix.ts
Download https://deno.land/std@0.119.0/path/separator.ts
Download https://deno.land/std@0.119.0/path/win32.ts
Download https://deno.land/std@0.119.0/_util/assert.ts
Download https://deno.land/std@0.119.0/log/handlers.ts
Download https://deno.land/std@0.119.0/log/levels.ts
Download https://deno.land/std@0.119.0/log/logger.ts
Download https://deno.land/std@0.119.0/http/cookie.ts
Download https://deno.land/std@0.119.0/http/server.ts
Download https://deno.land/std@0.119.0/streams/conversion.ts
Download https://deno.land/std@0.119.0/streams/delimiter.ts
Download https://deno.land/std@0.119.0/streams/merge.ts
Download https://deno.land/std@0.119.0/fmt/colors.ts
Download https://deno.land/std@0.119.0/fs/exists.ts
Download https://deno.land/std@0.119.0/io/buffer.ts
Download https://deno.land/std@0.119.0/path/_constants.ts
Download https://deno.land/std@0.119.0/path/_util.ts
Download https://deno.land/std@0.119.0/datetime/mod.ts
Download https://deno.land/std@0.119.0/async/deferred.ts
Download https://deno.land/std@0.119.0/bytes/bytes_list.ts
Download https://deno.land/std@0.119.0/bytes/mod.ts
Download https://deno.land/std@0.119.0/io/types.d.ts
Download https://deno.land/std@0.119.0/datetime/formatter.ts
Download https://deno.land/std@0.119.0/async/mod.ts
Download https://deno.land/std@0.119.0/bytes/equals.ts
Download https://deno.land/std@0.119.0/datetime/tokenizer.ts
Download https://deno.land/std@0.119.0/async/deadline.ts
Download https://deno.land/std@0.119.0/async/debounce.ts
Download https://deno.land/std@0.119.0/async/delay.ts
Download https://deno.land/std@0.119.0/async/mux_async_iterator.ts
Download https://deno.land/std@0.119.0/async/pool.ts
Download https://deno.land/std@0.119.0/async/tee.ts
Check file:///app/deps.ts
Removing intermediate container 8631c7d179c9
---> 7eb04e3ff288
Step 7/10 : COPY . .
---> 3f075ac6208e
Step 8/10 : RUN deno cache main.ts
---> Running in db2758a06aca
Check file:///app/main.ts
Removing intermediate container db2758a06aca
---> d0d4b05a7e38
Step 9/10 : RUN mkdir -p /var/tmp/log
---> Running in 681a5a2609e4
Removing intermediate container 681a5a2609e4
---> b4a7c50514b7
Step 10/10 : CMD ["run", "--allow-all", "main.ts"]
---> Running in 9c59c5fec027
Removing intermediate container 9c59c5fec027
---> 1d80d301b71d
Successfully built 1d80d301b71d
Successfully tagged app:latest

The available images can be listed using docker images command:

$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 1d80d301b71d 2 minutes ago 171MB
$

Deploying

Once an image is ready, it can be deployed using docker run command. A local port on the instance (say 8080) can be mapped to a port offered by the container (8080). This helps to call a service running inside the container. The container can be started in attached mode (using -it) or in detached mode (-d). The detached mode is useful to run in background.

$ sudo docker run -d -p 8080:8080 app
a7de48e9d3ab0394bc1d7eb3dd1df513beca35d740d018b1779697f033b5b30c
$

The container has been started. We can use docker container ls command to list the containers that are running:

$ sudo docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a7de48e9d3ab app "/tini -- docker-ent…" About a minute ago Up About a minute 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp quirky_benz

Testing

As local port 8080 is mapped onto container port 8080, we can simply call the service by running a curl command on the instance hosting the container.

$ curl http://localhost:8080/textFile.txt -v -H 'Authorization: Bearer cba633d4-59f3-42a5-af00-b7430c3a65d8'
* About to connect() to localhost port 8080 (#0)
* Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /textFile.txt HTTP/1.1
> User-Agent: curl/7.29.0
> Host: localhost:8080
> Accept: */*
> Authorization: Bearer cba633d4-59f3-42a5-af00-b7430c3a65d8
>
< HTTP/1.1 200 OK
< content-length: 22
< content-type: text/plain
< x-tracking-id: 6ab9fbcb-8ad4-4041-8ede-48fd8142c1c5
< date: Fri, 31 Dec 2021 19:47:50 GMT
<
Learning Deno Is Fun!

That’s all about running Deno in Docker containers. Let’s move to the last section where we’ll see how to run Deno container images orchestrated using K8s (aka Kubernetes).

Orchestrated Deno containers

Kubernetes is a portable, extensible, open-source platform for managing containerized workloads and services, that facilitates both declarative configuration and automation. It has a large, rapidly growing ecosystem. Kubernetes services, support, and tools are widely available.

The name Kubernetes originates from Greek, meaning helmsman or pilot. K8s as an abbreviation results from counting the eight letters between the “K” and the “s”. Google open-sourced the Kubernetes project in 2014. Kubernetes combines over 15 years of Google’s experience running production workloads at scale with best-of-breed ideas and practices from the community

Some benefits of K8s are:

  • Service discovery and load balancing
  • Storage orchestration
  • Automated rollouts and rollbacks
  • Automatic bin packing
  • Self-healing
  • Secret and configuration management

In the last section, we containerized Deno and ran it manually. In this section, we’ll run containerized Deno through K8s orchestration. A behemoth like K8s can’t be covered in a small section. Instead of going over basics, we’ll directly jump to building a K8s cluster.

Pre-requisites

To keep it simple, we’ll run a small cluster of 3 pods using minikube. Minikube is local Kubernetes, focusing on making it easy to learn and develop for Kubernetes. All that’s needed is Docker (or similarly compatible) container or a Virtual Machine environment, and Kubernetes is a single command away: minikube start.

Minikube can be easily installed using the following two commands:

$ curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-arm64
$ sudo install minikube-darwin-amd64 /usr/local/bin/minikube

For local development we’ll be using locally built (unpublished) images. This can be achieved by configuring minikube to run in development mode:

Local development applies only to the current shell

$ eval $(minikube docker-env)

Building & loading container image

Once minikube is configured, the next steps are:

  • build a container image
  • load it into minikube

The build procedure is the same (the local image is going to be called deno-app):

$ docker build --no-cache -t deno-app .
[+] Building 110.2s (12/12) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 37B
=> [internal] load .dockerignore
=> => transferring context: 2B
=> [internal] load metadata for docker.io/denoland/deno:1.17.1
=> [1/7] FROM docker.io/denoland/deno:1.17.1@sha256:9a4e5579c3c06421696dc28077fb9ad26873ac2
.....
=> [2/7] WORKDIR /app
=> [3/7] COPY deps.ts .
=> [4/7] RUN deno cache deps.ts
=> [5/7] COPY . .
=> [6/7] RUN deno cache main.ts
=> [7/7] RUN mkdir -p /var/tmp/log
=> exporting to image
=> => exporting layers
=> => writing image sha256:505b477a0a430535b8e8ae8baed83eaf437acd450982757a32e5719e82be2366
=> => naming to docker.io/library/deno-app

The container image is a local develoment image and will not be pushed to public hub. To make sure that K8s gets the local image, we need to load this image into minikube.

$ minikube image load deno-app
$

The image has been loaded successfully. Let’s move on to creating a simple K8s deployment.

Deployment

A K8s deployment specifies the pod, image, replicas, container port, etc. The deployment is usually specified using a yaml file. The following is the deployment file for a simple cluster with 3 replicas:

//deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: deno-app
spec:
selector:
matchLabels:
run: deno-app
replicas: 3
template:
metadata:
labels:
run: deno-app
spec:
containers:
- name: deno-app
image: deno-app:latest
imagePullPolicy: Never
ports:
- containerPort: 8080

The image pull policy has been set to Never so that K8s doesn’t pull it from public sources.

This deployment file needs to be applied (using kubectl apply) to create a cluster.

$ kubectl apply -f ./deployment.yaml
deployment.apps/deno-app created

As soon as the deployment has been applied, K8s will spawn pods and brings them into service.

$ kubectl get pods
NAME READY STATUS RESTARTS AGE
deno-app-6c5f7c45f-52kcd 1/1 Running 0 69s
deno-app-6c5f7c45f-kf75j 1/1 Running 0 69s
deno-app-6c5f7c45f-nx4b8 1/1 Running 0 69s
$ kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
deno-app 3/3 3 3 2m52s

All the three pods have been spawned & image has been loaded. The pods are available for service.

Port forwarding

The pods are up and running. The final step is to test them out. As we’re building a local cluster, we’ll use K8s port forward feature to do a round of testing. Using port foward we’ll forward local 8080 port to 8080 port of a deployment.

Kubectl port-forward allows access and interaction with internal Kubernetes cluster processes from localhost. This method can be used to investigate issues and adjust services locally without the need to expose them beforehand.

The port-forward command never returns

$ kubectl port-forward deployment/deno-app 8080:8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080

The forwarding service is up. We can use curl command to test it out:

$ curl http://localhost:8080/textFile.txt -H 'Authorization: Bearer cba633d4-59f3-42a5-af00-b7430c3a65d8' -v
* Trying ::1:8080...
* Connected to localhost (::1) port 8080 (#0)
> GET /textFile.txt HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.77.0
> Accept: */*
> Authorization: Bearer cba633d4-59f3-42a5-af00-b7430c3a65d8
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< content-length: 22
< content-type: text/plain
< x-tracking-id: 7ff46eca-ab97-46a6-9c83-e6cb0c3c8d06
< date: Sun, 02 Jan 2022 02:29:49 GMT
<
Learning Deno Is Fun!

For every connection getting forwarded, kubectl prints a log:

$ kubectl port-forward deployment/deno-app 8080:8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Handling connection for 8080

That’s all about running Deno in containers.

--

--