Escena Viva now configures itself properly and now says what it is doing: structured logs with per-request traceability, metrics on /metrics, /health/live and /health/ready probes. But all of that is still started by hand. Somebody SSHes into the server, types npm start, watches the first log lines go by and closes the terminal. And at that moment the application dies. This lesson solves the process problem: who starts it, who watches it, who brings it back when it falls over and who reloads it when you deploy a new version without cutting a single purchase in half. The tool will be PM2, but the concepts are what matter: they apply just the same if tomorrow you use systemd or an orchestrator.
Contents
- The problem:
npm startin a terminal - What a process supervisor is and which one to pick
- PM2: installation and reference commands
- The Escena Viva ecosystem file
- PM2 cluster mode versus our
src/cluster.js reloadversusrestart: zero-downtime reloads- Automatic restarts and loop protection
- Memory restarts as a safety net
- Starting on machine boot
- PM2 logs and their rotation
- Environment variables and secrets in PM2
- Deployment and monitoring
- What PM2 does not solve
- The problem:
npm start in a terminal
npm start in a terminalWhen you run npm start in an SSH session, the Node process is a child of your shell, and your shell is a child of the SSH daemon. When you disconnect, the kernel sends SIGHUP to the session and the whole tree collapses. Your API disappears. The classic workarounds — nohup npm start &, screen, tmux — solve only that part, and leave five problems untouched:
- If the process falls over with an
uncaughtException(which, remember, in 11-02 we deliberately decided terminates the process), nobody brings it back. - If the server reboots, the application does not come back on its own.
- The logs go to an endless file that will fill the disk.
- There is no way to deploy without downtime: stopping and starting leaves a gap of seconds full of 502s.
- There is no visibility: how many processes there are, how much memory they use, how many times they have restarted.
That is exactly what a supervisor does.
- What a process supervisor is and which one to pick
A supervisor is a program whose only job is keeping other programs alive: it starts them, watches them, restarts them if they exit, redirects their output and survives the end of your session.
| Supervisor | What it is | For | Against | When to pick it |
|---|---|---|---|---|
| PM2 | A supervisor written in Node, specific to Node applications | Built-in cluster, zero-downtime reloads, comfortable CLI, terminal dashboard | It is one more process that can fail; Node-specific | A VPS or your own server with one or a few Node applications |
| systemd | The system init on Linux | Already installed, integrated with boot, journald, cgroups |
No cluster of its own (you need src/cluster.js or unit templates), dry syntax |
A Linux server where you want zero extra dependencies |
Docker + restart: always |
The container engine watches the container | The environment travels with the app; one mechanism for everything | It does not spread processes inside the container: one per container | When you already package with Docker (lesson 11-04) |
| Orchestrator (Kubernetes, ECS, Nomad) | Supervises containers across a fleet of machines | Scaling, probes, progressive rollout, self-healing | High complexity and operational cost | Many services, several teams, real scale |
| PaaS (Heroku, Render, Fly) | The provider supervises for you | You manage nothing | Less control, pay per use | Small teams that want to move fast (lesson 11-05) |
For Escena Viva on a VPS, PM2 is the reasonable choice: it gives you clustering and zero-downtime reloads without writing a line of systemd. In 11-04 and 11-05 we will see that as soon as you containerize, the supervisor becomes somebody else — and that overlap is normal, not a design flaw.
- PM2: installation and reference commands
# Global installation on the server (it is not a project dependency).
npm install -g pm2
pm2 --versionPM2 is installed globally, not as a dependency. Putting it in dependencies is a common mistake: PM2 starts your application, it is not part of it, and folding it in complicates the Docker image in the next lesson. On the first command, PM2 starts a background daemon (the God daemon) that outlives your session and keeps the list of applications. The CLI commands talk to that daemon.
| Command | What it does |
|---|---|
pm2 start ecosystem.config.js --env production |
Starts whatever the ecosystem file defines |
pm2 list |
Table with status, CPU, memory and restarts per application |
pm2 logs escena-viva-api --lines 100 |
Follows the logs in real time |
pm2 monit |
Interactive terminal dashboard: CPU, memory and logs per process |
pm2 describe escena-viva-api |
Full record: paths, variables, uptime, restarts, PID |
pm2 restart <app> |
Stops and starts (there is downtime) |
pm2 reload <app> |
Rolling zero-downtime reload (cluster mode) |
pm2 stop <app> |
Stops without removing it from the list |
pm2 delete <app> |
Removes it from the PM2 list |
pm2 save |
Saves the current list so it can be restored on machine boot |
pm2 startup |
Generates the systemd service that starts PM2 at boot |
pm2 flush |
Empties the log files |
pm2 scale escena-viva-api 4 |
Changes the number of instances on the fly |
A note on style: although pm2 start src/server.js -i 4 works, on a real server you never start it that way. The configuration would live only in the bash history of whoever typed it. Everything goes into the ecosystem file.
- The Escena Viva ecosystem file
Escena Viva has two processes to deploy, and this comes straight from Module 10: the HTTP API and the BullMQ queue consumer (src/processes/ticket-consumer.js), which produces the ticket PDFs and the emails. They are two different applications, with different resource profiles, and they must scale separately: during the Festival de Jazz opening you may need more consumers without needing more API.
// ecosystem.config.js
'use strict';
module.exports = {
apps: [
{
name: 'escena-viva-api',
script: './src/server.js',
// 0 = as many instances as there are available cores.
instances: 0,
exec_mode: 'cluster',
// --- Startup and graceful shutdown ---
wait_ready: true, // Waits for process.send('ready'), not for 'listen'.
listen_timeout: 10000, // If 'ready' does not arrive in 10 s, it counts as failed.
kill_timeout: 15000, // Grace after SIGINT before SIGKILL: the M6 drain.
// --- Restart policy ---
autorestart: true,
max_restarts: 10,
min_uptime: '30s',
exp_backoff_restart_delay: 200,
max_memory_restart: '700M',
// --- Logs: pino already writes JSON to stdout (11-02) ---
out_file: '/var/log/escena-viva/api.log',
error_file: '/var/log/escena-viva/api.error.log',
merge_logs: true,
time: false, // Do NOT prepend a timestamp: it breaks the JSON.
// --- Per-environment configuration (NON-secret values only) ---
env: {
NODE_ENV: 'development',
PORT: 3000,
LOG_LEVEL: 'debug',
},
env_production: {
NODE_ENV: 'production',
PORT: 3000,
LOG_LEVEL: 'info',
TRUST_PROXY: 'true',
},
},
{
name: 'escena-viva-consumer',
script: './src/processes/ticket-consumer.js',
// The consumer does NOT listen on a port: there is no port to share.
instances: 2,
exec_mode: 'fork',
wait_ready: false,
kill_timeout: 30000, // Larger grace: it may be generating a PDF.
autorestart: true,
max_restarts: 10,
min_uptime: '60s',
exp_backoff_restart_delay: 500,
max_memory_restart: '900M', // The PDF worker threads use more.
out_file: '/var/log/escena-viva/consumer.log',
error_file: '/var/log/escena-viva/consumer.error.log',
merge_logs: true,
time: false,
env: {
NODE_ENV: 'development',
QUEUE_CONCURRENCY: 2,
},
env_production: {
NODE_ENV: 'production',
QUEUE_CONCURRENCY: 5,
},
},
],
};The decisions behind it:
exec_mode: 'cluster'for the API only. Cluster mode exists to share a port between several processes. The consumer does not listen on any port: BullMQ distributes the work through Redis. Clustering there would add nothing, so it runs infork.instances: 0for the API, a fixed2for the consumer. The API scales with the cores; the consumer scales with the queue load and with the memory the PDF-generating worker threads consume (Module 10). They are different axes.- A different
kill_timeoutfor each. The API drains HTTP requests: 15 seconds is plenty. The consumer may be halfway through a PDF: 30 seconds keeps you from killing it mid-job. time: false. It is subtle and very important: if PM2 prepends its own timestamp to each line, pino's JSON stops being valid JSON and the aggregator cannot index it. With structured logging, PM2 must limit itself to redirecting.env_productioncontains not a single secret. We will come back to this in section 11.
You start it with:
Without --env production, PM2 uses the env block, that is, development. It is one of the most common ways to end up with NODE_ENV=development on a production server, with all the consequences we saw in 11-01.
- PM2 cluster mode versus our
src/cluster.js
src/cluster.jsIn Module 10 we wrote src/cluster.js by hand: a primary process that forks N workers with node:cluster, supervises them, replaces them if they die and performs a rolling zero-downtime reload. It works. PM2 in exec_mode: 'cluster' does exactly the same: it uses Node's cluster module underneath, with the same mechanism for sharing the socket between child processes. It is not magic and it is not a different technology.
Our own src/cluster.js |
PM2 cluster mode | |
|---|---|---|
| Port sharing | node:cluster |
node:cluster (identical) |
| Replacing dead workers | You maintain it | Included |
| Rolling reload | You wrote it | pm2 reload |
| Changing N instances on the fly | No, you have to restart | pm2 scale |
| Memory restart | You would have to implement it | max_memory_restart |
| Per-worker metrics | You would have to implement it | pm2 monit, pm2 describe |
| External dependency | None | PM2 installed and running |
| Inside a container | Fine, and it is the usual choice | Redundant: the orchestrator already scales |
What you gain: you stop maintaining and testing infrastructure code that is not your business, and you get hot scaling, memory restarts and monitoring for free. What you lose: an external dependency that has to be installed and updated on the server, and less fine-grained control over each worker's lifecycle.
And why writing it by hand was not wasted time: because you now understand that instances: 4 does not create four threads but four processes with separate memory — which is why sessions and rate limiting had to move to Redis in Module 10 — that the Sequelize pool is multiplied by four (and that is why src/db/sequelize.js sizes it according to the worker count), and that one process's in-memory state does not exist for the other three. Somebody who has never written a cluster by hand configures instances: 16 and then wonders why PostgreSQL runs out of connections. In production with PM2, src/cluster.js falls out of use: script points at src/server.js and PM2 supplies the cluster. The file stays in the repository as an alternative for environments where PM2 is absent (inside a container, for instance, though it is rarely needed there either).
reload versus restart: zero-downtime reloads
reload versus restart: zero-downtime reloadsThis is the main reason to use PM2 on your own server. pm2 restart: it kills every process and starts new ones. Between the two there is a gap of one to three seconds during which nobody is listening on the port. Everything arriving in that gap is a 502.
pm2 reload (cluster mode only): it replaces workers one at a time. It starts the new one, waits until it is ready, takes the old one out of the connection rotation, gives it time to finish what it has in hand and kills it. Somebody is always listening. Zero errors for the user.
sequenceDiagram
participant PM2
participant W1 as Worker 1 (old)
participant W1n as Worker 1 (new)
participant W2 as Worker 2 (old)
PM2->>W1n: fork with the new code
W1n-->>PM2: process.send('ready')
PM2->>W1: SIGINT
W1->>W1: stops accepting, drains requests
W1-->>PM2: exit(0)
PM2->>PM2: repeats with worker 2
Note over W2: keeps serving the whole time
For this to really work you need three coordinated pieces, and the first two have existed since Module 6. 1. Graceful shutdown in src/server.js. PM2 sends SIGINT (not SIGTERM) to clustered processes. Our handler already listens for both signals: it stops accepting connections, sets state.acceptingTraffic = false so /health/ready returns 503, closes idle connections with closeIdleConnections, waits for the active ones and exits with a grace timer.
2. The ready signal. With wait_ready: true, PM2 does not consider the worker alive until the worker says so explicitly:
// src/server.js (startup excerpt)
async function startServer() {
await connectDatabase();
await getRedisClient().ping();
const app = createApplication(dependencies);
const server = http.createServer(app);
await new Promise((resolve) => server.listen(configuration.port, resolve));
logger.info(
{ port: configuration.port, environment: configuration.nodeEnv, pid: process.pid },
'server listening'
);
// We tell PM2 ONLY when everything is genuinely ready:
// database connected, Redis responding and port listening.
if (process.send) {
process.send('ready');
}
return server;
}The difference is enormous. Without wait_ready, PM2 accepts the worker as soon as the process exists, and retires the old one while the new one is still connecting to PostgreSQL: the first users see errors. With wait_ready, the replacement window only opens once the new worker can genuinely serve. Note the if (process.send): it only exists when the process has been forked by another one, so starting directly with node src/server.js still works. 3. A sufficient kill_timeout. It is the time PM2 waits from SIGINT to SIGKILL. It must be longer than the longest drain your application expects. If you have requests that take up to 10 seconds, a kill_timeout of 5000 kills responses halfway through and all the Module 6 work goes to waste. Our 15 seconds on the API cover the worst case with room to spare.
And listen_timeout is the flip side: if ready does not arrive within 10 seconds, PM2 assumes startup has failed. It protects against the scenario where a new version cannot connect to the database and hangs forever, leaving the deployment half-done.
- Automatic restarts and loop protection
With autorestart: true, if a process exits, PM2 starts another. Good. But imagine the new version has a startup bug: an environment variable is missing and src/config/index.js calls process.exit(1) (11-01). PM2 restarts it. It fails again. It restarts it. In a loop, hundreds of times a minute, burning CPU and filling the disk with logs. Three parameters prevent it:
| Parameter | What it does | Value in Escena Viva |
|---|---|---|
min_uptime |
Minimum alive time for a startup to count as "good" | 30s (API), 60s (consumer) |
max_restarts |
Consecutive restarts below min_uptime before giving up |
10 |
exp_backoff_restart_delay |
Growing delay between attempts (200, 400, 800 ms...) | 200 (API), 500 (consumer) |
With this combination, a process that dies a second after starting is retried ten times with a growing wait and then moves to the errored state, where it stays. PM2 stops insisting and waits for human intervention, which is the right thing to do: the application is broken and retrying will not fix it. And a warning that is worth the whole lesson: restarting without fixing the cause is papering over a problem. If pm2 list shows 3,400 restarts in the ↺ column, you do not have a resilient system: you have a bug that shows up every few minutes and a supervisor hiding it. Check that column every time you look. A number that grows is a pending investigation, not a reassurance.
- Memory restarts as a safety net
max_memory_restart: '700M' restarts the process when it exceeds that usage. It is useful, but you have to be clear about what it is and what it is not.
What it is: a safety net. Faced with a slow memory leak — the kind Module 9 taught us to diagnose with heap snapshots — it keeps the process from growing until the kernel kills it out of memory (the OOM killer) or until garbage collection works all day and the p99 goes through the roof. What it is not: a solution. If the process restarts on memory every two hours, you have a leak and it needs finding. The restart buys time to investigate, it does not close the matter.
To choose the value: look at the steady usage in production with pm2 monit and set it 60-80% above that. Too low and you will restart for no reason under heavy load; too high and the safety net never triggers. Also bear in mind that the limit is per process: with 4 instances at 700 MB you need 2.8 GB for the API alone, plus the consumer. That arithmetic becomes critical inside the container in the next lesson.
- Starting on machine boot
A supervisor that does not survive a server reboot solves half the problem. Two commands:
# 1. Generates and installs the systemd service that starts PM2 at boot.
# It prints a sudo command that you must run verbatim.
pm2 startup
# 2. Saves the CURRENT application list so it can be restored at boot.
pm2 saveThe order matters and forgetting is a classic: pm2 startup makes PM2 start, but with no application list. It is pm2 save that writes the dump (~/.pm2/dump.pm2) that PM2 restores. If you change the ecosystem file and do not run pm2 save again, the old configuration will come back after the next server reboot. Rule: after any change to the applications, pm2 save. A curious and healthy note: underneath, pm2 startup creates a systemd unit. That is, systemd supervises PM2 and PM2 supervises your applications. If that strikes you as one layer too many, the intuition is correct, and it is part of the argument for moving to containers in the next lesson.
- PM2 logs and their rotation
PM2 captures each process's stdout and stderr and writes them to the files given in out_file and error_file. Since pino already writes structured JSON (11-02), PM2 must only redirect: no added timestamps, no prefixes.
out_file: '/var/log/escena-viva/api.log',
error_file: '/var/log/escena-viva/api.error.log',
merge_logs: true, // All instances to the same file: they already carry pid inside.
time: false, // No prefix: keeps the JSON valid line by line.merge_logs: true puts the four instances into one file. It might look confusing, but every pino JSON line already carries pid and requestId, so splitting by file adds nothing and complicates querying.
Without rotation, those files grow until they fill the disk, and a full disk takes down the application and the database. The PM2 module that solves it:
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 14 # 14 rotated files
pm2 set pm2-logrotate:compress true # gzip the old ones
pm2 set pm2-logrotate:rotateInterval '0 0 * * *' # at midnightEven so, remember factor XI of the twelve factors (11-02): ideally the logs should not end up in files on the server but in an aggregator. A common pattern with PM2 is to let it write to files and put a lightweight agent (Vector, Fluent Bit, Promtail) in place to read those files and ship them. The files become a temporary buffer, not the final destination.
- Environment variables and secrets in PM2
The env and env_production blocks in the ecosystem file are convenient... and they are a risk, because that file is versioned. Putting JWT_ACCESS_SECRET there is exactly the failure of the public repository test from 11-01. Only non-secret values belong in the ecosystem file: NODE_ENV, PORT, LOG_LEVEL, TRUST_PROXY. Secrets arrive by another route. Three options, from least to most recommendable:
a) A .env file outside version control, loaded by dotenv. That is what src/config/index.js already does. On the server there is /opt/escena-viva/.env with 600 permissions, owned by the application user. Simple and sufficient for a VPS. b) systemd's EnvironmentFile in the unit generated by pm2 startup. The variables enter the PM2 daemon and every application inherits them. Less flexible if you have several applications with different secrets.
c) Exporting them before starting PM2, pulling them from a secret manager:
# In the deployment script, not in the repository.
export JWT_ACCESS_SECRET=$(vault kv get -field=jwt secret/escena-viva)
export SESSION_SECRETS=$(vault kv get -field=session secret/escena-viva)
pm2 reload ecosystem.config.js --env production --update-env--update-env is essential: PM2 remembers the environment an application started with and reuses it on restarts. Without that flag, you change a variable, reload, and it keeps running with the old value. It is one of the most frequent confusions with PM2, and it has cost plenty of people more than an hour of debugging. If you rotated a secret following the procedure in 11-01 and it appears to have had no effect, start here.
- Deployment and monitoring
PM2 includes an SSH-based deployment system, pm2 deploy, configured in the same file:
// ecosystem.config.js (additional block)
deploy: {
production: {
user: 'escena',
host: ['api1.escenaviva.test'],
ref: 'origin/master',
repo: '[email protected]:escena-viva/platform.git',
path: '/opt/escena-viva',
'post-deploy':
'npm ci --omit=dev && npm run migrate && pm2 reload ecosystem.config.js --env production --update-env',
},
},With pm2 deploy production it connects over SSH, runs git fetch and checkout, executes post-deploy and reloads without downtime. Notice the order: npm ci --omit=dev (the Module 5 payoff), migrations before the reload, and reload, never restart. It is a decent solution for a VPS and a small team. It has clear limits: it deploys from the repository on each machine, so every server builds on its own and can end up with slightly different dependencies. The alternative — building one artifact and promoting it — is what we will see with Docker in 11-04 and with the CI pipeline in 11-06. In the meantime, pm2 deploy works.
For day-to-day monitoring:
pm2 monit: an interactive dashboard with CPU and memory per process plus live logs. Useful during a deployment or a load spike like the Festival de Jazz opening.pm2 describe escena-viva-api: an application's full record. The first thing to look at when investigating: restart count, uptime, effective environment and log paths.pm2 list: the quick view. Keep an eye on the restart column (↺) and on memory.
These tools are for occasional inspection. Continuous monitoring is the 11-02 kind: metrics in Prometheus, dashboards in Grafana and alerts on symptoms. pm2 monit does not wake you up at night.
- What PM2 does not solve
And here comes the honest closing. PM2 solves the process, not the environment. PM2 guarantees your application is alive, restarts if it falls over, reloads without downtime and starts with the machine. All of that assuming the machine is correct. And that assumption is enormous:
- The Node version has to be the one
enginesdemands (>=24.5.0 <25). If the server has 20,require('node:...')may work and other things may not, and you will find out in production. - The system libraries have to be present:
bcryptcompiles against the system libc, the fonts for generating PDFs have to exist,libpqfor PostgreSQL. - The environment variables have to be set, with the right permissions and up to date.
- The user, the directories, the permissions and the log paths have to exist.
All of that is configured by hand on each server. And as soon as there are two servers, they start to diverge: one has a different OpenSSL version, on the other somebody installed something for debugging and never removed it. The result is the classic "it works on server A and not on server B", which is the grown-up version of "it works on my machine". The answer is to package the environment together with the application, so that what you deploy is not code running on an unknown machine but an artifact that carries its own filesystem inside. That is a container.
Common Mistakes and Tips
- Starting without
--env production. PM2 uses theenvblock (development). Always check withpm2 describewhichNODE_ENVis actually in use. - Forgetting
--update-envafter changing a variable. PM2 reuses the saved environment and your change has no effect. - Using
restartwherereloadwas called for. A two-second outage on every deployment, avoidable with one different letter. - A
kill_timeoutthat is too short. PM2 kills mid-response and cancels out the graceful shutdown from Module 6. - Leaving
time: truewith JSON logging. The prefix breaks the JSON and the aggregator cannot index anything. - Secrets in
env_production. They are versioned. It is the same mistake as 11-01 wearing different clothes. - Forgetting
pm2 saveafter changing the configuration. On the next server reboot the previous configuration comes back, sometimes months later, and nobody understands what happened. - Ignoring the restart column. A counter that grows is a bug hidden by the supervisor.
- Tip: run PM2 as an unprivileged user, never as root. And if the application needed port 80 (Module 4), put a reverse proxy in front instead of granting Node privileges.
- Tip: pin the PM2 version on the server and upgrade it deliberately. It is infrastructure, and a surprise upgrade during a sale makes for a bad night.
Exercises
Exercise 1 — Verify the zero-downtime reload
With the API running in cluster mode with four instances, fire autocannon (Module 10) at /api/events for 30 seconds and run pm2 reload escena-viva-api halfway through. Check that no error and no 502 appears. Then repeat with pm2 restart and compare.
Exercise 2 — Simulate a restart loop
Deliberately cause a startup failure (remove JWT_ACCESS_SECRET from the environment) and observe PM2's behavior with pm2 logs and pm2 list. Check that after max_restarts attempts the application ends up in the errored state. Document how long it takes to give up with exp_backoff_restart_delay: 200.
Exercise 3 — Scale the consumer
The ticket queue piles up 2,000 pending jobs during the Festival de Jazz early sales. Scale the consumer to six instances without stopping the API, verify the effect on the escenaviva_queue_pending metric from 11-02 and explain which other limit could be hit before adding more instances helps.
Solutions
Exercise 1.
pm2 start ecosystem.config.js --env production
npx autocannon -c 50 -d 30 http://localhost:3000/api/events &
sleep 10 && pm2 reload escena-viva-apiWith reload, the autocannon report shows non-2xx: 0 and errors: 0; in the latency you see at most a slight rise in the p99 while one fewer worker serves the traffic. With restart, dozens of connection-refused errors appear, all concentrated at the moment of the cut. The difference is entirely due to the three pieces in section 6: graceful shutdown, wait_ready and a sufficient kill_timeout. If you also see errors when testing with reload, check that process.send('ready') is genuinely being sent: without it, wait_ready: true would make startup time out.
Exercise 2. With exponential retries starting at 200 ms (200, 400, 800, 1600...), ten attempts add up to around 100 seconds before PM2 gives up. In pm2 logs you see the message from src/config/index.js repeated:
And there you can see the value of the validation from 11-01: the reason for the failure appears on the first line of every attempt, instead of a cryptic internal jsonwebtoken error three hours later. In pm2 list the final status is errored and the restart counter stops at 10.
Exercise 3.
The scaling is immediate and does not affect the API, because they are independent applications. escenaviva_queue_pending should fall with a slope roughly three times steeper. The limit you hit first is connections. Each consumer opens connections to Redis (BullMQ uses several per worker) and to PostgreSQL to record the issued tickets. With 6 consumers × concurrency 5, plus the 4 API instances with their pool, it is easy to exceed PostgreSQL's max_connections or Redis's client limit. It is the same arithmetic that forced us in Module 10 to size the Sequelize pool according to the worker count, and the one that will come back in 11-05 with the connection limits of managed plans. Adding instances without recomputing that product turns a performance problem into an outage.
Conclusion
Escena Viva no longer depends on an open SSH session. PM2 supervises it: two applications declared in a versioned ecosystem file — the API in cluster mode and the queue consumer in fork mode, each with its own resource policy — zero-downtime reloads resting on the graceful shutdown from Module 6 and on the ready signal we now emit, protection against restart loops, memory restarts as a safety net against leaks, automatic startup with the machine and rotated logs that do not break pino's JSON. We have also seen why writing src/cluster.js by hand in Module 10 is still valuable even though we no longer use it: understanding process-level distribution is what lets you configure instances without exhausting the database connections. But PM2 supervises the process, not the machine. You still need the server to have the exact Node version, the system libraries to compile bcrypt, the fonts for the PDFs and the right configuration — and you need the second server to be identical to the first, which never happens. In the next lesson, Packaging with Docker, we make the environment travel with the application: a multi-stage Dockerfile explained line by line, a non-root user, CMD in exec form so SIGTERM genuinely reaches the process, a HEALTHCHECK built on /health/ready, and a docker compose setup with the API, the consumer, PostgreSQL, MongoDB and Redis that will finally give you the complete development environment you have been asking for since Module 7.
Node.js Course: From Beginner to Advanced
Module 1: Introduction to Node.js
- What Is Node.js?
- Installing and Setting Up the Environment
- Your First Node.js Program
- The Node.js REPL
- Modern JavaScript for Node.js
- The Course Project: the Escena Viva Platform
Module 2: Core Concepts
- Node.js Architecture
- The Event Loop
- Callbacks and Asynchronous Programming
- Promises and async/await
- Events and EventEmitter
- CommonJS Modules and require()
- ES Modules and Interoperability
Module 3: File System and I/O
- Reading and Writing Files
- The fs Module in Depth
- Cross-Platform Paths with the path Module
- Working with Streams
- Transform Streams and pipeline
- Buffers and Binary Data
Module 4: HTTP and Web Servers
- Creating a Simple HTTP Server
- Handling Requests and Responses
- Manual Routing
- Serving Static Files
- Receiving Data: Request Bodies and JSON
- Consuming External APIs from Node.js
Module 5: NPM and Package Management
- Introduction to NPM and package.json
- Installing and Using Packages
- Semantic Versioning and package-lock
- npm Scripts and Project Automation
- Creating and Publishing Packages
- Dependency Security and Maintenance
Module 6: The Express.js Framework
- Introduction to Express.js
- Setting Up an Express Application
- Routing in Express
- Middleware
- Essential Third-Party Middleware
- Input Data Validation
- Error Handling
Module 7: Databases and ORMs
- Introduction to Databases
- Using MongoDB with Mongoose
- CRUD Operations
- Relationships, Population and Advanced Queries
- Using SQL Databases with Sequelize
- Migrations, Transactions and Seed Data
Module 8: Authentication and Authorization
- Introduction to Authentication
- User Registration and Password Hashing
- Sessions and Cookies with Passport.js
- Authentication with JWT
- Role-Based Access Control
- API Security Best Practices
Module 9: Testing and Debugging
- Introduction to Testing
- Unit Testing with Mocha and Chai
- Test Doubles with Sinon
- Integration Testing
- Coverage and Test Automation
- Debugging Node.js Applications
Module 10: Advanced Topics
- The Cluster Module
- Worker Threads
- Caching and Job Queues with Redis
- Performance Optimization
- Building RESTful APIs
- GraphQL with Node.js
Module 11: Deployment and DevOps
- Configuration and Environment Variables
- Logging and Monitoring in Production
- Using PM2 for Process Management
- Packaging with Docker
- Deploying to Heroku and Other PaaS
- Continuous Integration and Deployment
