Chapter 10: Dynamic Website Hosting (PHP, Python, Node.js with MySQL)
10.5 Node.js (Express) with MySQL
Install Node.js (LTS) from NodeSource
# Amazon Linux 2023 / CentOS Stream 9
curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -
sudo yum install -y nodejs git
# Ubuntu
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs git
node -v && npm -v
Alternative: nvm
nvm (Node Version Manager) installs Node per user without sudo and lets you switch versions: install it from the official nvm GitHub page, then nvm install --lts. On Amazon Linux 2023 you can also use the distribution packages (yum search nodejs).
Create the Express app
sudo mkdir -p /opt/nodeapp && sudo chown $USER:$USER /opt/nodeapp
cd /opt/nodeapp
npm init -y
npm install express mysql2 dotenv
cat > .env <<'EOF'
DB_HOST=127.0.0.1
DB_USER=appuser
DB_PASS=App@Pass123
DB_NAME=appdb
PORT=3000
EOF
chmod 600 .env
cat > app.js <<'EOF'
require("dotenv").config();
const express = require("express");
const mysql = require("mysql2/promise");
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
const pool = mysql.createPool({
host: process.env.DB_HOST, // use 127.0.0.1, not "localhost" (avoids IPv6 ::1 issue)
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
connectionLimit: 10,
});
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
app.get("/", async (req, res) => {
try {
const [rows] = await pool.query("SELECT id, name, city FROM students ORDER BY id");
const trs = rows.map(r => `<tr><td>${r.id}</td><td>${esc(r.name)}</td><td>${esc(r.city)}</td></tr>`).join("");
res.send(`<!DOCTYPE html><html><body style="font-family:Arial;max-width:700px;margin:30px auto">
<h1>Students (Node.js + Express + MySQL)</h1>
<table border="1" cellpadding="6"><tr><th>ID</th><th>Name</th><th>City</th></tr>${trs}</table>
<h3>Add student</h3>
<form method="post" action="/add"><input name="name" required placeholder="Name">
<input name="city" required placeholder="City"><button>Add</button></form></body></html>`);
} catch (err) {
res.status(500).send("Database error: " + esc(err.message));
}
});
app.post("/add", async (req, res) => {
await pool.execute("INSERT INTO students (name, city) VALUES (?, ?)", [req.body.name, req.body.city]);
res.redirect("/");
});
app.get("/api/students", async (req, res) => {
const [rows] = await pool.query("SELECT id, name, city FROM students");
res.json(rows);
});
app.get("/health", (req, res) => res.json({ status: "ok" }));
const port = process.env.PORT || 3000;
app.listen(port, "127.0.0.1", () => console.log(`API listening on 127.0.0.1:${port}`));
EOF
node app.js & # quick test
sleep 2 && curl -s http://127.0.0.1:3000/api/students
kill %1
Option A — keep it running with pm2
sudo npm install -g pm2
cd /opt/nodeapp
pm2 start app.js --name nodeapp
pm2 status
pm2 logs nodeapp --lines 20 # Ctrl+C to exit logs
pm2 startup systemd # prints a 'sudo env PATH=...' command - copy & run it
pm2 save # remember the current process list for reboot
Option B — run it as a systemd service
sudo tee /etc/systemd/system/nodeapp.service > /dev/null <<EOF
[Unit]
Description=Node.js Express app
After=network.target mariadb.service mysql.service
[Service]
User=$USER
WorkingDirectory=/opt/nodeapp
ExecStart=/usr/bin/node /opt/nodeapp/app.js
Restart=always
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo service nodeapp start
sudo systemctl enable nodeapp
Use either pm2 or systemd for one app, not both (they would fight for port 3000).