
More and more developers are choosing self-hosting over traditional PaaS. At first, self-hosting may seem like unnecessary heavy lifting, especially when you can deploy as fast as creating a repo. However, with correct tooling, it’s easy to see why devs are moving away from PaaS. You get dedicated resources and (if needed) a European data center at a fraction of the cost.
The traditional tradeoff was that once you self-host, you are pretty much on your own. But, take Hetzner, for example, an EU-based hosting provider. Hetzner’s dashboard can tell you about machine's resource utilization; however, it doesn’t say which Express route takes 4.5 seconds to complete. That’s the gap AppSignal fills: production-grade APM, error tracking, and host metrics, all in one place.
In this post, you will hook up a Node.js app, deployed to Hetzner via Hatchbox and monitored with AppSignal. By the end, you will have a base for performance monitoring and error alerting.
A few notes before we start:
- Hatchbox is Rails-first. Node.js deployments work (they ship Node via ASDF and support Corepack for Yarn and pnpm), but the documentation on Node specifics is limited. Part of this post covers those missing pieces.
- Keep in mind that everything here applies even if you are deploying Node directly on a bare Hetzner box using PM2 or systemd. Just skip the Hatchbox sections.
Prerequisites
First, make sure you’ve got accounts with the following services:
🔥 Hatchbox offers a 14-day trial, and you can register without a credit card.
P.S. If you get a paid Hatchbox account, you’ll also receive 10% off AppSignal as a new customer.✨
Setting up AppSignal in Node.js
In your Node.js project's directory, run the following command to install the AppSignal package and Express.
npm install @appsignal/nodejs express
Create an appsignal.cjs file to require and configure AppSignal. This file may also be placed in another location, like the src/ directory. For more information, see our configuration section.
// appsignal.cjs const { Appsignal } = require("@appsignal/nodejs"); new Appsignal({ active: true, name: "your-app-name", pushApiKey: process.env.APPSIGNAL_PUSH_API_KEY, });
Then, create a simple Express app in the server.js file:
// server.js const express = require("express"); const app = express(); app.use(express.json()); app.get("/", (req, res) => { res.json({ message: "Hey Hatchbox, AppSignal here." }); }); app.get("/submit", async (req, res, next) => { try { await new Promise((resolve) => setTimeout(resolve, 3000)); throw new Error("Random failure in /submit"); res.json({ message: "fail" }).status(500); } catch (err) { next(err); } }); app.use((err, req, res, next) => { console.error(err); res.status(500).json({ error: err.message }); }); const PORT = process.env.PORT || 9000; app.listen(PORT, () => { console.log(`Server running on <http://localhost>:${PORT}`); });
Now, AppSignal has to load before everything else (not just the app), including express or your ORM. You can make sure this happens with the --require flag:
{ "scripts": { "start": "node --require ./appsignal.cjs server.js" } }
Cool, that's it for the Node.js part. Keep in mind that you would integrate AppSignal further in a real app. This is just an example we used to cover the basics.
Deploying a Hetzner Server via Hatchbox
Now’s the time for us to deploy a Hetzner instance, and this is where Hatchbox comes in handy.
Spinning up an instance manually, SSH-ing into it, installing all the stuff, then pulling from a repo, running it, configuring Nginx… Wasting hours on end only to end up with an app that doesn't even work. Yikes. Hatchbox does all this boring stuff for ya (nothing against DevOps, I just really don't like that part 😄).
So, let's get the deployment from Hatchbox going.
First, navigate to the Clusters page and click New Cluster.

Then, enter the name of the cluster and choose Hetzner as the hosting provider.

⚠️ Go to Hetzner API Docs and allow IPv4 and IPv6 access.
Enter a project name and the access token, then press Connect.
⚠️ The Hetzner API token must be set as
Read & Write. This can be configured while you’re generating the token from Hetzner Console.

Continue by choosing a data center region. See the list of all Hetzner locations.

Now, let's add a real server to the cluster. Press Add a server.

Then, fill in the details:
- Name (the name of your server)
- Size (for the purpose of this guide, we'll use CAX11, which went for $4.99/mo at the time of writing)
- Responsibilities (web server)

Click on the server you’ve created and wait a couple of moments for it to provision and boot up.
Next, press on the Apps section in the left sidebar. Then choose Deploy a new application.

Select the cluster you have previously created and name the app.

Now, you need to connect any Git-based repository to Hatchbox and enter the path in the Repo path field. Then press Save changes and wait for Hatchbox to deploy the app.

Set up Environment Variables
In the Environment section, press Add env var and add the APPSIGNAL_PUSH_API_KEY environment variable with the Push API key for your app.

Set up a Process
After you’ve navigated to the Processes section, press Add new process in the top-right corner.
Select the server from the dropdown, come up with the process name you like, and add the command to run the Node.js server, npm run start. Then press Create process.

The process should run automatically. In the app's Overview section, press View app. You should be presented with the following output in your browser:
{"message":"Hey Hatchbox, AppSignal here."}
Now, you can spam the GET / and GET /submit endpoints for a bit, and your AppSignal dashboard will light up with data.
💡 Hatchbox defaults to port
9000and will create an automatic proxy for HTTP apps.
Performance Monitoring
Once the data from Hatchbox is flowing, you will spend most of the time in the Performance tab.
This tab helps you quickly identify issues like:
- Long response times, such as an API endpoint that normally responds in 80 ms suddenly taking 4 seconds after a deploy;
- Bottlenecks in real-time, like a database query holding up the entire request cycle while everything else is waiting;
- Weird event timelines, such as a background job firing three times in quick succession when it should run once.
And much more, all within clean visual charts and timetables.

AppSignal instruments a long list of libraries, databases, and frameworks out of the box. This means you don't have to write a single line of instrumentation code to get route-level timings and event timelines. It's all there the moment data starts flowing.
Error Tracking and Alerts
AppSignal automatically catches unhandled exceptions and promise rejections, which covers the majority of production issues. For errors you handle manually but still want visibility on, you can report them explicitly by using sendError (learn more about it here).
For notifications, head to App Settings -> Notifications -> Notifiers. It’s best to route alerts by severity rather than blasting every channel with every error:
| Channel | Good for | Bad for |
|---|---|---|
| Slack | New errors, deploy regressions | Anything on-call; async comms can easily miss these alerts |
| Weekly digests | Real-time incidents | |
| PagerDuty / OpsGenie | severity: critical tags, production outages | Noisy low-priority errors |
| Microsoft Teams | Same as Slack | — |
| Generic webhook | Notify your own tooling (status pages, Linear, etc.) | One-off alerts |
This could be a sensible default:
- Slack/Microsoft Teams for all new errors, depending on where your team lives
- Email for regressions
- PagerDuty for anything tagged critical
Let’s face it, nobody needs to be woken up at 3 a.m. by an ECONNRESET . 🙃
Host Metrics: Right-Sizing Your Server
This is the section where self-hosting really starts to make sense.
With PaaS, you are paying for "dynos," but the pricing is abstracted from the actual provided resources.
With Hetzner, you pay for 2vCPU and 4 GB of RAM + some storage. That's it. Host metrics then show whether you are actually using what you are paying for.

AppSignal provides extensive host metrics with real-time and historical CPU, memory, disk, network usage, and other collected data. These metrics allow you to spot performance issues ASAP and fix the root cause or just see how the billing correlates to usage.
Wrap-Up
Self-hosting does not mean giving up the nice-to-haves or the conveniences that come with PaaS (more about on what's wrong with PaaS). The tradeoff can absolutely be worth it.
With about 15 minutes of setup in Hatchbox, you get a deployed app PLUS extensive analytics and app monitoring from AppSignal. All of that at Hetzner-level prices, with full control and data sovereignty.
Try AppSignal's free trial and dig into the Node.js docs. Once you’ve covered the basics, read the Express performance post for the next stage.
Happy monitoring!
Frequently Asked Questions (FAQ)
Do I need Hatchbox to follow this guide, or can I deploy directly on Hetzner?
Hatchbox is optional. The AppSignal setup is identical whether you are deploying through Hatchbox or via SSH.
Why do I need --require ./appsignal.cjs instead of just importing AppSignal at the top of my entry file?
AppSignal instruments libraries by patching them the moment they get loaded. This allows some time for AppSignal to hook into other libraries.
Which Hetzner server size should I pick for a Node.js app?
CAX11 (~$5/mo) is sufficient for low-traffic web apps, personal projects, internal tools, MVPs, and similar.
Will AppSignal work with Fastify, NestJS, or Next.js, or is it Express-only?
All of the above, plus Koa and the raw Node HTTP module. Auto-instrumentation covers Express, Fastify, NestJS (which runs on Express or Fastify underneath), Next.js, and the usual database clients.
How much does this whole stack actually cost per month?
It comes out to roughly 5/month, Hatchbox charges a flat 23.25/month for the lowest tier.
Wondering what you can do next?
Finished this article? Here are a few more things you can do:
- Subscribe to our JavaScript Sorcery newsletter and never miss an article again.
- Start monitoring your JavaScript app with AppSignal.
- Share this article on social media
Most popular Javascript articles

Top 5 HTTP Request Libraries for Node.js
Let's check out 5 major HTTP libraries we can use for Node.js and dive into their strengths and weaknesses.
See more
When to Use Bun Instead of Node.js
Bun has gained in popularity due to its great performance capabilities. Let's see when Bun is a better alternative to Node.js.
See more
How to Implement Rate Limiting in Express for Node.js
We'll explore the ins and outs of rate limiting and see why it's needed for your Node.js application.
See more

Dejan Lukić
Our guest author Dejan is an electronics and backend engineer, who is pursuing entrepreneurship with SaaS and service-based agencies and is passionate about content creation.
All articles by Dejan LukićBecome our next author!
AppSignal monitors your apps
AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

