Vũ Văn HảiFull-stack · AI-native
GuidesBlog
Discuss a project

© 2026 Vu Van Hai · Written from real deployment experience.

HomeGuidesBlogRSS
  1. Guides
  2. /VPS
  3. /Where to put apps on a VPS: the /opt/apps directory layout

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.

Updated: Sep 21, 20266 min read
VPSDockerGitUbuntu
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 that git clone creates for you (no need to pick one up front). For example the repo my-api.git creates the folder my-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 755

NEVER 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/apps
  • install -d = mkdir + set owner/group + mode 755, all in one command.
  • chown (change owner) = change who owns the folder. Here /opt/apps goes 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 touch

FAQ

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), /opt is 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/apps

Several 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 2 in 2775): every file/folder created in /opt/apps automatically inherits the deploy group, 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 ./data in the app straight into the container). Never let data live inside the container, because a rebuild wipes it.

Troubleshooting

SymptomFix
git clone in /opt/apps fails with Permission deniedStep 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 appExpected - apps live in /opt/apps, not in your home. Use ls /opt/apps
Added the user to the deploy group but still cannot writeGroup membership only applies after that user logs out and back in
PreviousServe media via Cloudflare CDN + Backblaze B2 (free egress)

Related articles

  • Roll back a Git commit and update safely on a VPS

    Take your project back to an older commit, force push to the remote, then reset the VPS to match without a merge conflict, then rebuild the service and clear caches.

    Dev tools

    Dev tools
  • Automatic maintenance page with Caddy during Docker deploys

    Use Caddy's handle_errors to catch the 502 Bad Gateway that appears while a container rebuilds and serve a static maintenance page automatically, with no manual on/off switch.

    VPS

    VPS
  • Set up Caddy as a reverse proxy on a VPS with Docker

    Run Caddy as a Dockerized reverse proxy on a fresh VPS: create a shared network, write the Caddyfile and docker-compose file, get HTTPS automatically, then add new domains with a single zero-downtime reload.

    VPS

    VPS

Written by Vu Van Hai

I'm Hai, a full-stack developer based in Ho Chi Minh City. These guides come from systems I built and run myself. Need to build or untangle something similar? Get in touch.

Discuss a projectMore guides

Spot a mistake or a command that no longer works? Let me know

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