How to Deploy Deno to Digital Ocean
Digital Ocean is a popular cloud infrastructure provider offering a variety of hosting services ranging from networking, to compute, to storage.
Here’s a step by step guide to deploying a Deno app to Digital Ocean using Docker and GitHub Actions.
The pre-requisite for this is:
Create Dockerfile and docker-compose.yml
To focus on the deployment, our app will simply be a main.ts
file that returns
a string as an HTTP response:
import { Application } from "https://deno.land/x/oak/mod.ts";
const app = new Application();
app.use((ctx) => {
ctx.response.body = "Hello from Deno and Digital Ocean!";
});
await app.listen({ port: 8000 });
Then, we’ll create two files – Dockerfile
and docker-compose.yml
– to
build the Docker image.
In our Dockerfile
, let’s add:
FROM denoland/deno
EXPOSE 8000
WORKDIR /app
ADD . /app
RUN deno cache main.ts
CMD ["run", "--allow-net", "main.ts"]
Then, in our docker-compose.yml
:
version: '3'
services:
web:
build: .
container_name: deno-container
image: deno-image
ports:
- "8000:8000"
Let’s test this locally by running docker compose -f docker-compose.yml build
,
then docker compose up
, and going to localhost:8000
.
It works!
Build, Tag, and Push your Docker image to Digital Ocean Container Registry
Digital Ocean has its own private Container Registry, with which we can push and
pull Docker images. In order to use this registry, let’s
install and authenticate doctl
on the command line.
After that, we’ll create a new private registry named deno-on-digital-ocean
:
doctl registry create deno-on-digital-ocean
Using our Dockerfile and docker-compose.yml, we’ll build a new image, tag it,
and push it to the registry. Note that docker-compose.yml
will name the build
locally as deno-image
.
docker compose -f docker-compose.yml build
Let’s tag it with
new
:
docker tag deno-image registry.digitalocean.com/deno-on-digital-ocean/deno-image:new
Now we can push it to the registry.
docker push registry.digitalocean.com/deno-on-digital-ocean/deno-image:new
You should see your new deno-image
with the new
tag in your
Digital Ocean container registry:
Perfect!
Deploy to Digital Ocean via SSH
Once our deno-image
is in the registry, we can run it anywhere using
docker run
. In this case, we’ll run it while in our
Digital Ocean Droplet, their
hosted virtual machine.
While on your Droplet page, click on
your Droplet and then console
to SSH into the virtual machine. (Or you can
ssh directly from your command line.)
To pull down the deno-image
image and run it, let’s run:
docker run -d --restart always -it -p 8000:8000 --name deno-image registry.digitalocean.com/deno-on-digital-ocean/deno-image:new
Using our browser to go to the Digital Ocean address, we now see:
Boom!
Automate the Deployment via GitHub Actions
Let’s automate that entire process with GitHub actions.
First, let’s get all of our environmental variables needed for logging into
doctl
and SSHing into the Droplet:
- DIGITALOCEAN_ACCESS_TOKEN
- DIGITALOCEAN_HOST (the IP address of your Droplet)
- DIGITALOCEAN_USERNAME (the default is
root
) - DIGITALOCEAN_SSHKEY (more on this below)
DIGITALOCEAN_SSHKEY
Generate The DIGITALOCEAN_SSHKEY
is a private key where its public counterpart exists
on the virtual machine in its ~/.ssh/authorized_keys
file.
To do this, first let’s run ssh-keygen
on your local machine:
ssh-keygen
When prompted for an email, be sure to use your GitHub email for the GitHub Action to authenticate properly. Your final output should look something like this:
Output
Your identification has been saved in /your_home/.ssh/id_rsa
Your public key has been saved in /your_home/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:/hk7MJ5n5aiqdfTVUZr+2Qt+qCiS7BIm5Iv0dxrc3ks user@host
The key's randomart image is:
+---[RSA 3072]----+
| .|
| + |
| + |
| . o . |
|o S . o |
| + o. .oo. .. .o|
|o = oooooEo+ ...o|
|.. o *o+=.*+o....|
| =+=ooB=o.... |
+----[SHA256]-----+
Next, we’ll have to upload the newly generated public key to your Droplet. You
can either use ssh-copy-id
or
manually copy it, ssh into your Droplet, and pasting it to
~/.ssh/authorized_keys
.
Using ssh-copy-id
:
ssh-copy-id {{ username }}@{{ host }}
This command will prompt you for the password. Note that this will automatically
copy id_rsa.pub
key from your local machine and paste it to your Droplet’s
~/.ssh/authorized_keys
file. If you’ve named your key something other than
id_rsa
, you can pass it with the -i
flag to the command:
ssh-copy-id -i ~/.ssh/mykey {{ username }}@{{ host }}
To test whether this is done successfully:
ssh -i ~/.ssh/mykey {{ username }}@{{ host }}
Awesome!
Define the yml File
The final step is to put this all together. We’re basically taking each step during the manual deployment and adding them to a GitHub Actions workflow yml file:
name: Deploy to Digital Ocean
on:
push:
branches:
- main
env:
REGISTRY: "registry.digitalocean.com/deno-on-digital-ocean"
IMAGE_NAME: "deno-image"
jobs:
build_and_push:
name: Build, Push, and Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout main
uses: actions/checkout@v2
- name: Set $TAG from shortened sha
run: echo "TAG=`echo <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>G</mi><mi>I</mi><mi>T</mi><mi>H</mi><mi>U</mi><msub><mi>B</mi><mi>S</mi></msub><mi>H</mi><mi>A</mi></mrow><mi mathvariant="normal">∣</mi><mi>c</mi><mi>u</mi><mi>t</mi><mo>−</mo><mi>c</mi><mn>1</mn><mo>−</mo><mn>8</mn><mi mathvariant="normal">‘</mi><mi mathvariant="normal">"</mi><mo>></mo><mo>></mo></mrow><annotation encoding="application/x-tex">{GITHUB_SHA} | cut -c1-8`" >> </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">G</span><span class="mord mathnormal" style="margin-right:0.07847em;">I</span><span class="mord mathnormal" style="margin-right:0.13889em;">T</span><span class="mord mathnormal" style="margin-right:0.08125em;">H</span><span class="mord mathnormal" style="margin-right:0.10903em;">U</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em;">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em;"><span style="top:-2.55em;margin-left:-0.0502em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.05764em;">S</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord mathnormal" style="margin-right:0.08125em;">H</span><span class="mord mathnormal">A</span></span><span class="mord">∣</span><span class="mord mathnormal">c</span><span class="mord mathnormal">u</span><span class="mord mathnormal">t</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.7278em;vertical-align:-0.0833em;"></span><span class="mord mathnormal">c</span><span class="mord">1</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.7335em;vertical-align:-0.0391em;"></span><span class="mord">8‘"</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">>></span></span></span></span>GITHUB_ENV
- name: Build container image
run: docker compose -f docker-compose.yml build
- name: Tag container image
run: docker tag <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>e</mi><mi>n</mi><mi>v</mi><mi mathvariant="normal">.</mi><mi>I</mi><mi>M</mi><mi>A</mi><mi>G</mi><msub><mi>E</mi><mi>N</mi></msub><mi>A</mi><mi>M</mi><mi>E</mi></mrow><annotation encoding="application/x-tex">{{ env.IMAGE_NAME }} </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal">e</span><span class="mord mathnormal">n</span><span class="mord mathnormal" style="margin-right:0.03588em;">v</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.07847em;">I</span><span class="mord mathnormal" style="margin-right:0.10903em;">M</span><span class="mord mathnormal">A</span><span class="mord mathnormal">G</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em;"><span style="top:-2.55em;margin-left:-0.0576em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.10903em;">N</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord mathnormal">A</span><span class="mord mathnormal" style="margin-right:0.05764em;">ME</span></span></span></span></span></span>{{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.TAG }}
- name: Install `doctl`
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- name: Log in to Digital Ocean Container Registry
run: doctl registry login --expiry-seconds 600
- name: Push image to Digital Ocean Container Registry
run: docker push <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>e</mi><mi>n</mi><mi>v</mi><mi mathvariant="normal">.</mi><mi>R</mi><mi>E</mi><mi>G</mi><mi>I</mi><mi>S</mi><mi>T</mi><mi>R</mi><mi>Y</mi></mrow><mi mathvariant="normal">/</mi></mrow><annotation encoding="application/x-tex">{{ env.REGISTRY }}/</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal">e</span><span class="mord mathnormal">n</span><span class="mord mathnormal" style="margin-right:0.03588em;">v</span><span class="mord">.</span><span class="mord mathnormal">REG</span><span class="mord mathnormal" style="margin-right:0.07847em;">I</span><span class="mord mathnormal" style="margin-right:0.00773em;">STR</span><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span></span></span><span class="mord">/</span></span></span></span>{{ env.IMAGE_NAME }}:${{ env.TAG }}
- name: Deploy via SSH
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.DIGITALOCEAN_HOST }}
username: ${{ secrets.DIGITALOCEAN_USERNAME }}
key: ${{ secrets.DIGITALOCEAN_SSHKEY }}
script: |
# Login to Digital Ocean Container Registry
docker login -u <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>s</mi><mi>e</mi><mi>c</mi><mi>r</mi><mi>e</mi><mi>t</mi><mi>s</mi><mi mathvariant="normal">.</mi><mi>D</mi><mi>I</mi><mi>G</mi><mi>I</mi><mi>T</mi><mi>A</mi><mi>L</mi><mi>O</mi><mi>C</mi><mi>E</mi><mi>A</mi><msub><mi>N</mi><mi>A</mi></msub><mi>C</mi><mi>C</mi><mi>E</mi><mi>S</mi><msub><mi>S</mi><mi>T</mi></msub><mi>O</mi><mi>K</mi><mi>E</mi><mi>N</mi></mrow><mo>−</mo><mi>p</mi></mrow><annotation encoding="application/x-tex">{{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} -p </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal">secre</span><span class="mord mathnormal">t</span><span class="mord mathnormal">s</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.02778em;">D</span><span class="mord mathnormal" style="margin-right:0.07847em;">I</span><span class="mord mathnormal">G</span><span class="mord mathnormal" style="margin-right:0.07847em;">I</span><span class="mord mathnormal" style="margin-right:0.13889em;">T</span><span class="mord mathnormal">A</span><span class="mord mathnormal">L</span><span class="mord mathnormal" style="margin-right:0.05764em;">OCE</span><span class="mord mathnormal">A</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.10903em;">N</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em;"><span style="top:-2.55em;margin-left:-0.109em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">A</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord mathnormal" style="margin-right:0.05764em;">CCES</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.05764em;">S</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em;"><span style="top:-2.55em;margin-left:-0.0576em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.13889em;">T</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord mathnormal" style="margin-right:0.02778em;">O</span><span class="mord mathnormal" style="margin-right:0.07153em;">K</span><span class="mord mathnormal" style="margin-right:0.10903em;">EN</span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">p</span></span></span></span>{{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} registry.digitalocean.com
# Stop and remove a running image.
docker stop ${{ env.IMAGE_NAME }}
docker rm ${{ env.IMAGE_NAME }}
# Run a new container from a new image
docker run -d --restart always -it -p 8000:8000 --name <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>e</mi><mi>n</mi><mi>v</mi><mi mathvariant="normal">.</mi><mi>I</mi><mi>M</mi><mi>A</mi><mi>G</mi><msub><mi>E</mi><mi>N</mi></msub><mi>A</mi><mi>M</mi><mi>E</mi></mrow><annotation encoding="application/x-tex">{{ env.IMAGE_NAME }} </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal">e</span><span class="mord mathnormal">n</span><span class="mord mathnormal" style="margin-right:0.03588em;">v</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.07847em;">I</span><span class="mord mathnormal" style="margin-right:0.10903em;">M</span><span class="mord mathnormal">A</span><span class="mord mathnormal">G</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em;"><span style="top:-2.55em;margin-left:-0.0576em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.10903em;">N</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord mathnormal">A</span><span class="mord mathnormal" style="margin-right:0.05764em;">ME</span></span></span></span></span></span>{{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.TAG }}
When you push to GitHub, this yml file is automatically detected, triggering the Deploy action.