Deployment guide

Python 3.12

Overview

Python 3.12 on Ubuntu 24.04 LTS on Azure is a development workstation for writing, building, testing and packaging Python code. This guide covers deploying the SecureAnyCloud Azure Marketplace image and using it over SSH.

The image is a development workstation, not a server. It runs no network service of any kind. After first boot, the only listening socket reachable from outside the machine is SSH on port 22. There is no web console, no dashboard and no login page, so no credential of any kind ships in the image. There is nothing to log in to and nothing to rotate.

Ubuntu already includes a Python interpreter, so the value of this image is everything built around it. It answers the question that costs a Python developer the most time on a fresh machine: what happens when a package has no prebuilt wheel for this platform? On a stock cloud image, that ends in a compiler error. Here the package compiles, because the C, C++ and Fortran toolchains and the development headers that source-built packages look for are already installed and checked at build time.

The interpreter is deliberately Ubuntu's own. CPython 3.12.3 comes from noble's main component (package python3.12, version 3.12.3-1ubuntu0.15), which is carried in both noble-updates and noble-security. Canonical therefore publishes security fixes for this exact interpreter across the LTS support window. A newer 3.13 from a third-party archive would sit outside that coverage, and third-party archives have also been observed to pin their dependencies to exact versions in ways that silently block future security updates. The build stops if python3 -V is not exactly Python 3.12.3, or if any non-Ubuntu package repository has been configured.

Where your work lives. A dedicated 32 GiB data volume is attached at /srv/pydata. It carries the pip download and wheel cache, the pipx home, a directory for project virtual environments and a workspace. The cache location is set in /etc/pip.conf, which is pip's global configuration file, so it applies everywhere: an interactive shell, a non-interactive ssh host 'pip ...', a systemd unit, a cron job, and every virtual environment you create. You can grow or snapshot that volume independently of the operating system disk.

What is included

  • CPython 3.12.3 from Ubuntu main, with python3.12-dev, python3.12-venv and python3-pip, all at 3.12.3-1ubuntu0.15.
  • A complete native build toolchain: gcc, g++, gfortran, make, cmake, ninja and pkg-config, plus the development headers most often needed by source-built wheels: libffi, OpenSSL, zlib, bzip2, lzma, SQLite, readline, libxml2, libxslt, libjpeg, libpng, FreeType, libyaml, libcurl, libpq (PostgreSQL) and OpenBLAS.
  • ruff 0.16.0, a fast linter and formatter, and black 26.5.1.
  • python-lsp-server 1.14.0 (pylsp), so Neovim, Helix, Zed, Emacs or any other LSP client gives you completion, hover and navigation as soon as you connect over SSH.
  • IPython 9.15.0 for an interactive shell, pipx 1.16.3 for installing standalone command-line applications, virtualenv 21.7.0, and build 1.5.0 (pyproject-build) for producing wheels and source distributions.
  • A dedicated 32 GiB data volume at /srv/pydata holding the pip cache, the pipx home, venvs/ and workspace/, already warm with the wheels the guide uses.
  • sac-python-selftest, which compiles a real C extension from source, runs it, checks the number it computes, runs a test suite against it, and then compiles a third-party package from its source distribution.
  • sac-python-licence-audit, a fail-closed audit of every installed distribution's licence, and the resulting table shipped on the image.

Prerequisites

  • An Azure subscription with permission to create virtual machines.
  • An SSH key pair (ssh-keygen -t ed25519 if you do not already have one).
  • A network security group that allows inbound TCP 22 from your own address only.
  • The recommended size is Standard_B2s (2 vCPU, 4 GiB). Compiling large scientific packages from source benefits from more cores, so Standard_D4s_v5 or larger is a reasonable choice for that work.
1

Deploy from the Azure Portal

  • In the Azure Portal, select Create a resource and search the Marketplace for Python 3.12 on Ubuntu 24.04 LTS by SecureAnyCloud.
  • Select the offer, choose the plan, and select Create.
  • On Basics, pick your subscription, resource group and region, set the VM size to Standard_B2s, choose SSH public key authentication and paste your public key. The user name you enter here becomes the developer account.
  • On Disks, leave the attached 32 GiB data disk in place. It holds the caches and your workspace.
  • On Networking, restrict inbound SSH (22) to your own IP address. Do not open any other port, because this image serves nothing else.
  • Review and create.
2

Deploy from the Azure CLI

az vm image terms accept --urn sac1647283583153:python:default:latest
az vm create \
  --resource-group my-python-rg \
  --name my-python-vm \
  --image sac1647283583153:python:default:latest \
  --size Standard_B2s \
  --storage-sku StandardSSD_LRS \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_ed25519.pub \
  --public-ip-sku Standard
# then restrict inbound SSH to your own address
az vm open-port --resource-group my-python-rg --name my-python-vm --port 22 --priority 1001

Both commands run on your own machine, not on the VM.

3

First boot

A single one-shot systemd unit, python-firstboot.service, runs once on the first boot. The only thing that cannot be decided when the image is built is who you are, because the admin account name is chosen at VM creation time. First boot resolves that account, grants it access to the data volume, records the resolved environment and creates a sentinel file. It rotates no credentials, because this image has none.

Check that it completed:

systemctl is-active python-firstboot.service test -f /var/lib/sac/data/python-firstboot.done && echo "firstboot sentinel present"

Expected output:

active firstboot sentinel present

The environment it recorded is a plain, non-secret manifest:

cat /var/lib/sac/data/python-workspace.env
# sac Python workstation — resolved environment for this VM.
# Written once by python-firstboot.service. This file contains NO secrets: this image runs no
# network service, so there is nothing to authenticate against and nothing to rotate. PYTHON_VERSION=3.12.3 PYTHON_BIN=/usr/bin/python3 RUFF_VERSION=0.16.0 PIP_CACHE_DIR=/srv/pydata/cache/pip PIPX_HOME=/srv/pydata/pipx PIPX_BIN_DIR=/srv/pydata/pipx/bin PYVENVS=/srv/pydata/venvs PYWORKSPACE=/srv/pydata/workspace SAC_TOOLVENV=/opt/sac/pytools PYDEV_USER=azureuser PYDEV_GROUP=pydev PYDATA_MOUNTED=yes FIRSTBOOT_COMPLETED=2026-07-27T00:45:59Z
4

Connect and confirm the interpreter

ssh azureuser@<vm-ip>

The login banner summarises the machine. Confirm the interpreter and the packages that provide it:

python3 -V dpkg-query -W python3.12 python3.12-minimal python3.12-dev python3.12-venv

Expected output:

Python 3.12.3 python3.12 3.12.3-1ubuntu0.15 python3.12-dev 3.12.3-1ubuntu0.15 python3.12-minimal 3.12.3-1ubuntu0.15 python3.12-venv 3.12.3-1ubuntu0.15

All four come from Ubuntu main, which keeps them inside Canonical's security coverage. Confirm that the pip cache resolves onto the data volume even with a completely empty environment. This check proves the setting is global rather than in a login profile:

cat /etc/pip.conf env -i /usr/bin/python3 -m pip cache dir

Expected output:

# sac Python workstation — global pip configuration.
# The download/wheel cache lives on the dedicated data volume so it survives, is large, and
# is shared by every virtual environment on this VM. [global] cache-dir = /srv/pydata/cache/pip disable-pip-version-check = true /srv/pydata/cache/pip

And the tooling:

ruff --version black --version virtualenv --version pipx --version gcc --version | sed -n '1p'

Expected output:

ruff 0.16.0 black, 26.5.1 (compiled: yes) Python (CPython) 3.12.3 virtualenv 21.7.0 from /opt/sac/pytools/lib/python3.12/site-packages/virtualenv/__init__.py 1.16.3 gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
5

Verify the whole workstation in one command

sac-python-selftest is the fastest way to confirm that the machine works. It does more than print version banners. It creates a real virtual environment, has pip compile a C extension from source, imports the result and checks the number it computes. It then installs pytest with your own pip and runs a suite that cross-checks the C implementation against an independent pure-Python one. Finally, it compiles psutil from its source distribution to show that the toolchain also works on code SecureAnyCloud did not write.

sac-python-selftest

The run takes about a minute on a Standard_B2s. Every line begins with ok, and the output ends with:

sac-python-selftest: ALL CHECKS PASSED

Two switches are available: --offline skips everything that needs the package index, and --full is reserved for the longer build set used at image build time.

6

Where your code and caches live

findmnt /srv/pydata df -h /srv/pydata ls -l /srv/pydata

Expected output:

TARGET SOURCE FSTYPE OPTIONS /srv/pydata /dev/sdc ext4 rw,relatime Filesystem Size Used Avail Use% Mounted on /dev/sdc 31G 1.2G 29G 4% /srv/pydata total 24 drwxrwsr-x 3 root pydev 4096 Jul 27 00:38 cache drwxrwsr-x 3 root pydev 4096 Jul 27 00:38 pipx drwxrwsr-x 2 root pydev 4096 Jul 27 00:38 venvs drwxrwsr-x 2 root pydev 4096 Jul 27 00:38 workspace

The directories are group-owned by pydev with the set-group-id bit and a POSIX default ACL. First boot added your account to that group and granted it a direct ACL, so you can write to all of them without sudo. The ACL is applied to the top-level directories only, and everything beneath already inherits it. For that reason, first boot completes instantly instead of walking a large cache tree.

The fstab entry is keyed by filesystem UUID with nofail, so the volume comes back on every reboot and a missing disk never blocks boot. The disk is addressed by its Azure LUN path instead of a device letter, because device letters move between reboots.

pipx is configured to install standalone applications onto the same volume:

pipx environment --value PIPX_HOME pipx environment --value PIPX_BIN_DIR

Expected output:

/srv/pydata/pipx /srv/pydata/pipx/bin

Installing a command line application with pipx install <name> puts it in its own isolated environment and links it into /srv/pydata/pipx/bin, which is on your PATH. Use this method to install tools. The next step covers installing libraries.

7

Start a project

The system interpreter is protected. Ubuntu marks it externally managed under PEP 668 so that installing packages system-wide cannot break the operating system, and that guard rail is deliberately left intact on this image. It is the intended behaviour, and the workflow it points you to is one virtual environment per project:

python3 -m venv /srv/pydata/venvs/myproject source /srv/pydata/venvs/myproject/bin/activate python -V

Inside the environment, pip refers to that environment's pip and installs only into it:

pip install requests

Everything pip downloads is cached on the data volume, so a second project that needs the same package installs from local disk. If you prefer a virtual environment that starts with a newer pip and setuptools than the ones Ubuntu bundles, use virtualenv /srv/pydata/venvs/myproject instead. It seeds its own.

Leave the environment with deactivate.

8

Compile a package that has no wheel

This is what the image is for. The --no-binary option makes pip refuse the prebuilt wheel and build from the source distribution instead, which needs a C compiler and the CPython development headers:

Run this inside the activated environment from Step 7, then confirm that the extension was compiled here and that it works:

pip install --no-binary :all: psutil==7.2.2 python -c "import psutil; print(psutil._psutil_linux.__file__)" python -c "import psutil; print('cpus', psutil.cpu_count(), 'ram_mib', psutil.virtual_memory().total // 1048576)"

Expected output on a Standard_B2s:

/srv/pydata/venvs/myproject/lib/python3.12/site-packages/psutil/_psutil_linux.abi3.so cpus 2 ram_mib 3912

The same applies to anything else that ships only an sdist for your platform, or that you want to build locally. lxml finds libxml2 and libxslt, psycopg finds libpq, Pillow finds libjpeg, libpng and FreeType, and numerically heavy packages find OpenBLAS and gfortran.

9

Lint, format and test

ruff is on the PATH and needs no configuration. It operates on source text, so running it globally is correct:

cd /srv/pydata/workspace printf 'import os\n\n\ndef f( x ):\n return x+1\n' > sample.py ruff check sample.py ruff format sample.py cat sample.py

ruff check reports the unused os import, and ruff format rewrites the spacing. black is available as an alternative formatter.

For tests, install pytest into the project's environment instead of globally. A globally installed test runner executes with the wrong interpreter and cannot see your project's dependencies, which is why this image does not put pytest on the system PATH. Its wheel is already in the cache on the data volume, so the install is served locally:

pip install pytest python -m pytest
10

Remote development from your editor

The machine is ready for an editor attached over SSH.

  • VS Code: install the Remote SSH extension locally and connect to azureuser@<vm-ip>. The Python extension finds /usr/bin/python3 and any virtual environment under /srv/pydata/venvs/. Select the project's environment with Python: Select Interpreter.
  • Neovim, Helix, Zed, Emacs, Kate: pylsp is installed and on the PATH, so point your LSP client at the pylsp command. It provides completion, hover, signature help and go-to-definition through jedi.

The caches live on the data volume and are shared by every environment, so an editor that rebuilds an environment does not download anything again.

11

The licence audit

Every distribution installed on this image, direct and transitive, has been audited against its real licence artifact instead of a badge on a project page. The auditor resolves each licence in four tiers: PEP 639 License-Expression metadata, then trove classifiers, then the legacy License field, then the licence files the distribution ships inside its own .dist-info directory. For packages installed by apt, it also reads the Debian machine-readable copyright file. Anything it cannot resolve is a policy violation, not a warning, so the audit fails closed.

The result ships on the image, and you can run the audit again at any time:

sac-python-licence-audit --acknowledged 'docstring-to-markdown pathspec' --quiet grep -E '^AUDIT VERDICT' /usr/share/doc/sac-python/licenses/python-dependency-licences.txt

Expected output:

AUDIT VERDICT: CLEAN — 125 distributions examined, no policy violation AUDIT VERDICT: CLEAN — 125 distributions examined, no policy violation

Two distributions in SecureAnyCloud's tool environment are weak copyleft instead of permissive, and both are named explicitly on the acknowledged list above:

  • docstring-to-markdown (LGPL-2.1-or-later), a dependency of python-lsp-server.
  • pathspec (MPL-2.0), a dependency of black.

Both are pure Python and ship unmodified, so the corresponding source is exactly what is installed on the machine. The auditor rejects any other weak copyleft distribution, so that list cannot grow without the build failing. Strong copyleft and the source-available licence family (SSPL, Business Source, Commons Clause, Elastic) are rejected outright.

The interpreter's own licence is the PSF License Agreement, a permissive, GPL-compatible licence. The full text, together with the licences of the third-party code CPython bundles, is on the image:

grep -c 'PYTHON SOFTWARE FOUNDATION LICENSE' /usr/share/doc/sac-python/licenses/cpython/copyright find /usr/share/doc/sac-python/licenses -type f | wc -l
12

Security posture

ss -Hltn

Expected output: SSH, plus the loopback-only DNS stub resolver that every stock Ubuntu image runs:

LISTEN 0 4096 127.0.0.54:53 0.0.0.0:* LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* LISTEN 0 4096 [::]:22 [::]:*

You can confirm this directly. The first probe is a positive control. It has to succeed, because a probe that never works would refuse everything and the refusal on the second probe would prove nothing.

timeout 5 bash -c 'exec 3<>/dev/tcp/127.0.0.1/22' && echo "22 accepted (positive control)" timeout 5 bash -c 'exec 3<>/dev/tcp/127.0.0.1/8000' || echo "8000 refused, as expected" swapon --show || echo "no swap"

Expected output:

22 accepted (positive control) 8000 refused, as expected no swap

Notes on the posture:

  • No credentials ship in the image. There is no service to authenticate to. /var/lib/sac/data/python-workspace.env is a plain environment manifest, not a secrets file.
  • No swap is present, on the OS disk or anywhere else, as Azure image certification requires.
  • Unattended security upgrades are enabled, exactly as on a stock Ubuntu image.
  • The PEP 668 guard rail is intact. Installing into the system interpreter with --break-system-packages is possible but is not the supported workflow. Use a virtual environment or pipx.
  • Keep the network security group restricted to SSH from your own address. If you expose an application you develop here, put it behind a separate, properly configured front end instead of opening a port on the workstation.

Resizing and backing up the data volume

The data volume is an ordinary Azure managed disk. To grow it, deallocate the VM, resize the disk, start the VM and grow the filesystem:

sudo resize2fs /dev/disk/azure/scsi1/lun0 df -h /srv/pydata

Snapshot it from the Azure CLI whenever you want a restore point for your workspace. Nothing in /srv/pydata is required for the machine to boot, so restoring a snapshot is non-disruptive.

Updating

Operating system packages, including the interpreter itself, come from the Ubuntu archive:

sudo apt-get update apt list --upgradable

Apply updates with sudo apt-get dist-upgrade in your own maintenance window. Because the interpreter is an archive package, a CPython security fix reaches you through the same channel as the rest of the operating system.

To update the Python packages inside one of your own environments, activate it and run pip install --upgrade. SecureAnyCloud's tool environment at /opt/sac/pytools is deliberately separate and is not on your PATH as a pip target, so your upgrades cannot affect it, and its upgrades cannot affect you.

Troubleshooting

error: externally-managed-environment when installing a package. This is PEP 668 working as intended, because you are outside a virtual environment. Create one with python3 -m venv /srv/pydata/venvs/<name> and activate it, or use pipx for a standalone application.

A package fails to build from source. Read the compiler error for the missing header it names, then install the matching -dev package with apt-get. The common ones are already present, and anything unusual is one apt-get install lib<name>-dev away.

/srv/pydata is missing after a resize or a disk swap. Confirm the entry in /etc/fstab still matches the volume's UUID with sudo blkid, then sudo mount -a.

Permission denied writing to /srv/pydata. Confirm your account is in the pydev group with id. First boot adds the account that existed at that moment. An account created later needs sudo usermod -aG pydev <user> and a fresh login.

The first boot manifest is missing. Check the unit with systemctl status python-firstboot.service and journalctl -u python-firstboot.service. It is a one-shot unit that runs only while its sentinel is absent.

Support

SecureAnyCloud provides 24/7 support for this image. Questions about the image, the toolchain layout or the data volume are in scope; questions about your own application code are not.

Python is a trademark of the Python Software Foundation. SecureAnyCloud is not affiliated with, sponsored by, or endorsed by the Python Software Foundation. Ubuntu is a registered trademark of Canonical Ltd.

Deploy on Azure

Launch Python 3.12 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure