How to deploy a Node.js app from a GitHub repo in 2026

Published 2026-05-08 by tavin

The shortest path from a Node.js repository on GitHub to a live URL is shorter than it has ever been. You no longer need a build server, a registry account, a Helm chart, or a 200-line YAML pipeline to put a small backend on the public internet.

The path most teams should take in 2026 looks like this:

  1. Connect a Git repository.
  2. Pick a branch and the port the app listens on.
  3. Push code.
  4. The platform builds a container, runs it, and gives you an HTTPS URL.

This post walks through that path on tavin.cloud and explains the tradeoffs that show up at each step.

What “Node.js on GitHub” usually looks like

A typical small Node service has:

The platform job is to read that repo, decide how to build it, run the resulting process with the right env, and route external traffic to the right port.

Why Railpack is usually the right default

Most Node apps do not need a hand-written Dockerfile to deploy. Railpack is an open-source builder that reads the repo, detects the Node version from package.json or .nvmrc, picks the right package manager from the lockfile, installs dependencies, builds, and produces a container image. The output is a normal Linux container — nothing proprietary about the runtime.

For tavin.cloud, Railpack is the default build path. You do not declare a build command for a typical Node app; the builder infers it. This is the same general pattern that Buildpacks popularized for Heroku and that Nixpacks brought to Railway.

The convenience matters because most Node deployments are repetitive operational work: install, build, start. Auto-detection turns that into platform behavior instead of a per-repo configuration burden.

When you actually need a Dockerfile

A Dockerfile becomes useful when your app needs:

In those cases, drop a Dockerfile at the repo root. tavin.cloud detects the Dockerfile and uses it instead of Railpack. This is the escape hatch principle: auto-detection is the default, explicit control is always available, and the artifact is still a normal container.

A minimal repo-to-deploy walkthrough

Assume an Express app at the repo root:

// server.js
import express from 'express'
const app = express()
app.get('/', (_req, res) => res.send('hello from tavin.cloud'))
app.listen(process.env.PORT || 3000)

package.json:

{
  "name": "hello",
  "type": "module",
  "scripts": { "start": "node server.js" },
  "dependencies": { "express": "^4.21.0" },
  "engines": { "node": ">=20" }
}

The deploy steps:

  1. Push the repo to GitHub.
  2. Sign in at tavin.cloud and connect the repo.
  3. Pick the branch and the listening port (3000).
  4. tavin.cloud detects Node, runs npm install (or bun install, pnpm install, yarn install based on the lockfile), runs npm start, and routes a public URL to port 3000.

Subsequent pushes to the selected branch trigger a new build automatically. If the build fails, the previous deployment keeps serving traffic.

The full step-by-step tour with screenshots is in the tavin.cloud quickstart.

Environment variables and secrets

Real apps need configuration. The interesting choices in 2026 are:

Avoid putting secrets in the repo. Keep DATABASE_URL, STRIPE_SECRET_KEY, and OAuth client secrets in the platform’s env config. Reference them with process.env.X in code.

For services that talk to external APIs, the 12-factor config principle still holds.

Logs, builds, and health checks

A useful Node deployment surface gives you:

This is platform behavior, not application behavior. The Node app stays a Node app.

Custom domains and TLS

Once the app is live on a tavin.app URL, you usually want it on a real domain. The pattern most platforms have converged on:

  1. Add a domain to the project.
  2. Add a CNAME or A record in your DNS provider.
  3. The platform issues a TLS certificate via Let’s Encrypt and renews it automatically.

You should never have to handle a private key by hand for a basic public service.

When AI agents enter the picture

Coding agents — Claude Code, Cursor, Codex, and others — change the deployment story for Node apps. The agent can already read your repo, edit code, run tests, and explain a diff. The remaining gap is shipping that code to production.

The naive solution is to give the agent a long-lived platform API key. That works once and ages badly: the key has too much authority and is hard to revoke without breaking the user’s session.

The better path is a narrow deployment surface — exactly what we argued in the deployment-surface post. For tavin.cloud:

The Node app does not change. The way agents touch it does.

Choosing a host: rough mental model

Render, Railway, Fly.io, Vercel, and tavin.cloud all deploy Node apps from GitHub. The differences worth caring about:

We have side-by-side comparisons for tavin.cloud vs Render, vs Railway, vs Vercel, and vs Fly.io. They are written from our perspective; read them with that bias in mind.

If your app is mostly serverless-shaped (short request handlers, no background work), serverless may still be the right default. If it is service-shaped — long-lived process, queue worker, daemon, ML service, or anything where you would rather think in containers than functions — a repo-to-container PaaS is usually simpler.

A reasonable 2026 default

For a new Node service that you want on a public URL by the end of the day:

That is the path tavin.cloud is built around: Railpack for the common case, Dockerfile for the explicit case, and an agent-safe surface so coding agents and humans can ship the same Node app through the same primitives.

If you want to try it, the quickstart is the shortest version.