Run an application
Choose a deployment layout
Give application code and runtime files a predictable location and owner instead of running the project from a root home directory.
Before we copy any code to the server, let’s decide where it lives and who owns it. Most first deployments skip this. The app ends up in /root/myapp, running as root, with uploads mixed into the source tree. It works until the first rollback, and then nothing is where you need it.
The rule I follow: the person who deploys is not the process that runs. Our deploy user installs releases through sudo. A separate service account called notes-app runs the application. It has no password and no login shell, so it can’t be used to get into the server.
Create the account and the directories
sudo adduser --system --group --home /var/lib/notes-app notes-app
sudo install -d -m 755 -o deploy -g notes-app /srv/notes-app
sudo install -d -m 755 -o deploy -g notes-app /srv/notes-app/releases
sudo install -d -m 750 -o notes-app -g notes-app /var/lib/notes-app
--system creates a low-numbered account with no login. install -d creates a directory with the mode, owner and group in one command.
Each path has one job:
/srv/notes-app/releases/ versioned application code
/srv/notes-app/current symlink to the active release
/var/lib/notes-app/ persistent application data
/etc/notes-app.env production configuration and secrets
The service needs to read and execute the active release. It needs to write only in /var/lib/notes-app. It must not own Nginx config, systemd units or anything else on the system.
Prove the boundary
Check the account exists and test what it can write:
id notes-app
namei -l /srv/notes-app/releases
sudo -u notes-app test -w /etc && echo unexpected
sudo -u notes-app test -w /var/lib/notes-app && echo writable
The third command prints nothing. If you see unexpected, the account has far more power than it should. The fourth prints writable.
Why releases and a symlink
Each deploy goes into its own directory, named after the Git commit. Then current points at it:
sudo -u deploy ln -sfn /srv/notes-app/releases/4f92c1a /srv/notes-app/current
readlink -f /srv/notes-app/current
ln -sfn replaces the symlink in one step. readlink -f prints the directory it points at, so you can confirm what’s live.
When a release breaks, you point current back at the previous directory and restart the service. That’s a code rollback in seconds. Notice what it doesn’t undo: a database migration, changed uploads or a rotated secret. Those need their own plans, and we’ll get to them.
The mistake to avoid is letting the app write inside its release directory. Uploads and SQLite files end up next to the code, the next deploy doesn’t have them, and the rollback you counted on becomes a data loss. Keep state in /var/lib/notes-app.
Create the account and directories, run both permission tests, and write down which of the four paths belong in a backup. Hint: not the ones you can rebuild from Git.
Lesson completed