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
postgressuperuser with a password (unlike Homebrew on Mac, which is often passwordless for your OS user). Use that password inDATABASE_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 startupprints asudo ... launchd ...command — paste and run it. - Windows:
pm2 startupprints instructions to install a Windows startup/service hook — run what it prints (often needs an Administrator terminal). Thenpm2 saveagain 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 buildin 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:
- Simplicity — one config file per app, one command to run them all. No Dockerfiles, images, or volumes to maintain.
- Lightweight — the apps run directly on the computer's Node. No container overhead eating RAM/CPU on a laptop.
- Fast iteration — change code,
pnpm run pm2:reload, done. No rebuilding images. - Shared local Postgres — every app just points
DATABASE_URLatlocalhost:5432. No container networking to wire up. - 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.