Deploy a TypeScript application to production on a Digital Ocean Droplet using Temporal Cloud
Introduction
When you are ready to deploy your TypeScript based Temporal Application to production, you need a server to host it. Digital Ocean provides flexible Cloud servers called Droplets that can be used to host your application.
In this tutorial we will walk through the steps to deploy your Temporal Application to a Digital Ocean Droplet while using Temporal Cloud as your Temporal Application's orchestrator.
Prerequisites
Before you begin, you will need the following:
- A Digital Ocean account: You need a Digital Ocean account to create a Droplet.
- A domain name: You need a domain, because you need to have a valid SSL certificate for the Temporal Application to communicate with Temporal Cloud.
- A Temporal Cloud account: You need a Temporal Cloud account to generate a certificate for your Temporal Application to communicate with Temporal Cloud.
- A Temporal Application that you want to deploy: You need a TypeScript based Temporal Application that you want to deploy to a Digital Ocean Droplet.
Step 1 -- Create and set up Droplet
Create a new Droplet and choose the Ubuntu 20.04 image or Ubuntu 22.04 image.
Then, set up the following on your Droplet:
- Configure SSH
- Install Node.js
- Install TypeScript and TS Node
- Set up Nginx as a reverse proxy
- Create a domain certificate
Set up SSH
Next, set up SSH so you can set up your server from your local terminal. You can do that by following the Add SSH Key to Droplet tutorial.
If you already have a private key on your local machine and you need to force SSH to use the new key, you can force it with the -i flag, for example:
ssh -i ~/.ssh/id_rsa_digitalocean
Install Node.js
Since you are going to be deploying a TypeScript application, you need to install TypeScript into this environment. First, install Node.js, a JavaScript runtime for server-side programming.
There are several options for installing Node.js. The most straightforward is to install from your system package manager. For example, on Ubuntu:
sudo apt update
sudo apt install nodejs
This will install the newest stable version from Ubuntu sources. If you need to install a different version of Node.js for compatibility reasons, refer to these tutorials respectively:
Install TypeScript and TS Node
Next, use the Node.js package manager, npm, to install some packages that provide TypeScript compatibility. Install the typescript and ts-node packages globally, using the -g flag:
sudo npm install -g typescript ts-node
Update your domain's DNS
Ensure the domain's A record points to the droplet's IP address.
Install and Configure Nginx as a Reverse Proxy
Because you'll be deploying an application that handles external requests over HTTP/S, we strongly recommend also deploying an Nginx reverse proxy.
Node.js applications typically bind to localhost. This means by default, your application will only be accessible locally on the machine it resides on. While you can specify a different port to allow access through the internet, Node.js's built in application server provides fairly minimal security and configurability. Using a reverse proxy isolates the application server from direct internet access, allows you to centralize firewall protection, and minimizes the attack surface for common threats such as denial of service attacks.
From a client’s perspective, interacting with a reverse proxy is no different from interacting with the application server directly. It is functionally the same, and the client cannot tell the difference. A client requests a resource and then receives it, without any extra configuration required by the client. Nginx, a popular open source web server, can act as a reverse proxy with a nearly drop-in configuration.
Follow these tutorials, for Ubuntu 20.04 or 22.04 respectively, including setting up the server blocks:
Create a domain certificate
Use Let's encrypt certbot to create a certificate for your domain. This will effectively enable HTTPS for your domain and your Digital Ocean Droplet.
Follow the tutorial for your Ubuntu version:
- How To Secure Nginx with Let's Encrypt on Ubuntu 20.04
- How To Secure Nginx with Let's Encrypt on Ubuntu 22.04
Step 2 -- Create a Temporal Cloud certificate
You will need a seperate certificate to authenticate the communication between your Temporal Client, Temporal Workers, and Temporal Cloud.
You can use tcld to generate the certificate.
Follow the steps to install tcld.
Then, follow the steps to generate a Temporal Cloud certificate using tcld.
Step 3 -- Configure your app on the Droplet
Now that you have your Droplet set up, and your Temporal Cloud certificates, you can set up your Temporal Application on the Droplet.
Clone the repo
Using Git, you can clone the droplet and install the dependencies.
git clone <your-repo>
cd <your-repo>/temporal-application
npm install
cd ..
cd <your-repo>/bot
npm install
Populate your .env file
Per the 12 Factor App methodology, you should store your credentials in environment variables.
Create a .env file in the root of any project that uses a Temporal Client or runs a Temporal Worker.
Then, add your Temporal Cloud Namespace, address, certificate PEM and KEY to the .env file.
TEMPORAL_CLOUD_ADDRESS=""
TEMPORAL_CLOUD_NAMESPACE=""
TEMPORAL_CLOUD_PEM=""
TEMPORAL_CLOUD_PRIVATE_KEY=""
Your Temporal Client and Worker will need those environment variables to connect to Temporal Cloud.
Use Temporal Cloud connection details
Connecting to Temporal Cloud requires a few changes to your Temporal Client and Worker, using the environment variables you set in the .env file.
Temporal Client
When developing locally, your Temporal Client may look like this:
docs/tutorials/typescript/work-queue-slack-app/code/bot/modules/dev-temporal-client.ts
- TypeScript
- JavaScript
import "dotenv/config";
import {Client, Connection} from "@temporalio/client";
export let temporalClient: Client;
export async function initializeTemporalClient() {
const connection = await Connection.connect();
temporalClient = new Client({
connection,
namespace: process.env.TEMPORAL_DEV_NAMESPACE!,
});
}
import "dotenv/config";
import { Client, Connection } from "@temporalio/client";
export let temporalClient;
export async function initializeTemporalClient() {
const connection = await Connection.connect();
temporalClient = new Client({
connection,
namespace: process.env.TEMPORAL_DEV_NAMESPACE,
});
}
When using Temporal Cloud, your Temporal Client should look like this:
docs/tutorials/typescript/work-queue-slack-app/code/bot/modules/cloud-temporal-client.ts
- TypeScript
- JavaScript
import "dotenv/config";
import {Client, Connection} from "@temporalio/client";
export let temporalClient: Client;
export async function initializeTemporalClient() {
const key = Buffer.from(process.env.TEMPORAL_CLOUD_PRIVATE_KEY!, "utf-8");
const cert = Buffer.from(process.env.TEMPORAL_CLOUD_PEM!, "utf-8");
const address = process.env.TEMPORAL_CLOUD_ADDRESS!;
const namespace = process.env.TEMPORAL_CLOUD_NAMESPACE!;
const connection = await Connection.connect({
address: address,
tls: {
clientCertPair: {
crt: cert,
key: key,
},
},
});
temporalClient = new Client({
connection,
namespace: namespace,
});
}
import "dotenv/config";
import { Client, Connection } from "@temporalio/client";
export let temporalClient;
export async function initializeTemporalClient() {
const key = Buffer.from(process.env.TEMPORAL_CLOUD_PRIVATE_KEY, "utf-8");
const cert = Buffer.from(process.env.TEMPORAL_CLOUD_PEM, "utf-8");
const address = process.env.TEMPORAL_CLOUD_ADDRESS;
const namespace = process.env.TEMPORAL_CLOUD_NAMESPACE;
const connection = await Connection.connect({
address: address,
tls: {
clientCertPair: {
crt: cert,
key: key,
},
},
});
temporalClient = new Client({
connection,
namespace: namespace,
});
}
Temporal Worker
When developing locally, your Temporal Worker may look like this:
docs/tutorials/typescript/work-queue-slack-app/code/temporal-application/src/dev-worker.ts
- TypeScript
- JavaScript
import "dotenv/config";
import path from "path";
import {Worker, NativeConnection} from "@temporalio/worker";
import * as activities from "./activities/index";
async function run() {
try {
const worker = await Worker.create({
namespace: process.env.TEMPORAL_DEV_NAMESPACE || "",
workflowsPath: path.resolve(__dirname, "./workflows"),
activities,
taskQueue: `${process.env.ENV}-temporal-iq-task-queue`,
});
await worker.run();
} catch (err) {
console.error(err);
process.exit(1);
}
}
run();
import "dotenv/config";
import path from "path";
import { Worker } from "@temporalio/worker";
import * as activities from "./activities/index";
async function run() {
try {
const worker = await Worker.create({
namespace: process.env.TEMPORAL_DEV_NAMESPACE || "",
workflowsPath: path.resolve(__dirname, "./workflows"),
activities,
taskQueue: `${process.env.ENV}-temporal-iq-task-queue`,
});
await worker.run();
}
catch (err) {
console.error(err);
process.exit(1);
}
}
run();
When using Temporal Cloud, your Temporal Client should look like this:
docs/tutorials/typescript/work-queue-slack-app/code/temporal-application/src/cloud-worker.ts
- TypeScript
- JavaScript
import "dotenv/config";
import path from "path";
import {Worker, NativeConnection} from "@temporalio/worker";
import * as activities from "./activities/index";
async function run() {
try {
const key = Buffer.from(
process.env.TEMPORAL_CLOUD_PRIVATE_KEY || "",
"utf-8"
);
const cert = Buffer.from(process.env.TEMPORAL_CLOUD_PEM || "", "utf-8");
const connection = await NativeConnection.connect({
address: process.env.TEMPORAL_CLOUD_ADDRESS || "",
tls: {
clientCertPair: {
crt: cert,
key: key,
},
},
});
const worker = await Worker.create({
connection,
namespace: process.env.TEMPORAL_CLOUD_NAMESPACE || "",
workflowsPath: path.resolve(__dirname, "./workflows"),
activities,
taskQueue: `${process.env.ENV}-temporal-iq-task-queue`,
});
await worker.run();
} catch (err) {
console.error(err);
process.exit(1);
}
}
run();
import "dotenv/config";
import path from "path";
import { Worker, NativeConnection } from "@temporalio/worker";
import * as activities from "./activities/index";
async function run() {
try {
const key = Buffer.from(process.env.TEMPORAL_CLOUD_PRIVATE_KEY || "", "utf-8");
const cert = Buffer.from(process.env.TEMPORAL_CLOUD_PEM || "", "utf-8");
const connection = await NativeConnection.connect({
address: process.env.TEMPORAL_CLOUD_ADDRESS || "",
tls: {
clientCertPair: {
crt: cert,
key: key,
},
},
});
const worker = await Worker.create({
connection,
namespace: process.env.TEMPORAL_CLOUD_NAMESPACE || "",
workflowsPath: path.resolve(__dirname, "./workflows"),
activities,
taskQueue: `${process.env.ENV}-temporal-iq-task-queue`,
});
await worker.run();
}
catch (err) {
console.error(err);
process.exit(1);
}
}
run();
Step 4 -- Use pm2 to run your Worker
pm2 is a process manager for Node.js applications, and will ensure that your TypeScript application runs continuously on your Droplet.
sudo npm install -g pm2
Change directory into your project and run your Temporal Worker with pm2.
pm2 start <your-app-entry>.ts --interpreter ts-node
Conclusion
You should now have a TypeScript based Temporal Application running on a Digital Ocean Droplet using Temporal Cloud as your Temporal Application's orchestrator.