Run an application
Install the runtime and application
Use a supported runtime source, deploy a pinned application revision, install production dependencies, and test it on localhost first.
Our notes application is a Node.js app, so we need Node on the server. Install the version the project declares, using Node’s current official Ubuntu instructions or a package source whose update behavior you understand.
One habit I want you to pick up: never pipe an installer script from an old blog post into a root shell without reading it first. Look at where it comes from and what it changes. On your own laptop that’s sloppy. On a public server it’s how you get someone else’s software running as root.
Check the tools
node --version
npm --version
git --version
Compare the Node version with the project’s engines field in package.json. The command proves what is installed. It doesn’t prove the app supports it.
Deploy a pinned commit
Each release gets its own directory named after the commit, inside the releases folder from the previous lesson:
cd /srv/notes-app/releases
git clone https://github.com/your-account/notes-app.git 4f92c1a
cd 4f92c1a
git checkout 4f92c1a
npm ci
git rev-parse HEAD
Use your real repository and commit hash. The last command prints the full hash, so you know exactly what code is on this server.
npm ci installs from the lockfile and fails if it disagrees with package.json. That failure is useful. It means the lockfile in Git is stale. Fix it on your laptop and push. Don’t run npm install on the server to make it go away, because then production runs dependency versions nobody reviewed.
If the app needs a build step, run it here, then npm prune --omit=dev to drop the build-only packages. If a CI system already built the artifact, install just the production dependencies with npm ci --omit=dev.
Let the service read it
The release belongs to deploy. The notes-app account needs to read and execute it, nothing more:
sudo chown -R deploy:notes-app /srv/notes-app/releases/4f92c1a
sudo chmod -R g=rX,o= /srv/notes-app/releases/4f92c1a
g=rX gives the group read access, plus execute on directories only. o= removes every permission for everyone else.
Run it once by hand
Before systemd gets involved, start the app manually as the service account, bound to localhost:
sudo -u notes-app env HOST=127.0.0.1 PORT=3000 npm start
From a second SSH session, check the endpoint and the bind address:
curl --fail --show-error http://127.0.0.1:3000/health
sudo ss -lntp | grep ':3000'
The health request returns a 200 body. The ss line must show 127.0.0.1:3000, not 0.0.0.0:3000. A 0.0.0.0 bind means the app would accept the whole internet if the firewall ever slipped. Stop the process with Ctrl-C when you’re done.
The usual failures here are a Node version that’s too old, a dependency that was only in devDependencies, or a start script that assumes another working directory. Fix the release. Never open port 3000 in UFW to “see if it works”.
Record the commit hash, the Node version, the npm ci result, the health response and the bind address. Keep the previous release directory around when you deploy the next one. That’s your rollback.
Lesson completed