Skip to content
EducationLLM ready file

Local development setup guide for newsrooms that want to be local first

AI For Newsroom

A simple, free setup for running multiple apps (web apps + Telegram bots) on your own computer — no cloud bill, no Docker complexity. One database server for everything, and one process manager that keeps your apps alive.

Published Aug 27, 2026

Run your side projects locally: PostgreSQL + PM2

A simple, free setup for running multiple apps (web apps + Telegram bots) on your own computer — no cloud bill, no Docker complexity. One database server for everything, and one process manager that keeps your apps alive.

This is the exact setup I use for all my projects during development. Works on macOS and Windows (install steps differ; the architecture is the same).


The idea in one picture

Your computer (always on)
│
├── PostgreSQL 16  ──►  one server, one database per project
│                       (webapp, shop, bot, ...)
│
└── PM2  ──────────►  keeps every app running in the background
                        (web apps + Telegram bots), restarts on crash,
                        survives closing the terminal
  • PostgreSQL = where your data lives.
  • PM2 = what keeps your apps running.

Two tools, and your whole local stack is online 24/7 on your own machine.


Part 1 — Why local PostgreSQL?

When you're building, you don't need a paid cloud database yet. A single Postgres server on your laptop gives you:

  • Free & fast — no network latency, no usage limits.
  • One server, many projects — each project gets its own database, fully isolated, zero conflicts.
  • Real Postgres — same engine as Supabase/Neon/Railway, so moving to production later is easy.

Important limitation: this database only listens on localhost. It is not reachable from the internet or from an app deployed on Replit/a VPS. Use it for local development and apps you run on your own machine. For deployed apps that other people use, get a hosted DB (Supabase, Neon, Railway).

Setup — macOS (Apple Silicon)

# 1. Install Postgres (Homebrew)
brew install postgresql@16

# 2. Make it always-on (starts on login, restarts on crash)
brew services start postgresql@16

# 3. Put Postgres 16 on your PATH
echo 'export PATH="/opt/homebrew/opt/postgresql@16/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

# 4. Create one database per project
createdb webapp
createdb shop
createdb bot

By default Homebrew's Postgres uses your macOS username with no password for local connections — perfect for development.

Handy commands (macOS):

brew services list        # is Postgres running?
psql -l                   # list all databases
psql webapp               # open a database
createdb my_new_project   # add a database for a new project

Setup — Windows

Same idea: one always-on Postgres service, one database per project.

# 1. Install Postgres 16 (pick one)
winget install PostgreSQL.PostgreSQL.16
# or download the installer: https://www.postgresql.org/download/windows/

# During install: set a password for the `postgres` user and keep port 5432.
# Add "Command Line Tools" / bin to PATH when the installer offers it
# (or add C:\Program Files\PostgreSQL\16\bin manually).

# 2. Confirm the Windows service is running
# Services app → "postgresql-x64-16" → Running
# Or in an elevated PowerShell:
Get-Service -Name postgresql*

# 3. Open a new terminal, then create one database per project
createdb -U postgres webapp
createdb -U postgres shop
createdb -U postgres bot
# You'll be prompted for the postgres password you set at install.

Handy commands (Windows):

psql -U postgres -l              # list databases
psql -U postgres -d webapp       # open a database
createdb -U postgres my_project  # new project DB

Windows note: the installer usually creates a postgres superuser with a password (unlike Homebrew on Mac, which is often passwordless for your OS user). Use that password in DATABASE_URL.

Connect an app

Put this in each project's .env (pointing at that project's own database):

macOS (typical):

DATABASE_URL="postgresql://<your-mac-username>@localhost:5432/webapp"

Windows (typical):

DATABASE_URL="postgresql://postgres:<your-password>@localhost:5432/webapp"

That's it — the app talks to its own database on the shared server.


Part 2 — Why PM2?

During development you run apps with npm run dev. But that dies the moment you close the terminal. PM2 fixes this:

  • Keeps apps running in the background — close the terminal, they stay up.
  • Auto-restart on crash — if a bot throws, PM2 brings it back.
  • Runs many apps at once — web apps and Telegram bots, all managed together.
  • One command to rule them all — start / reload / stop everything at once.
  • Survives reboot (once configured) — apps come back after you restart the computer.

Setup

# Install PM2 globally
npm install -g pm2

Each project describes its processes in an ecosystem.config.cjs file. Example (a web app + a Telegram bot):

// ecosystem.config.cjs
const path = require("path");

module.exports = {
  apps: [
    {
      name: "myapp-web",
      cwd: __dirname,
      script: "npm",
      args: "run start",       // or "run dev"
      env: { NODE_ENV: "production", PORT: 4783 },
      autorestart: true,
      max_restarts: 10,
    },
    {
      name: "myapp-telegram",
      cwd: __dirname,
      script: path.join(__dirname, "node_modules/.bin/tsx"),
      args: "telegram/bot.ts",
      interpreter: "none",
      autorestart: true,
      max_restarts: 10,
    },
  ],
};

Run one app

pm2 start ecosystem.config.cjs   # start this project's processes
pm2 reload all                   # reload after code changes
pm2 stop all                     # stop
pm2 list                         # see what's running
pm2 logs                         # tail logs

Run all your projects with one command

If you have several projects, add a tiny workspace script so you can control them all at once.

macOS / Git Bash / WSL — scripts/pm2-all.sh

#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"

ECOSYSTEMS=(
  "$ROOT/webapp/ecosystem.config.cjs"
  "$ROOT/shop/ecosystem.config.cjs"
  "$ROOT/bot/ecosystem.config.cjs"
)

case "${1:-}" in
  start)
    for eco in "${ECOSYSTEMS[@]}"; do pm2 startOrReload "$eco" --update-env; done
    pm2 save && pm2 list ;;
  reload) pm2 reload all --update-env && pm2 list ;;
  stop)   pm2 stop all && pm2 list ;;
  status) pm2 list ;;
  *) echo "Usage: $0 <start|reload|stop|status>"; exit 1 ;;
esac

Root package.json (macOS / Git Bash):

{
  "scripts": {
    "pm2:start":  "bash scripts/pm2-all.sh start",
    "pm2:reload": "bash scripts/pm2-all.sh reload",
    "pm2:stop":   "bash scripts/pm2-all.sh stop",
    "pm2:status": "bash scripts/pm2-all.sh status"
  }
}

Windows PowerShell — scripts/pm2-all.ps1

param(
  [Parameter(Mandatory = $true)]
  [ValidateSet("start", "reload", "stop", "status")]
  [string]$Action
)

$Root = Split-Path -Parent $PSScriptRoot
$Ecosystems = @(
  "$Root\webapp\ecosystem.config.cjs",
  "$Root\shop\ecosystem.config.cjs",
  "$Root\bot\ecosystem.config.cjs"
)

switch ($Action) {
  "start" {
    foreach ($eco in $Ecosystems) { pm2 startOrReload $eco --update-env }
    pm2 save
    pm2 list
  }
  "reload" { pm2 reload all --update-env; pm2 list }
  "stop"   { pm2 stop all; pm2 list }
  "status" { pm2 list }
}

Root package.json (Windows):

{
  "scripts": {
    "pm2:start":  "powershell -ExecutionPolicy Bypass -File scripts/pm2-all.ps1 start",
    "pm2:reload": "powershell -ExecutionPolicy Bypass -File scripts/pm2-all.ps1 reload",
    "pm2:stop":   "powershell -ExecutionPolicy Bypass -File scripts/pm2-all.ps1 stop",
    "pm2:status": "powershell -ExecutionPolicy Bypass -File scripts/pm2-all.ps1 status"
  }
}

Same daily commands on both OSes (from your dev folder):

pnpm run pm2:start     # start every project
pnpm run pm2:reload    # after you change code
pnpm run pm2:stop      # stop everything
pnpm run pm2:status    # see everything

Survive a reboot (one-time)

pnpm run pm2:start
pm2 save            # remember the current app list
pm2 startup         # prints an OS-specific command — run it once
  • macOS: pm2 startup prints a sudo ... launchd ... command — paste and run it.
  • Windows: pm2 startup prints instructions to install a Windows startup/service hook — run what it prints (often needs an Administrator terminal). Then pm2 save again if asked.

After this: closing the terminal keeps apps running, and a reboot restores them automatically.

Daily workflow after you change code

pnpm run pm2:reload

Apps running a production build (next start) need a rebuild first: npm run build in that project, then reload. Apps in dev mode reload as-is.


Part 3 — PM2 vs Docker: what's the difference?

Both keep apps running, but they solve different problems.

| | PM2 | Docker | |---|---|---| | What it is | A process manager for Node apps | A container platform that packages the whole environment | | What it manages | Your running Node processes | Isolated containers (OS libs, runtime, app, sometimes the DB too) | | Setup effort | npm i -g pm2, one config file | Dockerfiles, images, volumes, networks, compose files | | Resource use | Very light | Heavier (each container ships its own environment) | | Isolation | Shares your machine's Node/OS | Fully isolated per container | | Best for | Local dev, small always-on apps on one machine | Reproducible deploys, teams, complex multi-service systems, "works on every machine" | | Learning curve | Minutes | Hours/days |

Why we chose PM2 (browser-based apps) for these projects

Our projects are browser-based web apps + Telegram bots running on one developer's machine during development. For that, PM2 is the right tool:

  1. Simplicity — one config file per app, one command to run them all. No Dockerfiles, images, or volumes to maintain.
  2. Lightweight — the apps run directly on the computer's Node. No container overhead eating RAM/CPU on a laptop.
  3. Fast iteration — change code, pnpm run pm2:reload, done. No rebuilding images.
  4. Shared local Postgres — every app just points DATABASE_URL at localhost:5432. No container networking to wire up.
  5. Good enough uptime — auto-restart on crash + restore on reboot is exactly what a solo dev needs locally.

When we'd switch to Docker: when an app goes to production on a server, needs to run identically across many machines/teammates, or has many interdependent services. Docker shines for deployment and reproducibility; PM2 shines for simple local always-on. For the build phase, PM2 keeps us fast and cheap.


TL;DR

macOS

brew install postgresql@16 && brew services start postgresql@16
npm install -g pm2
createdb my_project
# DATABASE_URL="postgresql://<mac-username>@localhost:5432/my_project"

Windows

winget install PostgreSQL.PostgreSQL.16
npm install -g pm2
createdb -U postgres my_project
# DATABASE_URL="postgresql://postgres:<password>@localhost:5432/my_project"

Every day (both)

pnpm run pm2:start     # bring everything online
pnpm run pm2:reload    # after code changes
pnpm run pm2:status    # check health

Free, local, always-on. Move to a hosted DB + Docker only when you actually deploy.