Run an application
Create a service and reverse proxy
Run the application with systemd and let Nginx handle public HTTP and HTTPS while forwarding to the private local port.
Starting the app by hand from an SSH session is fine for a test. It’s not how it runs in production. Close the terminal and the process dies. Reboot and nothing comes back. We want systemd, the process manager Ubuntu already uses for everything else, to own the application.
Systemd starts the app at boot, runs it as the notes-app user, loads the environment file, collects its output in the journal, and restarts it when it crashes.
The unit file
Create /etc/systemd/system/notes-app.service with sudoedit and put this in it:
[Unit]
Description=Notes application
After=network.target
[Service]
Type=simple
User=notes-app
Group=notes-app
WorkingDirectory=/srv/notes-app/current
EnvironmentFile=/etc/notes-app.env
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5s
TimeoutStopSec=30s
[Install]
WantedBy=multi-user.target
Every line refers to something we set up earlier: the account, the current symlink, the environment file. Restart=on-failure restarts a crashed process after five seconds. WantedBy=multi-user.target is what makes it start at boot.
ExecStart needs the absolute path to node. Run command -v node to find yours, and use the app’s real entry file. A service doesn’t get your shell’s PATH or your nvm setup.
Validate, enable, check
sudo systemd-analyze verify /etc/systemd/system/notes-app.service
sudo systemctl daemon-reload
sudo systemctl enable --now notes-app
sudo systemctl status notes-app --no-pager
sudo journalctl -u notes-app -n 50 --no-pager
curl --fail http://127.0.0.1:3000/health
verify catches typos in the unit. daemon-reload makes systemd read the new file. enable --now starts the service and registers it for boot in one step. The status should say active (running), the journal should show the app’s startup lines, and the health check should return 200. Get this green before touching Nginx.
Put Nginx in front
Now Nginx becomes a reverse proxy: it accepts the public HTTPS request and forwards it to the app on 127.0.0.1:3000. Open the server block from the HTTPS lesson and replace the static location / with this:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
The proxy_set_header lines pass along the original hostname, the client IP and the scheme. Without them the app thinks every request comes from 127.0.0.1 over plain HTTP.
Validate and reload, then test the public URL:
sudo nginx -t
sudo systemctl reload nginx
curl -I https://notes.example.com
You should get 200 OK, served by your app through Nginx over TLS.
Reading a 502
502 Bad Gateway means Nginx tried to reach the app and got nothing usable. The app is down, or on another port. Check systemctl status notes-app, the journal, sudo ss -lntp and the port in the unit file against the one in proxy_pass. Don’t open port 3000 in UFW to work around it. The port is private on purpose.
To roll back code, point /srv/notes-app/current at the previous release and sudo systemctl restart notes-app. To roll back a unit or Nginx file, restore the last working copy, validate it, then reload.
Pick a quiet moment and reboot the Droplet with sudo reboot. Reconnect, run systemctl is-active notes-app, and hit the public health endpoint. If both work, the server survives a restart without you. The systemd course covers units in much more depth.
Lesson completed