megacolorboy

Abdush Shakoor's Weblog

Writings, experiments & ideas.

List Plesk hosted sites and PHP handlers

If you need a quick inventory of the sites hosted on a Plesk server, plesk db is useful to have around. I find it especially handy when checking which PHP handler each site uses.

From an administrative shell on the Plesk server, run:

plesk db -e "SELECT d.name AS Domain, h.php AS PHP_ENABLED, h.php_handler_id AS PHP_Handler FROM domains d JOIN hosting h ON h.dom_id = d.id ORDER BY d.name ASC;"

An illustrative result might look like this:

+---------------------+-------------+-----------------+
| Domain              | PHP_ENABLED | PHP_Handler     |
+---------------------+-------------+-----------------+
| example.com         | true        | plesk-php83-fpm |
| staging.example.com | true        | plesk-php82-fpm |
+---------------------+-------------+-----------------+

The query joins domain names to their hosting settings. Plesk uses the same tables and handler field in its PHP handler management examples.

Because this is an inner join with hosting, it lists domains with hosting records. It isn't an inventory of every alias or forwarding-only domain. Also, a handler ID identifies the configured handler, not its exact installed PHP patch version.

To narrow the list to domains whose database status is 0 (active), add a WHERE clause:

plesk db -e "SELECT d.name AS Domain, h.php AS PHP_ENABLED, h.php_handler_id AS PHP_Handler FROM domains d JOIN hosting h ON h.dom_id = d.id WHERE d.status = 0 ORDER BY d.name ASC;"

For example, if only example.com from the first listing is active, the result would be:

+-------------+-------------+-----------------+
| Domain      | PHP_ENABLED | PHP_Handler     |
+-------------+-------------+-----------------+
| example.com | true        | plesk-php83-fpm |
+-------------+-------------+-----------------+

That status filter reflects Plesk's domain status; it doesn't test whether the website responds successfully.

If you only need a count of those active hosted domains:

plesk db -e "SELECT COUNT(*) AS Active_Hosted_Domains FROM domains d JOIN hosting h ON h.dom_id = d.id WHERE d.status = 0;"

All three queries only read the database, which makes them handy for a quick inventory before reviewing site settings.

Hope you found this useful!

Fix Plesk installer download failures with HTTPS

While updating Plesk on an Ubuntu server, I came across an error that didn't make much sense for an existing installation:

ERROR: No products are installed yet.
Use the 'install' command to install one first.

The installer logs gave me a more useful clue:

Can't process versions.inf3:
Failed to download the file http://autoinstall.plesk.com/versions.inf3

The installer was trying to download its repository metadata over HTTP. I checked whether the HTTPS endpoint was reachable from the same server:

curl -ILv --connect-timeout 10 --max-time 20 https://autoinstall.plesk.com/versions.inf3

Here, -I requests response headers, -L follows redirects, and -v shows connection details. The timeouts keep the check from hanging indefinitely.

HTTPS was reachable. That suggested a problem with the HTTP request path, although it didn't establish whether a firewall, proxy, or another network condition was responsible.

I then ran the installer as root with an explicit HTTPS package source:

plesk installer \
  --source https://autoinstall.plesk.com \
  --select-release-latest \
  --upgrade-installed-components

The --source option overrides the package source for this run, as described in the Plesk installer documentation. The remaining options select the latest release and update installed components; see the Plesk update documentation.

This command performs an update, so run it during your planned maintenance window with a current backup. In a multiline shell command, each backslash must be the final character on its line.

Using HTTPS got the installer past the metadata download failure. If you see the same initial error, check the installer logs first—the failed download was the clue that mattered here.

Hope you found this useful!

Fix Kubernetes NFS mount failures on Ubuntu workers

While deploying an application to staging, I noticed its pod was stuck in ContainerCreating. Restarting it didn't help, so I checked the pod's events:

kubectl describe pod <pod_name> -n <namespace>

Replace the placeholders with your pod name and namespace. The relevant error was:

MountVolume.SetUp failed for volume:
mount failed: exit status 32

Mounting command: mount
Mounting arguments: -t nfs ...

Output:
mount: bad option; for several filesystems
(e.g. nfs, cifs) you might need a
/sbin/mount.<type> helper program.

I initially suspected the PVC or NFS server, but the missing helper message pointed me toward the worker node.

To see which node the pod was assigned to:

kubectl get pod <pod_name> -n <namespace> -o wide

In my case, it was a newly installed Ubuntu worker. For this NFS volume setup, the mount was performed on the node, before the application container could start.

After logging into that worker, I checked for the helper:

which mount.nfs

There was no output. That suggested the helper wasn't available on the command search path; checking the installed package was the next step:

dpkg-query -W nfs-common

On Ubuntu, nfs-common provides the NFS client utilities. The Ubuntu NFS guide covers client installation.

The package was missing on this worker, so I installed it there:

sudo apt update
sudo apt install -y nfs-common

Once the utilities were installed, the pod started successfully after I restarted it. Kubernetes also retries failed volume mounts, so a restart isn't always necessary.

The useful lesson for me was to include the NFS client utilities in the setup for every worker that can run these workloads. Installing them inside the application image wouldn't fix this node-level mount failure.

Hope you found this useful!

Fix FOR loop percent signs in Azure DevOps pipelines

One day, a vendor told me they couldn't push updates to staging because the release pipeline was failing while extracting ZIP artifacts.

The error looked like this:

i" -C "...\extracted" was unexpected at this time.
Cmd.exe exited with code '255'.

At first glance, the command looked fine:

for /r "drop" %i in (*.zip) do tar -xf "%i" -C "drop\extracted"

The part I'd missed was where the command was running. On Windows, Azure DevOps Command Line tasks put inline scripts into a temporary .cmd file. The Microsoft task documentation describes this behavior.

Windows CMD uses %i for a FOR variable in an interactive session, but %%i in a batch script. That applies both when declaring the variable and when using it inside the loop.

So the corrected script was:

if not exist "drop\extracted" mkdir "drop\extracted"
for /r "drop" %%i in (*.zip) do tar -xf "%%i" -C "drop\extracted"

Here, drop is relative to the task's working directory. This assumes the Windows agent has a tar executable that supports ZIP extraction and that the output directory is dedicated to these artifacts. Archives with matching filenames can overwrite each other's extracted files.

After changing both occurrences to %%i, the deployment succeeded.

It was a small difference, but one I'll remember the next time a command works in a terminal and fails in a pipeline.

Hope you found this useful!

Fix Kubernetes initialization with subPath

While deploying a .NET Core application to production, I came across an error that hadn't appeared in staging:

System.InvalidOperationException: Fresh setup refused: existing private files, keys or uploads requires operator recovery.

At first, I suspected a permissions issue. The application needed persistent directories for uploads, encryption keys, and configuration, with a separate PVC for each one. This was the container's mount configuration:

volumeMounts:
  - name: uploads
    mountPath: /var/lib/app/storage
  - name: keys
    mountPath: /var/lib/app/keys
  - name: configuration
    mountPath: /var/lib/app/configuration

These are mount entries from the workload, not a complete manifest. Each name refers to a volume backed by its corresponding PVC.

The vendor confirmed that the directories had to be empty during initial setup. To inspect them, I temporarily replaced the container's startup command with sleep and opened a shell inside it.

Listing the storage directory revealed an unexpected entry:

ls -la /var/lib/app/storage

Alongside . and .., there was a lost+found directory. The other two mounts had one too.

Filesystems such as ext4 typically create lost+found during initialization for filesystem recovery. So a freshly provisioned PVC doesn't necessarily expose an empty directory. In this case, the application treated that entry as existing application data and refused to initialize.

The fix was to mount a dedicated subdirectory using subPath:

volumeMounts:
  - name: uploads
    mountPath: /var/lib/app/storage
    subPath: app-data
  - name: keys
    mountPath: /var/lib/app/keys
    subPath: app-data
  - name: configuration
    mountPath: /var/lib/app/configuration
    subPath: app-data

Each of the three separate PVCs then had this layout:

PVC root/
├── lost+found/
└── app-data/

The application now saw only app-data at each mount path, without the lost+found entry. After restoring the normal startup command, initialization succeeded.

The Kubernetes volume documentation explains how subPath selects a directory within a volume. Make sure that directory has the permissions required by the container's runtime user. If you reuse one PVC for several mounts, use different subdirectories when the data needs to stay separate.

For an existing deployment, back up the data and plan the move first: files at the PVC root won't automatically appear inside the new subdirectory.

Hope you found this useful!

Create, extract, and inspect tar archives

Whenever I need to bundle a directory before moving it between servers, tar is a useful tool to have around. I thought I'd write down the few commands I keep coming back to.

Let's say you have an uploads directory in your current location. To create a gzip-compressed archive:

tar -czf uploads.tar.gz uploads/

Here, -c creates the archive, -z uses gzip compression, and -f specifies the archive filename.

If the directory is somewhere else, you can use -C to choose the source directory without storing its entire path:

tar -czf uploads.tar.gz -C /var/www/html/public uploads/

These are alternative ways to create the same archive. Replace the paths with your own and choose the one that suits your situation.

Before extracting it, you can inspect the contents:

tar -tzvf uploads.tar.gz

For example, a listing might look like this:

drwxr-xr-x devops/devops       0 2026-09-20 09:00 uploads/
-rw-r--r-- devops/devops    2048 2026-09-20 09:00 uploads/logo.png
-rw-r--r-- devops/devops    4096 2026-09-20 09:00 uploads/banner.jpg

The -t flag lists entries, and -v adds details such as permissions and sizes.

To extract into your current directory:

tar -xzvf uploads.tar.gz

Or extract into a specific directory, creating it first if needed:

mkdir -p ./restored
tar -xzvf uploads.tar.gz -C ./restored

This puts the files under ./restored/uploads/. The -x flag extracts the archive, and verbose output might look like this:

uploads/
uploads/logo.png
uploads/banner.jpg

I find it helpful to list the contents first so I know which directory structure to expect. Extracting into an empty directory also helps avoid overwriting existing files.

Hope you found this useful!

Test API CORS headers using curl

When troubleshooting an API request from a browser, I find it useful to inspect the CORS headers directly with curl. It makes it easier to see what the server is returning.

For a cross-origin JSON POST request with an authorization header, start by simulating the browser's preflight request:

curl -i -X OPTIONS 'https://api.example.com/endpoint' \
  -H 'Origin: https://example.com' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: content-type, authorization'

Replace both URLs with your API endpoint and frontend origin. An illustrative successful response could look like this:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: content-type, authorization
Vary: Origin

Next, test the actual POST request:

curl -i 'https://api.example.com/endpoint' \
  -H 'Origin: https://example.com' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  --data '{"message":"Testing CORS"}'

Use a suitable test endpoint and payload: this sends a real POST request. Replace the token if authentication is required, or remove that header if it isn't.

Notice that the Access-Control-Request-* headers belong to the preflight. The actual POST carries the content type and authorization header themselves.

An example POST response might be:

HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://example.com
Vary: Origin

{"message":"Request received"}

Check the CORS headers on both responses. A successful preflight alone isn't enough if the actual response is missing the required headers.

For browser requests made with credentials, such as cookies using credentials: 'include', the server must allow the specific origin and return Access-Control-Allow-Credentials: true; a wildcard origin won't work for those requests.

Finally, curl displays the response but doesn't enforce CORS like a browser does. Confirm the behavior in your browser too. The MDN CORS guide explains how the browser evaluates these responses.

Hope you found this tip useful!

View a Kubernetes ConfigMap using kubectl

When checking an application's configuration, I often want to see what is actually stored in its ConfigMap. You can get that straight from the terminal.

First, list the ConfigMaps in your namespace:

kubectl get configmaps -n staging

Then view the one you're interested in as YAML:

kubectl get configmap web-config -n staging -o yaml

Replace web-config and staging with your own values. Here's an illustrative response with some metadata omitted:

apiVersion: v1
kind: ConfigMap
metadata:
  name: web-config
  namespace: staging
data:
  APP_ENV: staging
  LOG_LEVEL: info
  API_BASE_URL: https://api.example.com

If you only need a single value, JSONPath keeps the output small:

kubectl get configmap web-config -n staging -o jsonpath='{.data.LOG_LEVEL}{"\n"}'

For the example above, that prints:

info

One thing to remember: this shows the ConfigMap stored in Kubernetes. It doesn't prove that the application is already using its latest values.

Values injected as environment variables require a pod restart to pick up changes. Mounted ConfigMap files generally update eventually, but subPath mounts don't receive those updates, and the application may still need to reload the file. The ConfigMap documentation explains these differences.

ConfigMaps are intended for non-confidential configuration. Passwords and tokens belong in Secrets or your existing secrets-management system.

Hope you found this useful!