Appearance
Deploy a Node.js app
A complete walkthrough: from a fresh pod to a Node.js app serving HTTPS traffic on your own domain. Every click is named, so you can follow it straight through without guessing which screen you should be on.
Budget twenty minutes, most of which is waiting for DNS.
What you'll build
- A Small pod running Ubuntu 24.04
- Node.js 20 LTS
- A trivial Express app on internal port 3000
- Mapped to
app.example.comover HTTPS
What you need first
- At least $3 of credit on the account. Click Billing in the console's side panel, or go straight to Billing.
- A domain you own, plus access to wherever its DNS is managed - your registrar, Cloudflare, Route 53, whoever answers for the name. You will be adding records there, and nobody else can do that part for you.
- An SSH client, and ideally an SSH public key to hand.
1. Create the pod
- Click Pods in the side panel, or open Pods.
- Click New, at the top right of the pods table. You land on the Create Pod page.
- Fill in the form:
- Workload - Ubuntu. (Picking MySQL here would give you a database pod instead, and you cannot map a domain to one of those.)
- Version - 24.04
- Compute -
1 vCPU, 1GB RAM. The dropdown lists CPU and RAM combinations, not size names. - Pod Name (optional) -
hello, so you can find it again in the table. - Region - nearest to your users.
- SSH Public Key (optional) - paste a public key, or choose one you have used before from the Select a saved SSH key dropdown. Pasted keys are saved for next time.
- Storage - 5 GB.
- Leave Network (optional) and Firewall (optional) blank. These two only appear once you have a network or a firewall to pick, so on a new account they aren't on the form at all. Either way, step 2 deals with what that means.
- Click Create Pod.
You come back to the pods table, not to the pod itself. Its Status goes from Initializing to Running, usually in under a minute.
Click the pod's name in the table to open it. You land on the pod's page, on the Overview tab.
Grab the password now if you skipped the key
Skip the SSH key and you get a one-time login password instead. It appears in the toast at the top right immediately after you click Create Pod ("Auto-generated password: ...") and never again. Copy it before the toast fades - hovering over it stops the clock. The full story is in Connecting via SSH.
More on the form, field by field: the pod quickstart.
2. Let your IP through the firewall
Because you didn't pick a firewall, the pod got a brand new firewall of its own that blocks all public traffic - your own SSH included. Skip this step and the next one just hangs until it times out, with no error worth reading. Put your address on the allow-list first.
- On the pod's page, click the Networking tab.
- In the Security & Public Access card, click the firewall's name in the Firewall row. You land on that firewall's page.
- Click New Rule, at the top right. An Add Rule form opens above the rules.
- Fill in IP Address / Network -
203.0.113.42/32for a single address - and a Description you will still recognise in six months. Click Add. - Click Apply Changes, at the top right. Until you do, the rule exists only in your browser, and the header says Unsaved changes.
Rules take effect within seconds, no reboot. They carry no ports, so an address on the list reaches every public port on the pod, and an address that isn't reaches none of them. See Networks & firewalls for the whole model.
The allow-list guards the pod's own public ports, which is where SSH arrives. Visitors who turn up later on your mapped domain come in by a different route and aren't filtered by it, so this rule is the only one you need for this tutorial.
3. SSH in
- Open Pods again and click the pod's name in the table.
- Click the Connection Strings tab.
- Click the Public SSH card. It copies a whole command, with the external port, username and host already in it:bash
ssh -p 3001 ubuntu@your-pod.microapps.io - Paste it into your terminal and run it. Your port and host will differ from the example.
If you would rather assemble the command yourself, the pieces are labelled on two other tabs of the same page: Username on the Overview tab's Details card, and Public Host and Ports on the Networking tab's Security & Public Access card. The Ports row shows two numbers, one marked private and one marked public - the public one is what goes after -p.
No SSH client to hand? Click Shell at the top right of the pod's page for an in-browser terminal. It needs no port, no key and no allow-list entry, so it also works when step 2 has gone wrong. Everything below runs the same in either.
Details and troubleshooting: Connecting via SSH.
4. Install Node.js
bash
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
node --version5. Write the app
bash
mkdir ~/hello && cd ~/hello
npm init -y
npm install expressindex.js:
js
const express = require('express')
const app = express()
app.get('/', (_, res) => res.send('Hello from MicroApps!'))
app.listen(3000, () => console.log('Listening on 3000'))The app listens on internal port 3000. We'll expose it via a domain mapping in step 8 - no need to mess with port mapping for HTTP traffic.
6. Run it under systemd
/etc/systemd/system/hello.service:
ini
[Unit]
Description=Hello app
After=network.target
[Service]
ExecStart=/usr/bin/node /home/ubuntu/hello/index.js
Restart=always
User=ubuntu
WorkingDirectory=/home/ubuntu/hello
[Install]
WantedBy=multi-user.targetbash
sudo systemctl daemon-reload
sudo systemctl enable --now hello
sudo systemctl status helloCheck it from inside the pod before you go anywhere near DNS:
bash
curl localhost:3000That should print Hello from MicroApps!. If it doesn't, fix it here - the rest of the tutorial only changes who can reach this port, not whether it answers.
7. Add the domain and prove you own it
- Click Domains in the side panel, or open Domains.
- Click New, at the top right of the domains table. You land on the Create Domain page.
- Type
app.example.cominto Domain Name. That is the whole form. - Click Create Domain.
- You come back to the domains table. Click the domain's name to open it. You land on its Overview tab.
- Click the DNS tab. The table has Name, Type, Content, Status and TTL columns, and two rows you can publish right now:
microapps-verify, a TXT, marked with an Ownership chip._acme-challenge, a CNAME, marked with an SSL chip. Publishing it now saves a round trip in step 9.
- At your DNS provider, create both records. Copy each value from the Content column with the copy icon rather than retyping it - the ownership token is long and one wrong character means it never verifies. Your provider most likely calls that field Value, Target or Data, and calls Name either Host or Record name.
- Give DNS a few minutes, then click Verify ownership on the
microapps-verifyrow. Recheck DNS, at the top right, rechecks every row at once.
Each row's Status goes from Pending to Verified. Once ownership verifies, the domain is locked to your account for good - which is why it is worth doing before anyone else tries.
Record names are relative to the domain you added
The console shows names as they sit under app.example.com. If your DNS provider's zone is example.com, add the missing part yourself: microapps-verify.app and _acme-challenge.app, and app for the @ row that turns up in step 8.
8. Map the domain to the pod
Mapping tells us which pod serves the domain. Publishing the record it produces tells the internet to send visitors to us.
- On the domain's Overview tab, find the Pod mapping card. It says "No pod mapped".
- Click Map a pod. A dialog opens.
- Fill it in:
- Pod - your
hellopod. Database pods are not offered, because you cannot map a domain to one. - Pod port number -
3000, the internal port your app listens on. You don't need to assign an external port to it yourself. - Traffic Type - HTTP. This is what your app speaks inside the pod, not what your visitors use. Express here is serving plain HTTP, so HTTP it is. Visitors still arrive over HTTPS: we accept the encrypted connection on the public side and forward it to internal port 3000.
- Pod - your
- Click Save Mapping. The card now shows the pod's name and its internal endpoint.
- Click the DNS tab. There is a new row named
@, marked with a Web chip. For a subdomain likeapp.example.comit is a CNAME. - Publish it at your provider the same way as the others, then click Recheck DNS. When that row reads
Verified, visitors are reaching your pod.
Mapping a root domain instead
A root domain like example.com gets an ALIAS rather than a CNAME, because a plain CNAME cannot sit at the root of a zone next to its MX and TXT records. Your provider may call it ANAME or CNAME flattening, and not all of them have one - see Root domains and ALIAS records for the ways round it.
9. Turn on SSL
The _acme-challenge record from step 7 is the entire mechanism: the certificate is issued through it and renewed through it, for as long as the domain exists.
- Go to the domain's Overview tab and find the SSL certificate card. It says "Not enabled".
- Read the line under that. "The _acme-challenge record is verified, so the certificate can be issued now" means you're ready. If it tells you to publish the record first, click View DNS records, sort that row out, and come back.
- Click Enable SSL, then confirm.
The card switches to "Enabled" and names what the certificate covers: app.example.com and *.app.example.com, renewed automatically before it expires.
Leave the _acme-challenge record in place
Renewal runs through it too. Delete it and the certificate you have keeps serving until it expires, then quietly fails to renew.
10. Visit your app
Open https://app.example.com. You'll see "Hello from MicroApps!".
Nothing there? Work backwards through the three things that can be wrong, in this order:
- The DNS tab still shows
Pendingon the@row. DNS hasn't propagated, or the record went into the wrong zone. Click Recheck DNS. - The Pod mapping card's endpoint is right but the pod isn't answering. Back on the pod,
sudo systemctl status helloandcurl localhost:3000. - The certificate. If the browser complains about HTTPS rather than failing to connect, the SSL certificate card on the domain's Overview tab will say so.
That's it. You have a Node.js app on the internet, on your own domain, with HTTPS. Billing is hourly from your credit - see the pricing page.
See also
- Custom domains & SSL - the domain half of this tutorial, in full
- Connecting via SSH - keys, passwords and the firewall
- Enable automatic snapshots
- Lock down with firewalls