Continuous Integration with Deno and Docker
Tags: javascript, deno, docker, tutorial
Published: 2020-07-15 17:20:45

*Deno has been getting a lot of attention. There are already more than 700 third-party modules and the number is quickly rising. People are starting to use it for real business applications. We even have a [blockchain-based repository](https://nest.land/), something that blew my mind. But I haven't seen a lot of tutorials covering CI/CD and Docker with Deno, so I wrote one. I hope you find it handy, so be sure to bookmark it* 🔖

---

Node has gained a lot of popularity since it was introduced in 2009. Despite its success, Ryan Dahl, Node’s original creator, believes there’s room for improvement, and so he has recently released [Deno](https://deno.land/), a new runtime for JavaScript and TypeScript, as its successor.

How is Deno different? Well, Deno, like Node, uses the V8 engine and event-driven architecture. But here is where the similarities end.

- TypeScript gets first-class support at last. Deno compiles to JavaScript without additional packages.
- Deno ships as a single executable with built-in, Go-inspired [test runners](https://deno.land/manual/testing) and [dependency management](https://deno.land/manual/linking_to_external_code).
- Deno has better security by default. Programs run in a sandbox that doesn’t have access to the network, the environment, or the filesystem unless explicitly granted.

The most significant difference, though, is that Deno doesn’t have a package manager (say goodbye to npm). That means Node.js modules are largely unsupported. Instead, Deno uses decentralized ES Modules. To compensate, Deno developers have introduced an audited [standard library](https://deno.land/std) and support for [third-party modules](https://deno.land/x).

In this tutorial, we’ll learn how to use [Semaphore Continuous Integration](https://semaphoreci.com/continuous-integration) (CI) to test Deno applications. As a bonus, we’ll explore how to release Deno applications in Docker using [Continuous Delivery](https://semaphoreci.com/blog/cicd) (CD).

## Prerequisites

If you wish to do this tutorial along with me, you’ll need the following:

- [Deno](https://deno.land/) on your Mac, Linux, or Windows machine.
- A [Docker Hub](https://hub.docker.com/) account.
- A Semaphore account. You can get one for free at [semaphoreci.com](https://semaphoreci.com/).

To get started quickly, you can use our starter [demo project](https://github.com/TomFern/addressbook-deno).

{% github https://github.com/TomFern/addressbook-deno %}

Feel free to fork it. It features an [oak-based](https://deno.land/x/oak) HTTP API service with a PostgreSQL backend and includes integration tests. The project comes with everything you need to build Docker images.

Otherwise, these instructions should work with any Deno application. You may need to make slight adjustments, though, depending on how your code is organized.

## Prepare a Testing Image

Semaphore’s [composable containers](https://docs.semaphoreci.com/ci-cd-environment/choosing-between-a-vm-and-docker-based-environment/#docker-based-environments) feature lets us work with cutting-edge tools and languages like Deno. We can tailor Docker images to exact specifications and use them to drive CI/CD jobs seamlessly. Semaphore supports any container as long as it includes some basic packages like SSH, Git, and FTP.

Let’s take a few minutes to prepare a `Dockerfile` for Deno.

We can start from a Debian image:

```docker
FROM debian:buster
```

Then, we tweak some settings and install the required packages:

```docker
RUN echo 'APT::Get::Assume-Yes "true";' > /etc/apt/apt.conf.d/99semaphore
RUN echo 'DPkg::Options "--force-confnew";' >> /etc/apt/apt.conf.d/99semaphore
ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update \
        && apt-get install -y --no-install-recommends \
               ca-certificates sudo locales netbase netcat \
               procps lftp curl unzip git openssh-client \
        && rm -rf /var/cache/apt/archives
```

Next, we set up the locale, so logs have the correct dates and format:

```docker
RUN ln -sf /usr/share/zoneinfo/Etc/UTC /etc/localtime
RUN locale-gen C.UTF-8 || true
ENV LANG=C.UTF-8
```

And finally, install Deno. We’ll the most current version at the time of writing this, v1.1.1:

```docker
RUN curl -fsSL https://deno.land/x/install/install.sh | sh -s v1.1.1
RUN cp /root/.deno/bin/deno /usr/local/bin && rm -rf /root/.deno
```

The final result, which we’ll call `Dockerfile.ci` should look like this:

```docker
# Dockerfile.ci

FROM debian:buster

RUN echo 'APT::Get::Assume-Yes "true";' > /etc/apt/apt.conf.d/99semaphore
RUN echo 'DPkg::Options "--force-confnew";' >> /etc/apt/apt.conf.d/99semaphore
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
        && apt-get install -y --no-install-recommends \
               ca-certificates sudo locales netbase netcat \
               procps lftp curl unzip git openssh-client \
        && rm -rf /var/cache/apt/archives

RUN ln -sf /usr/share/zoneinfo/Etc/UTC /etc/localtime
RUN locale-gen C.UTF-8 || true
ENV LANG=C.UTF-8

RUN curl -fsSL https://deno.land/x/install/install.sh | sh -s v1.1.1
RUN cp /root/.deno/bin/deno /usr/local/bin && rm -rf /root/.deno

CMD ["/bin/sh"]
```

Now all we have to do is build the image:

```docker
$ docker build -t YOUR_DOCKER_HUB_USER/deno:v1.1.1 -f Dockerfile.ci .
```

Upload it to Docker Hub:

```docker
$ docker login -u YOUR_DOCKER_HUB_USER
$ docker push YOUR_DOCKER_HUB_USER/deno:v1.1.1
```

And we are set to go.

## Add Your Project to Semaphore

To add your project to Semaphore follow these steps:

- Log in with your account and click on the **\+ (plus sign)** next to projects.

![Add Project](https://dev-to-uploads.s3.amazonaws.com/i/igyt7dcjzslgl5s1y1fy.png)

- Select your repository from the list.

![Choose Repo](https://dev-to-uploads.s3.amazonaws.com/i/vlvzich66vu4hglr62d2.png)

- In the next screen, you can add more people to the project. Once done, click on **Go to Workflow Builder** to continue.

![Invite People](https://dev-to-uploads.s3.amazonaws.com/i/2utt27gbxr5vag0ttejd.png)

- Choose the Single Job starter workflow and click on **Customize it first**.

![Choose Starter](https://dev-to-uploads.s3.amazonaws.com/i/e7z88ew76li429jse500.png)

You are now at the Workflow Builder, which lets you visually set up the CI/CD pipeline.

1. The main element in the view is the [pipeline](https://semaphoreci.com/blog/cicd-pipeline). A pipeline consists of a series of _blocks_ which are executed from left to right.
2. Each block has one or more _jobs_. Once all jobs in a block complete, the next block starts.
3. Jobs contain the commands that do the work and run in parallel. If any command fails, the pipeline stops and is marked as failed.

![Pipeline Overview](https://dev-to-uploads.s3.amazonaws.com/i/nb0lb0x8jtafkhshui52.png)
Let’s create our first job:

- Click the pipeline to view its settings. On the right side, under **Environment Type** select **Docker containers**.

![Using Containers](https://dev-to-uploads.s3.amazonaws.com/i/wa3yujzm8x83xb15yq08.png)

- Type the name of the image you uploaded in the previous section: `YOUR_DOCKER_HUB_USER/deno:v1.1.1`.

![Set Container Image](https://dev-to-uploads.s3.amazonaws.com/i/ja9qtkqx91yfjfb3mzuk.png)

- Click on the first block in the pipeline to begin editing it.

![Block 1](https://dev-to-uploads.s3.amazonaws.com/i/ppcit9sa5gxlrz1b8nl4.png)

In this block, we only need to download and cache the project’s dependencies. For this, we can combine Semaphore’s [cache](https://docs.semaphoreci.com/reference/toolbox-reference/#cache) tool with `deno cache`:

1. `cache restore` takes a list of keys and retrieves the first match. Our project lists all dependencies in `src/deps.ts`, so we can use it as part of the key:

```bash
cache restore deps-$(checksum src/deps.ts),deps-master
```

2. `deno cache` downloads dependencies without executing any code. To download them in the current directory:

```bash
export DENO_DIR=$PWD/deps
deno cache src/deps.ts
```

3. `cache store` takes a list of keys and a directory and saves it for future runs:

```bash
cache store deps-$(checksum src/deps.ts),deps-master deps
```

Put together, the commands for the job are:

```bash
checkout
export DENO_DIR=$PWD/deps
cache restore deps-$(checksum src/deps.ts),deps-master
deno cache src/deps.ts
cache store deps-$(checksum src/deps.ts),deps-master deps
```

![Download Dependencies](https://dev-to-uploads.s3.amazonaws.com/i/cqt1p5ndsrmz70jra991.png)

## Testing with Continuous Integration

In this section, we’ll create a new block with two test jobs. The tests use a PostgreSQL database. The easiest way to get one is to connect a new container since we’re already using them in the pipeline.

- Select the pipeline and click on **+Add Container**

![Add Database Container](https://dev-to-uploads.s3.amazonaws.com/i/n4vwgybc37zg2c0x1asu.png)

- Call the new container “postgres”
- Type the name of a postgres image on **Image**: `postgres:12`
- Click on **+Add environment variable** and add the `POSTGRES_HOST_AUTH_METHOD` variable with value `trust` to allow connections without a password.

![Set Postgres Image](https://dev-to-uploads.s3.amazonaws.com/i/c2vs6ac96nc791a8oasd.png)

- Create a new block using **+Add Block**.

![Add Block](https://dev-to-uploads.s3.amazonaws.com/i/8kw7e1sn0g6ovqgdutn2.png)

- Open the **Prologue** section. The commands we put here will be executed before every job in the block. We’ll use these commands to retrieve the dependencies:

```bash
checkout
export DENO_DIR=$PWD/deps
cache restore deps-$(checksum src/deps.ts),deps-maste
```

![Set Prologue](https://dev-to-uploads.s3.amazonaws.com/i/0wj6a587y4ifepsjll5a.png)

- Open the **Environment Variables** section and create the variable `DB_HOST` = `postgres`

![Set Environment Variables](https://dev-to-uploads.s3.amazonaws.com/i/z9e6q3iq42isnc3m40xc.png)

Create three test jobs:

- The first job does **Database tests**. Type the following commands:

```bash
deno run --allow-net --allow-env src/migrate.js
deno test --allow-net --allow-env src/tests/database.test.js
```

- Click on **+Add another job**.
- The second job does **Integration tests**. Type the following commands. Note that in this one, we also need to start the application before running the tests.

```bash
deno run --allow-net --allow-env src/app.js &
deno run --allow-net --allow-env src/migrate.js
deno test --allow-net --allow-env src/tests/app.test.js
```

- The third job does **Static tests**. Deno ships with a code linter. To enable it, we need to use the `--unstable` flag.

```bash
deno lint --unstable src/*.js src/*.ts src/tests/*.js
```

![Test Block](https://dev-to-uploads.s3.amazonaws.com/i/u24ikm0jxmg8ri3u1yja.png)

You can try the pipeline with **Run the Workflow** > **Start**.

![Run Workflow](https://dev-to-uploads.s3.amazonaws.com/i/qi4ph0ynle3vviwyw88i.png)

![Tests Done](https://dev-to-uploads.s3.amazonaws.com/i/16pat13ufsdv13uyh9l4.png)

## Run Deno with Docker

Docker lets us create portable images that can run anywhere. In this section, we’ll learn how to prepare a production image Deno.

Before Semaphore can push to your Docker registry, you must create a [secret](https://docs.semaphoreci.com/essentials/using-secrets/) with the login details.

To do it:

- Click on **Secrets** under Configuration on the left menu.

![Go to Secrets](https://dev-to-uploads.s3.amazonaws.com/i/1u5jbz8yljzdy4w3wqp4.png)


- Click **Create New Secret**.
- Create two variables for your Docker Hub username and password:
    - `DOCKER_USERNAME` = `YOUR DOCKER USERNAME`
    - `DOCKER_PASSWORD` = `YOU DOCKER PASSWORD`
- Click on **Save changes**

![Docker Hub Secret](https://dev-to-uploads.s3.amazonaws.com/i/ula4c8a12k44bar5x2po.png)


Open the Workflow Builder again and scroll right to the end of the pipeline. 

![Edit Workflow](https://dev-to-uploads.s3.amazonaws.com/i/d6hgrty1teemnzblm4qs.png)

We’ll add a second pipeline using a [promotion](https://docs.semaphoreci.com/guided-tour/deploying-with-promotions/). Promotions lets us tie multiple pipelines together with user-defined conditions.

- Click on **\+ Add First Promotion**.

![Add Promotion](https://dev-to-uploads.s3.amazonaws.com/i/1dpgc9ds8i2ebakpggav.png)

- Check the **Enable automatic promotion** option. You can set start conditions here.

![Promotion Conditions](https://dev-to-uploads.s3.amazonaws.com/i/ai64g6w3dcbiksyrxq5a.png)

- Click on the first block on the new pipeline.
- Rename the job to “Docker Build”.
- On the right side of the screen, scroll down to the **Secrets** section and enable the dockerhub secret.

![Import Secret on Block 1](https://dev-to-uploads.s3.amazonaws.com/i/po1bc8yoplb99nmfyo07.png)

The Docker build job consists of four commands:

- Log in to the Docker registry.
```bash
echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
```

- Pull the latest image available.

```bash
docker pull "${DOCKER_USERNAME}"/addressbook-deno:latest || true
```

- Build the new version of the image, reusing layers from previous versions when possible.

```bash
docker build -t "${DOCKER_USERNAME}"/addressbook-deno:latest --cache-from "${DOCKER_USERNAME}"/addressbook-deno:latest .
```

- Push the new image version.

```bash
docker push "${DOCKER_USERNAME}"/addressbook-deno:latest
```

We’ll use this `Dockerfile` to build the production image:

```docker
FROM debian:buster
RUN apt-get update \
        && apt-get install -y --no-install-recommends ca-certificates curl unzip netcat \
        && rm -rf /var/cache/apt/archives
RUN groupadd --gid 1000 deno \
  && useradd --uid 1000 --gid deno --shell /bin/bash --create-home deno
USER deno
RUN curl -fsSL -k https://deno.land/x/install/install.sh | sh -s v1.1.1
ENV HOME "/home/deno"
ENV DENO_INSTALL "${HOME}/.deno"
ENV PATH "${DENO_INSTALL}/bin:${PATH}"
RUN mkdir -p $HOME/app/src
COPY --chown=deno:deno src/ $HOME/app/src
WORKDIR $HOME/app/src
EXPOSE 4000
RUN deno cache deps.ts
CMD deno run --allow-env --allow-net app.js
```

Compared with the image we used in the CI pipeline, this production image is leaner, has all the code and dependencies baked in, and runs as the deno user instead of root.

The final Docker build job should look like this:

```bash
checkout
echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
docker pull "${DOCKER_USERNAME}"/addressbook-deno:latest || true
docker build -t "${DOCKER_USERNAME}"/addressbook-deno:latest --cache-from "${DOCKER_USERNAME}"/addressbook-deno:latest .
docker push "${DOCKER_USERNAME}"/addressbook-deno:latest
```

![Docker Build Job](https://dev-to-uploads.s3.amazonaws.com/i/d48rl1mutvtkwk251b4q.png)

We’re done configuring the pipeline. Start it one last time.

Once it completes, click on **promote** to start the build.

![Click Promote](https://dev-to-uploads.s3.amazonaws.com/i/m7jl16t4d322p20ra4kf.png)

That’s all! From now on, Semaphore will run the pipelines on every push to GitHub.

![All Done](https://dev-to-uploads.s3.amazonaws.com/i/xktutm15p5rj27ky74ap.png)

## What’s Next?

You have prepared a production image with your application, now it’s time to think about deploying it 🚀 — don’t leave it sitting on Docker Hub, collecting dust.

Do you want to run your Deno application in Kubernetes but don’t know how to begin? Download our free [CI/CD with Docker and Kubernetes](https://semaphoreci.com/resources/cicd-docker-kubernetes) ebook, which explains everything you need to know to get started and includes detailed how-to tutorials.

After that, check out my Docker & Kubernetes guides:

{% link https://dev.to/semaphore/docker-and-java-spring-boot-part-1-continuous-integration-1mfl %}

{% link https://dev.to/semaphore/google-cloud-kubernetes-in-10-minutes-2enb %}

{% link https://dev.to/semaphore/continuous-integration-and-delivery-to-aws-kubernetes-12d6 %}

{% link https://dev.to/semaphore/speeding-up-docker-builds-x7-benchmark-1en4 %}

Interested in JavaScript or TypeScript? Check out these posts in the Semaphore blog:

- [Dockerizing a Node.js Application](https://semaphoreci.com/community/tutorials/dockerizing-a-node-js-web-application)
- [JavaScript and TypeScript Continuous Integration Guide](https://docs.semaphoreci.com/examples/node-js-and-typescript-continuous-integration/)
- [JavaScript and Node.js Continuous Integration Guide](https://docs.semaphoreci.com/programming-languages/javascript-and-node-js/)
- [Getting Started with Node.js and Mocha](https://semaphoreci.com/community/tutorials/getting-started-with-node-js-and-mocha)

Thanks for reading! Leave a comment if you liked it 👋