Skip to main content

Directory layout

This document defines where code, virtual environments, configuration, and logs live on the Ubuntu API server. Paths follow the Filesystem Hierarchy Standard (FHS) and common production practice for systemd-managed Python services.

Design principles

  1. Separate code from secrets — Application source is plain git checkout; credentials never live in the repo.
  2. Separate code from runtime — Virtual environments are not committed and live outside the app tree.
  3. Least privilege — A dedicated unprivileged user runs all APIs; root is only used for install and systemd/Nginx changes.
  4. Localhost-only apps — Each API listens on 127.0.0.1:<port>. Only Nginx is reachable from the network.
  5. Predictable naming — One canonical <api-name> slug used everywhere (directories, systemd unit, env file, Nginx filename).

<api-name> naming convention

Use lowercase kebab-case matching the repository or product name.

GoodAvoid
customer-apiCustomerAPI
report-apiapi1
document-apicustomer_api (underscores)

The same slug appears in:

  • /opt/apis/<api-name>/
  • /opt/venvs/<api-name>/
  • /etc/<api-name>/
  • /etc/systemd/system/<api-name>.service
  • /etc/nginx/sites-available/<api-name> (or one file per subdomain)
  • /var/log/<api-name>/ (if file logging is used)

Full directory map

/
├── opt/
│ ├── apis/ # Application source trees
│ │ └── <api-name>/ # Git checkout; one directory per API
│ │ ├── app/ # (example) Python package — layout varies by repo
│ │ ├── requirements.txt
│ │ └── README.md
│ │
│ └── venvs/ # Virtual environments (not in git)
│ └── <api-name>/ # Created with python3 -m venv
│ ├── bin/
│ │ ├── python
│ │ ├── pip
│ │ └── uvicorn # or gunicorn
│ └── lib/

├── etc/
│ ├── <api-name>/ # Per-API host configuration
│ │ ├── <api-name>.env # Secrets and environment (mode 600)
│ │ └── README # Optional: documents non-secret env vars
│ │
│ ├── systemd/system/
│ │ └── <api-name>.service # systemd unit
│ │
│ └── nginx/
│ ├── sites-available/
│ │ └── <api-name> # Server block for this API's subdomain
│ └── sites-enabled/
│ └── <api-name> -> ../sites-available/<api-name>

└── var/
└── log/
└── <api-name>/ # Optional; systemd journal is default
└── app.log

Why these locations (industry rationale)

/opt/apis/<api-name>/ — application code

FHS defines /opt for add-on application software. Custom API deployments commonly use /opt/<vendor-or-app>/ or /opt/apps/. Using /opt/apis/ keeps all Python services under one parent and avoids mixing them with system packages in /usr.

Alternatives (not used here):

PathTypical use
/srv/<api-name>/FHS "data for services"; common for web content, less common for Python app code
/var/www/<api-name>/Legacy Apache convention; still seen but less ideal for non-PHP APIs
/home/<user>/apps/Acceptable for single-developer boxes; weaker for shared ops ownership

/opt/venvs/<api-name>/ — virtual environments

Keeping venvs outside the git tree:

  • Prevents accidental commit of lib/ and bin/
  • Allows git clean / redeploy without destroying the environment
  • Makes dependency upgrades explicit (pip install -r requirements.txt in the known venv path)

Some teams use .venv inside the app directory; that works for development but the split layout scales better when multiple APIs share one server and one deploy playbook.

/etc/<api-name>/ — configuration and secrets

FHS /etc is for host-specific configuration. Storing <api-name>.env here (not in /opt/apis/) ensures:

  • Secrets survive git pull / directory replace
  • File permissions can be locked down independently of code ownership
  • systemd can load env via EnvironmentFile= with a stable path

Do not store secrets in:

  • The git repository
  • /opt/apis/<api-name>/.env (too easy to overwrite on deploy unless explicitly excluded)

/etc/systemd/system/ — service units

Standard location for administrator-defined units on systemd systems. Package-provided units live in /lib/systemd/system/; local overrides belong here.

/etc/nginx/sites-available/ — reverse proxy

Debian/Ubuntu Nginx convention: define in sites-available, enable via symlink in sites-enabled. SSL certificates are referenced from these blocks (exact cert paths depend on your existing cert layout).

/var/log/<api-name>/ — optional file logs

By default, systemd captures stdout/stderr in the journal (journalctl -u <api-name>). Add file logging under /var/log/<api-name>/ only if an API or compliance requirement needs log files on disk.

Service account

Create one shared service user for all APIs on this host:

FieldValue
Usernameapisvc
Home/var/lib/apisvc (or /nonexistent with nologin shell)
Shell/usr/sbin/nologin
PurposeRuns all API processes; owns code and venv directories
sudo useradd --system --home-dir /var/lib/apisvc --create-home \
--shell /usr/sbin/nologin apisvc

When to use per-API users instead: Strict isolation between untrusted apps, different teams with separate sudo boundaries, or compliance requiring separate UIDs. For a single-org internal server, one apisvc user is standard and simpler.

Ownership and permissions

Apply after creating directories for each API. Replace <api-name> with the actual slug.

PathOwnerModeNotes
/opt/apis/root:apisvc775Group write — deploy users in apisvc can git clone
/opt/apis/<api-name>/apisvc:apisvc755Entire git checkout; g+rwX after clone for git pull
/opt/venvs/root:apisvc775Parent directory
/opt/venvs/<api-name>/apisvc:apisvc755Created by apisvc or root then chown
/etc/<api-name>/root:apisvc750Only root and apisvc read secrets
/etc/<api-name>/<api-name>.envroot:apisvc640Secrets; group-readable by apisvc
/var/log/<api-name>/apisvc:apisvc755If used

Deploy user (e.g. gqc) runs git clone and git pull. That user must be in the apisvc group and /opt/apis must be mode 775. Without this, clone fails with “Permission denied” — do not use sudo git clone.

sudo usermod -aG apisvc gqc
# Log out and SSH back in

sudo mkdir -p /opt/apis
sudo chown root:apisvc /opt/apis
sudo chmod 775 /opt/apis

Verify after re-login:

groups          # must include apisvc
touch /opt/apis/.write-test && rm /opt/apis/.write-test

After clone: sudo chown -R apisvc:apisvc /opt/apis/<api-name> and sudo chmod -R g+rwX /opt/apis/<api-name> so future git pull works.

Full Git/deploy-key setup: 07-code-management-and-git.md.
Users, docker group, and Docker install: 08-docker-install-and-users.md.

Port allocation

Assign one localhost port per API. Document assignments in the root README.md to avoid collisions.

API (example)PortNotes
First API8001Initial deployment
Second API8002Increment by one
Third API8003...

Bind address is always 127.0.0.1, never 0.0.0.0.

Initial server setup (one time)

Run on the Ubuntu server as root or with sudo:

# Parent directories — 775 so gqc (apisvc group) can git clone
sudo mkdir -p /opt/apis /opt/venvs
sudo chown root:apisvc /opt/apis /opt/venvs
sudo chmod 775 /opt/apis /opt/venvs

# Service user (skip if already exists)
sudo useradd --system --home-dir /var/lib/apisvc --create-home \
--shell /usr/sbin/nologin apisvc 2>/dev/null || true

# Deploy login user (example: gqc)
sudo usermod -aG apisvc gqc
# Log out and SSH back in

Per-API directory creation is covered in 02-adding-a-new-api.md.

Relationship to Nginx and firewall

Internet / LAN


Nginx :443 / :80 ← only these ports allowed in UFW (when enabled)

├── 127.0.0.1:8001 ← API 1 (not in UFW)
├── 127.0.0.1:8002 ← API 2
└── 127.0.0.1:8003 ← API 3

Nginx configuration is not under /opt/; it lives in /etc/nginx/ as described above. See 04-nginx-reverse-proxy.md.

Azure and external secrets

APIs that use Azure Storage should read credentials from /etc/<api-name>/<api-name>.env:

AZURE_STORAGE_CONNECTION_STRING=...
# or
AZURE_CLIENT_ID=...
AZURE_CLIENT_SECRET=...
AZURE_TENANT_ID=...

Never commit these values. Rotate by editing the env file and restarting the service: sudo systemctl restart <api-name>.

Docker (future / parallel path)

When APIs run in containers, /opt/venvs/ is not used. Secrets stay in /etc/<api-name>/. Nginx still proxies to 127.0.0.1:<port>. See 05-docker-strategy.md.

Quick reference for technicians

I need to…Location or command
Find application code/opt/apis/<api-name>/
Activate the venvsource /opt/venvs/<api-name>/bin/activate
Edit secretssudo nano /etc/<api-name>/<api-name>.env
Edit systemd unitsudo nano /etc/systemd/system/<api-name>.service
View logsjournalctl -u <api-name> -f
Restart APIsudo systemctl restart <api-name>
Edit Nginx/etc/nginx/sites-available/<api-name>