Skip to main content

Command Palette

Search for a command to run...

How to Deploy a Node.js App From GitHub

A practical guide to taking a Node.js repository from GitHub to a running application without managing a server

Updated
9 min readView as Markdown
How to Deploy a Node.js App From GitHub
D
Engineering, deployment guides, and lessons from building Deploy Hatch — a developer platform for deploying applications and persistent workloads from GitHub without managing the infrastructure underneath. Ship the App. Not the Server.

You have a Node.js application working locally.

The code is pushed to GitHub. Your dependencies are defined. npm start works.

Now you need to get it running somewhere other people—or other services—can actually reach it.

This is the point where a relatively simple Node.js project can suddenly turn into an infrastructure project.

A traditional deployment might require provisioning a server, configuring SSH, installing Node.js, cloning the repository, managing environment variables, setting up a process manager, configuring a reverse proxy, enabling HTTPS, and figuring out what should happen when the application crashes.

Those are useful skills to understand.

But they aren't necessarily things you should have to manage every time you want to ship an application.

In this guide, we'll look at what a Node.js application actually needs to be deployable and how a GitHub repository can go from source code to a running workload.

What we're deploying

Consider a very small Express application.

const express = require("express");

const app = express();

const port = process.env.PORT || 3000;

app.get("/", (req, res) => {
  res.json({
    status: "running",
    message: "Hello from Node.js!"
  });
});

app.listen(port, () => {
  console.log(`Server listening on port ${port}`);
});

Its package.json might look like this:

{
  "name": "example-node-app",
  "version": "1.0.0",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "express": "^5.0.0"
  }
}

There isn't much to it.

But there are several details here that become important once the application leaves your computer.

1. Make sure your application has a start command

A deployment platform needs to know how to start your application.

For many Node.js projects, that information already exists in package.json.

For example:

{
  "scripts": {
    "start": "node index.js"
  }
}

Or, if your application needs to be built first:

{
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

Frameworks may use their own commands:

{
  "scripts": {
    "build": "next build",
    "start": "next start"
  }
}

The important idea is simple:

Your repository should contain a reproducible way to build and start the application.

If running your project requires a series of commands that exist only in your head, deployment becomes much harder.

2. Don't hard-code the port

This is one of the easiest mistakes to make when moving a Node.js application into a hosted environment.

Locally, you might write:

app.listen(3000);

A deployment environment may need to provide the port dynamically.

Instead, read it from the environment:

const port = process.env.PORT || 3000;

app.listen(port);

The fallback keeps local development convenient while allowing the deployment environment to control the production port.

The same principle applies to other configuration.

Your application should get environment-specific values from its environment rather than requiring source-code changes for each deployment.

3. Keep secrets out of GitHub

Suppose your application uses an API key.

Don't do this:

const apiKey = "my-secret-api-key";

And don't commit a production .env file containing credentials to your repository.

Instead:

const apiKey = process.env.API_KEY;

Your deployment environment can then provide API_KEY separately from the source code.

This is useful for:

  • API keys

  • database connection strings

  • authentication secrets

  • Discord bot tokens

  • webhook secrets

  • third-party service credentials

Your GitHub repository contains the application.

Your deployment environment contains its secrets.

Keeping those responsibilities separate makes deployments safer and easier to reproduce.

4. Push the deployable version to GitHub

Once the application is ready, commit it normally:

git add .
git commit -m "Prepare app for deployment"
git push origin main

At this point, GitHub becomes more than source control.

It can also become the source for your deployment.

Instead of manually transferring files to a server, a deployment system can retrieve a specific repository and revision directly from GitHub.

Conceptually, the workflow becomes:

GitHub repository
       ↓
Deployment request
       ↓
Build application
       ↓
Create runtime
       ↓
Start application
       ↓
Expose application

This is the model used by many modern application platforms.

The traditional VPS approach

You can absolutely deploy the application yourself.

A simplified VPS workflow might look something like:

ssh user@server

git clone <repository>
cd <repository>

npm install
npm run build
npm start

But npm start alone isn't enough for a reliable production deployment.

What happens when you disconnect from SSH?

What restarts the application after a crash?

What happens when the machine reboots?

How does traffic reach the application?

Where does HTTPS come from?

How do you inspect previous deployments?

How do you safely update the application?

You can solve those problems yourself.

You might introduce a process manager such as PM2 or systemd, configure Nginx or Caddy, provision TLS certificates, establish deployment scripts, configure firewall rules, and build your own logging and monitoring workflow.

For some projects, that's exactly the right approach.

For others, maintaining all of that infrastructure is unrelated to what you're actually trying to build.

Deploying directly from GitHub instead

Deployment platforms move much of that infrastructure behind an application-level workflow.

This is also the approach we're building with Deploy Hatch.

Instead of provisioning a server for every application, the workflow starts with the repository.

A deployment looks roughly like:

GitHub
   ↓
Deploy Hatch control plane
   ↓
Deployment queue
   ↓
Worker
   ↓
Build/runtime preparation
   ↓
Isolated container
   ↓
Running Node.js application

The developer chooses the repository and deployment configuration.

The platform handles the infrastructure required to turn that source code into a running workload.

Deploying a Node.js repository with Deploy Hatch

The basic workflow is intentionally short.

Connect GitHub

Authenticate with Deploy Hatch and connect your GitHub account.

Select the repository containing your Node.js application.

This allows the deployment system to retrieve the repository and the Git revision being deployed.

Create the project

Create a project for the repository.

The repository provides much of the information needed to understand the application, including files such as:

package.json
package-lock.json

and the scripts defined inside them.

Configure environment variables

Add any values your application expects from process.env.

For example:

DATABASE_URL
API_KEY
JWT_SECRET

These values belong in the deployment configuration rather than being committed to GitHub.

Start the deployment

Deploy the project.

The deployment is queued for a worker, which prepares the workload and starts it inside an isolated container.

During that process, logs provide visibility into what is happening.

Instead of seeing only:

Deployment failed

you want the underlying output that explains why it failed.

For example:

npm ERR! Missing script: "start"

or:

Error: Cannot find module 'express'

Those messages turn deployment failures into problems you can actually debug.

What happens after the application starts?

A successful process isn't automatically a useful web application.

The application also needs a way to receive traffic.

For supported web workloads, Deploy Hatch provides a public application URL and routes HTTPS traffic through its ingress layer to the running container.

The path becomes:

Internet
   ↓
HTTPS
   ↓
Ingress
   ↓
Application container
   ↓
Node.js process

This removes another collection of infrastructure tasks from the individual project.

You don't need to manually configure a reverse proxy just to make a small Node.js application reachable.

Deploying updates

Eventually you'll change the application.

Perhaps you fix a bug:

res.json({
  status: "running",
  version: "2.0"
});

Commit and push the update:

git add .
git commit -m "Update API response"
git push

A new deployment can then run from the updated repository revision.

Tracking the Git revision matters because "the latest code" isn't a particularly useful description when you're debugging production.

You want to know which commit is actually running.

A deployment history tied to repository revisions gives you that visibility.

Web applications aren't the only Node.js workloads

Not every Node.js process needs an HTTP endpoint.

Consider a Discord bot:

const { Client, GatewayIntentBits } = require("discord.js");

const client = new Client({
  intents: [GatewayIntentBits.Guilds]
});

client.once("ready", () => {
  console.log(`Logged in as ${client.user.tag}`);
});

client.login(process.env.DISCORD_TOKEN);

There is no Express server here.

There may be no public website at all.

The important requirement is simply:

keep the process running.

The same applies to many:

  • Discord bots

  • queue consumers

  • background workers

  • scheduled processors

  • event listeners

  • automation services

That's why it's useful to think in terms of deploying workloads, not just websites.

Some applications need public ingress.

Others only need a reliable persistent runtime.

What makes a repository deployment-friendly?

Before deploying a Node.js application, I use a short checklist:

  • The application has a clear start command.

  • Dependencies are declared in package.json.

  • A lockfile is committed when appropriate.

  • Secrets come from environment variables.

  • Web servers respect the provided PORT.

  • Required build commands are reproducible.

  • Generated local files aren't required for startup.

  • The application writes useful information to stdout/stderr.

  • The repository contains the code needed to reproduce the running application.

These practices aren't specific to any one hosting platform.

They make Node.js applications easier to deploy almost anywhere.

Deployment should be boring

There is a lot happening between a Git commit and a production process.

Repository retrieval, dependency installation, builds, runtime configuration, containers, networking, TLS, logging, health management, recovery, and deployment state all have to work together.

Understanding those pieces is valuable.

Having to manually rebuild them for every side project isn't.

The goal of a deployment platform isn't to pretend infrastructure doesn't exist.

It's to provide a reliable abstraction over infrastructure so developers can spend more time working on their applications.

If you have a Node.js repository on GitHub and want to try that workflow, you can deploy it with Deploy Hatch:

https://deployhatch.com/deploy-nodejs