Where to put apps on a VPS: the /opt/apps directory layout
A simple convention for app code on a VPS: keep each app in its own folder under /opt/apps, chown the parent folder once, and git clone, git pull and .env edits never need sudo again.
On this page
- Quick reference
- Step by step
- Step 1: Create the parent folder /opt/apps (once per VPS)
- Step 2: Clone a new app (no sudo from now on)
- Step 3: The resulting layout (the full picture from the root /)
- FAQ
- Why /opt instead of ~/apps?
- ls ~ does not show /opt/apps?
- Several people administering the VPS (optional)
- Where the related pieces live
- Troubleshooting
When you clone an app onto a VPS to run it (with Docker), which directory is the
production-grade choice that does not force a sudo on every git pull? This guide
settles on one convention: put each app in /opt/apps/<app-name>, chown the parent
folder exactly once, and from then on clone/pull/edit .env without sudo.
Throughout, anything written as <...> is a value you fill in yourself:
<username>- the regular (non-root) user you created on the VPS, see Set up a new VPS. Example:alice.<repo-url>- the git URL of the app to deploy. Example:[email protected]:<your-user>/<your-repo>.git.<app-name>- the app folder name, which is simply the repo name thatgit clonecreates for you (no need to pick one up front). For example the repomy-api.gitcreates the foldermy-api.
Quick reference
# 1. Do this ONCE (for the life of the VPS): create /opt/apps owned by your user
sudo install -d -o <username> -g <username> /opt/apps
# 2. Every new app - NO sudo, NO need to name the folder first:
cd /opt/apps
git clone <repo-url> # creates /opt/apps/<app-name> from the repo name
cd /opt/apps/<app-name>
# 3. Check (note: /opt/apps is NOT inside ~, you have to ls /opt)
ls -ld /opt/apps # owner must be <username>, mode 755NEVER chown the whole of /opt. Docker has already created /opt/containerd (owned
by root) in there. Chown /opt/apps only.
Step by step
Step 1: Create the parent folder /opt/apps (once per VPS)
/opt belongs to root by default, so you cannot clone straight into it without sudo.
The cleanest fix is to create a parent folder /opt/apps and hand its ownership to your
user exactly once:
sudo install -d -o <username> -g <username> /opt/appsinstall -d=mkdir+ set owner/group + mode 755, all in one command.- chown (change owner) = change who owns the folder. Here
/opt/appsgoes to<username>so you never need sudo for it later. - mode 755 = the permission set: the owner (you) can read/write/execute; other users can only read and enter, not modify.
- Replace
<username>with your regular user (for example the one created in Set up a new VPS).
Step 2: Clone a new app (no sudo from now on)
Because /opt/apps is now yours, git clone creates the subfolder from the repo name
on its own - no naming it first, no sudo:
cd /opt/apps
git clone <repo-url> # e.g. creates /opt/apps/my-api
cd /opt/apps/<app-name>Create the .env file right inside this app folder, then run docker compose up -d as
usual.
Step 3: The resulting layout (the full picture from the root /)
/opt/apps and your home (/home/<username>, i.e. ~) are two separate branches
off the root /. That is why ls ~ will not show your apps:
/ <- filesystem root (two separate branches below)
+-- home/
| +-- <username>/ <- ~ (your home) - apps do NOT live here
| +-- <your-folders>/ <- your other personal folders
+-- opt/
+-- apps/ <- OWNED by <username>: clone/pull with NO sudo
| +-- <app-name>/
| | +-- .git/
| | +-- Dockerfile
| | +-- docker-compose.yml
| | +-- .env <- create it right here
| | +-- src/ ...
| +-- <app-2>/
+-- containerd/ <- Docker's (root): do NOT touchFAQ
Why /opt instead of ~/apps?
Once the chown is done, the day-to-day workflow in /opt/apps and ~/apps is
identical (neither needs sudo). /opt wins because:
- Production convention - under the FHS (Filesystem Hierarchy Standard, the
standard directory layout on Linux),
/optis where add-on application software belongs. Whoever takes over or administers the server later will look for apps in/opt, not dig through/home. - Not tied to a personal account - if that user is deleted or renamed one day, the app is not stuck inside their home.
If you prefer ~/apps, that is technically perfectly fine - you lose nothing. This is
a convention choice, not right versus wrong.
ls ~ does not show /opt/apps?
That is expected (see the tree in Step 3). /opt/apps starts with /, so it lives at
the filesystem root (/opt), a different tree from your home (/home/<username>).
To see it:
ls /opt # or
cd /opt/appsSeveral people administering the VPS (optional)
With multiple admins, instead of giving /opt/apps to a single user, create a deploy
group and turn on setgid so new files always inherit the group:
sudo groupadd -f deploy
sudo usermod -aG deploy <username>
sudo install -d -o root -g deploy -m 2775 /opt/apps # 2775 = setgid
# log out and back in for the group to take effect- setgid (the leading
2in2775): every file/folder created in/opt/appsautomatically inherits thedeploygroup, so any admin in the group can read/modify it - no manual permission fixes. - Adding someone later:
sudo usermod -aG deploy <new-user>(then that user logs out and back in).
Where the related pieces live
This guide only covers where the app code goes. Everything else has its own place:
- Caddy reverse proxy: kept separate, NOT inside an app folder. It depends on how
you run it: Caddy on the host (installed via apt) reads
/etc/caddy/Caddyfile; Caddy in Docker keeps its config in~/services/caddy/(see the Caddy reverse proxy guide). Either way, Caddy is infrastructure you set up once, so it does not belong in/opt/apps. - Database: use Postgres already on the host or bundle it in Docker - see Set up PostgreSQL on a VPS and SSH tunnel to the database from your local machine.
- Stateful data (db, file uploads): prefer a Docker named volume (Docker manages
it under
/var/lib/docker/volumes, and it survives container rebuilds) or a bind mount (map a folder such as./datain the app straight into the container). Never let data live inside the container, because a rebuild wipes it.
Troubleshooting
| Symptom | Fix |
|---|---|
git clone in /opt/apps fails with Permission denied | Step 1 was skipped or the owner is wrong. Run ls -ld /opt/apps: the owner must be <username>. If not, run sudo chown <username>:<username> /opt/apps (this folder only, never the whole of /opt) |
ls ~ does not show the app | Expected - apps live in /opt/apps, not in your home. Use ls /opt/apps |
Added the user to the deploy group but still cannot write | Group membership only applies after that user logs out and back in |