{"id":2952,"date":"2026-11-05T09:52:42","date_gmt":"2026-11-05T09:52:42","guid":{"rendered":"https:\/\/hostry.com\/blog\/?p=2952"},"modified":"2026-11-05T09:52:44","modified_gmt":"2026-11-05T09:52:44","slug":"python-3-10-end-of-life-ubuntu-22-04","status":"publish","type":"post","link":"https:\/\/hostry.com\/blog\/python-3-10-end-of-life-ubuntu-22-04\/","title":{"rendered":"Python 3.10 End of Life: What It Means on an Ubuntu 22.04 VPS"},"content":{"rendered":"\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1200\" height=\"675\" src=\"https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/python-3-10-end-of-life-ubuntu-22-04-cover.webp\" alt=\"Python 3.10 end of life on Ubuntu 22.04: applications on a VPS moving from Python 3.10 to Python 3.12\" class=\"wp-image-2942\" srcset=\"https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/python-3-10-end-of-life-ubuntu-22-04-cover.webp 1200w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/python-3-10-end-of-life-ubuntu-22-04-cover-300x169.webp 300w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/python-3-10-end-of-life-ubuntu-22-04-cover-1024x576.webp 1024w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/python-3-10-end-of-life-ubuntu-22-04-cover-450x253.webp 450w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/python-3-10-end-of-life-ubuntu-22-04-cover-768x432.webp 768w\" sizes=\"auto, (max-width: 1200px) 100vw, 1200px\" \/><\/figure>\n\n\n\n<pre class=\"wp-block-preformatted\" style=\"font-size:14px\"><em><strong>You run an Ubuntu 22.04 VPS, python3 --version prints Python 3.10.12, and a scanner or a library changelog now flags Python 3.10 end of life. The interpreter Ubuntu installed keeps getting Canonical security fixes until May 2027 (Ubuntu Pro stretches that to 2032), but upstream support ended on October 1, 2026, and your own virtualenvs and pip dependencies are the part that ages from here.<\/strong><\/em><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That 3.10.12 does not mean ten missed releases. Canonical keeps the version string it shipped and backports fixes into it. What needs checking is each of your applications: where its interpreter comes from, and which pip dependencies it pins. On a <strong><a href=\"https:\/\/hostry.com\/products\/vps\/\">Hostry KVM VPS<\/a><\/strong> you have root, so you run that check and the move to 3.12 yourself.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Paths below such as <span class=\"highlighted\">\/srv\/app<\/span>, the user <span class=\"highlighted\">deploy<\/span> and the unit <span class=\"highlighted\">app.service<\/span> are placeholders for your own. On November 5, 2026, package versions and dates were checked against the Ubuntu archive and python.org. The uv commands ran with uv 0.13.7 on a clean Ubuntu test system, and the python3-minimal repair in a minimal 22.04 chroot.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Python 3.10 end of life: what ended on October 1 and what did not<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><a href=\"https:\/\/peps.python.org\/pep-0619\/\" target=\"_blank\" rel=\"noopener\">PEP 619, the Python 3.10 release schedule<\/a><\/strong>, promised source-only security releases until October 1, 2026, five years after 3.10.0. Python 3.10.22 came out that day and was the final one. The core team now accepts no 3.10 issues on the bug tracker, so a CPython flaw found in November is patched on 3.11 and newer and stays open on 3.10 upstream.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Upstream, Python 3.10 is no longer supported in any form. The <strong><a href=\"https:\/\/devguide.python.org\/versions\/\" target=\"_blank\" rel=\"noopener\">Python developer guide&#8217;s version table<\/a><\/strong> shows where the newer lines stand:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Version<\/th><th>Upstream status (November 5, 2026)<\/th><th>Upstream end of life<\/th><th>Default python3 on<\/th><\/tr><\/thead><tbody><tr><td>3.10<\/td><td>End of life, last release 3.10.22<\/td><td>October 1, 2026<\/td><td>Ubuntu 22.04<\/td><\/tr><tr><td>3.11<\/td><td>Security fixes only<\/td><td>October 2027<\/td><td>None of the LTS releases<\/td><\/tr><tr><td>3.12<\/td><td>Security fixes only<\/td><td>October 2028<\/td><td>Ubuntu 24.04<\/td><\/tr><tr><td>3.13<\/td><td>Security fixes only<\/td><td>October 2029<\/td><td>None of the LTS releases<\/td><\/tr><tr><td>3.14<\/td><td>Bug fixes<\/td><td>October 2030<\/td><td>Ubuntu 26.04<\/td><\/tr><tr><td>3.15<\/td><td>Bug fixes, first released October 9, 2026<\/td><td>October 2031<\/td><td>None yet<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The packaged <span class=\"highlighted\">\/usr\/bin\/python3.10<\/span> keeps receiving Canonical fixes through apt, as <strong><a href=\"#who-patches-what\">the patching breakdown below<\/a><\/strong> shows.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"who-patches-what\">Python 3.10 security updates on Ubuntu 22.04: who patches what<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">On 22.04, python3.10 lives in the <strong>main<\/strong> component. According to the <strong><a href=\"https:\/\/ubuntu.com\/about\/release-cycle\" target=\"_blank\" rel=\"noopener\">Ubuntu release cycle page<\/a><\/strong>, packages in main get standard security maintenance on 22.04 until May 2027, and Expanded Security Maintenance through Ubuntu Pro runs to May 2032. Canonical is still doing that work. The current jammy-security build, <span class=\"highlighted\">3.10.12-1~22.04.18<\/span>, reached the archive in September 2026 with fixes for CVE-2026-4360 in tarfile extraction and CVE-2026-15308 in the HTML parser.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Those updates stop at the package itself. Everything else Python-related on the server is a separate installation or dependency to inspect:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>pip packages inside each virtualenv. A venv whose <span class=\"highlighted\">pyvenv.cfg<\/span> says <span class=\"highlighted\">home = \/usr\/bin<\/span> runs on Ubuntu&#8217;s patched interpreter, yet its libraries stay at whatever pip installed. When a project stops supporting 3.10, its fixes ship in releases that venv cannot install.<\/li><li>Interpreters built by pyenv or from source under <span class=\"highlighted\">\/usr\/local<\/span>. A venv whose home points at <span class=\"highlighted\">.pyenv<\/span> or <span class=\"highlighted\">\/usr\/local<\/span> runs on one of these, and with upstream 3.10 closed, nobody patches it now.<\/li><li>Container images built on <span class=\"highlighted\">python:3.10<\/span>, whose interpreter comes from the image, never from the host.<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Python libraries installed with apt follow their component. A <span class=\"highlighted\">python3-*<\/span> package from main is patched like the interpreter; many come from universe, which gets security coverage only through ESM under Ubuntu Pro.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1200\" height=\"800\" src=\"https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/python-3-10-end-of-life-ubuntu-22-04-who-patches.webp\" alt=\"Who patches Python 3.10 on Ubuntu 22.04: upstream CPython closed October 1, 2026; Ubuntu python3.10 package patched to May 2027 and to 2032 with Ubuntu Pro; virtualenv packages, pyenv builds and Docker images patched by you\" class=\"wp-image-2945\" srcset=\"https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/python-3-10-end-of-life-ubuntu-22-04-who-patches.webp 1200w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/python-3-10-end-of-life-ubuntu-22-04-who-patches-300x200.webp 300w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/python-3-10-end-of-life-ubuntu-22-04-who-patches-1024x683.webp 1024w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/python-3-10-end-of-life-ubuntu-22-04-who-patches-450x300.webp 450w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/python-3-10-end-of-life-ubuntu-22-04-who-patches-768x512.webp 768w\" sizes=\"auto, (max-width: 1200px) 100vw, 1200px\" \/><figcaption>Three layers of Python on an Ubuntu 22.04 server and who is responsible for fixing each one after October 1, 2026.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Pip dependencies age quietly. These projects already require a newer interpreter, per the Requires-Python field of their current releases on PyPI:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>NumPy 2.3.0 (June 2025) needs 3.11 or later, and the current 2.5.4 needs 3.12. The last line for 3.10 is 2.2.x.<\/li><li>pandas 3.0.0 (January 2026) and SciPy 1.16.0 both need 3.11.<\/li><li>Django 6.0 needs 3.12. Django 5.2 LTS still supports 3.10 and gets security releases until April 2028; 5.2.19 shipped on November 3, 2026.<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Inventory first: find which apps use Python 3.10<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:14px\"><strong>1. Confirm the system interpreter is untouched<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python3 --version\nreadlink -f \/usr\/bin\/python3\napt-cache policy python3.10 | head -n 3\nupdate-alternatives --list python3<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A healthy 22.04 server prints <span class=\"highlighted\">Python 3.10.12<\/span>, resolves the link to <span class=\"highlighted\">\/usr\/bin\/python3.10<\/span>, shows the installed and candidate versions as equal, and answers the last command with &#8220;no alternatives for python3&#8221;.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If <span class=\"highlighted\">\/usr\/bin\/python3<\/span> already points at something else, someone repointed it in the past, and restoring it comes before anything else here. The link belongs to the python3-minimal package, so a reinstall puts it back:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo update-alternatives --remove-all python3   # only if step 1 listed alternatives\nsudo apt install --reinstall python3-minimal\nreadlink -f \/usr\/bin\/python3<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Run the first two lines back to back, since the link is missing between them. The check passes when <span class=\"highlighted\">readlink<\/span> prints <span class=\"highlighted\">\/usr\/bin\/python3.10<\/span> again.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:14px\"><strong>2. List every virtualenv and its base interpreter<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo find \/ -xdev -name pyvenv.cfg 2&gt;\/dev\/null | while read -r f; do\n  echo \"== $f\"; sudo grep -E '^(home|version|version_info) ' \"$f\"\ndone<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Each venv carries a <span class=\"highlighted\">pyvenv.cfg<\/span>. <span class=\"highlighted\">version = 3.10.12<\/span> next to <span class=\"highlighted\">home = \/usr\/bin<\/span> marks a venv on the Ubuntu package; a home under <span class=\"highlighted\">.pyenv<\/span> or <span class=\"highlighted\">\/usr\/local<\/span> is one of the unpatched interpreters listed earlier. Add the mount points to the command if <span class=\"highlighted\">\/srv<\/span> or <span class=\"highlighted\">\/home<\/span> sits on a separate disk, since <span class=\"highlighted\">-xdev<\/span> stays on the root filesystem.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:14px\"><strong>3. Find what starts them<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>grep -rlE 'python|venv' \/etc\/systemd\/system\/ 2&gt;\/dev\/null\nsystemctl cat app.service | grep -E '^(User|WorkingDirectory|ExecStart)='\nsudo grep -rnE 'python|venv' \/etc\/crontab \/etc\/cron.d\/ \/var\/spool\/cron\/crontabs\/ 2&gt;\/dev\/null\nps -eo user,pid,args | grep -E '[p]ython'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Write down every unit, cron line and process with its interpreter path; that list is your work plan for the switch.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:14px\"><strong>4. Freeze the dependencies and see what is stuck<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/srv\/app\/.venv\/bin\/python -m pip freeze &gt; \/srv\/app\/requirements-310.lock\n\/srv\/app\/.venv\/bin\/python -m pip list --outdated<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Read the second list with care. pip only offers releases whose Requires-Python accepts the running interpreter, so a 3.10 venv never shows the versions that dropped 3.10. In a 3.10.22 venv on November 5, pip named NumPy 2.2.6 as the newest release, while PyPI&#8217;s actual latest was 2.5.4. Treat &#8220;up to date&#8221; there as &#8220;up to date for 3.10&#8221;.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:14px\"><strong>5. Check containers<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker ps --format '{{.Image}}  {{.Names}}'\ngrep -rn '^FROM python:3\\.10' \/srv --include='Dockerfile*' 2&gt;\/dev\/null<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If every Python app on the server runs in containers, skip ahead to <strong><a href=\"#python-310-docker-images\">the section on python:3.10 images<\/a><\/strong>. Steps 6 and 7 cover apps in host virtualenvs.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Upgrade Python 3.10 to 3.12 on Ubuntu 22.04 without touching \/usr\/bin\/python3<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Never repoint <span class=\"highlighted\">\/usr\/bin\/python3<\/span> with update-alternatives or a manual symlink. unattended-upgrade, add-apt-repository, cloud-init and do-release-upgrade are Python scripts whose shebang line names <span class=\"highlighted\">\/usr\/bin\/python3<\/span>, and the apt bindings they import ship as <span class=\"highlighted\">apt_pkg.cpython-310-x86_64-linux-gnu.so<\/span>, a module built for 3.10 only. Under another interpreter that import fails, and the tools that install your security updates stop working. Leave python3.10 installed, too; python3, ubuntu-minimal and netplan.io depend on it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two shortcuts also look tempting. <span class=\"highlighted\">apt install python3.11<\/span> on 22.04 brings in <span class=\"highlighted\">3.11.0~rc1-1~22.04.1<\/span> from universe, a release candidate. The deadsnakes PPA states on its own page that there is no guarantee of timely security updates, so we leave it out of production plans. That leaves four routes.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Route<\/th><th>Fits when<\/th><th>What you take on<\/th><\/tr><\/thead><tbody><tr><td>Keep 3.10 for now<\/td><td>The app uses only apt packages from main, or a dated move is already planned before May 2027<\/td><td>pip dependencies frozen at their last 3.10 releases; the OS deadline still arrives<\/td><\/tr><tr><td>uv-managed 3.12 on the same 22.04 server<\/td><td>A library or auditor needs 3.12 soon and the server cannot move yet<\/td><td>You update the interpreter yourself; the 22.04 standard support date stays<\/td><\/tr><tr><td>New Ubuntu 24.04 VPS<\/td><td>Most production servers<\/td><td>Data migration and a DNS switch; python3.12 then comes from main, patched until May 2029<\/td><\/tr><tr><td>In-place do-release-upgrade to 24.04<\/td><td>A simple server with a backup you have restored once<\/td><td>Every venv built on python3.10 breaks, since noble has no python3.10 package; rollback means restore<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1200\" height=\"800\" src=\"https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/python-3-10-end-of-life-ubuntu-22-04-routes.webp\" alt=\"Decision card for Python 3.10 on Ubuntu 22.04: keep 3.10 until May 2027, uv-managed Python 3.12 on the same server, a new Ubuntu 24.04 VPS, or an in-place upgrade to 24.04\" class=\"wp-image-2946\" srcset=\"https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/python-3-10-end-of-life-ubuntu-22-04-routes.webp 1200w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/python-3-10-end-of-life-ubuntu-22-04-routes-300x200.webp 300w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/python-3-10-end-of-life-ubuntu-22-04-routes-1024x683.webp 1024w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/python-3-10-end-of-life-ubuntu-22-04-routes-450x300.webp 450w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/python-3-10-end-of-life-ubuntu-22-04-routes-768x512.webp 768w\" sizes=\"auto, (max-width: 1200px) 100vw, 1200px\" \/><figcaption>Four ways off Python 3.10 on an Ubuntu 22.04 VPS, with the condition that makes each one the right choice.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">We would build a fresh 24.04 server for anything in production, such as a <strong><a href=\"https:\/\/hostry.com\/products\/vps\/\">KVM VPS deployed from the Ubuntu 24.04 image<\/a><\/strong>. It settles the Python question and the May 2027 OS deadline in one migration, the 22.04 machine stays live as the fallback, and apt keeps patching python3.12 afterwards. Use uv on the old server when an application has to move before you can schedule that migration.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the in-place route, first confirm that <span class=\"highlighted\">\/etc\/update-manager\/release-upgrades<\/span> contains <span class=\"highlighted\">Prompt=lts<\/span>, the 22.04 default. Then start the upgrade with <span class=\"highlighted\">sudo do-release-upgrade<\/span>. If SSH drops while it runs, log in through the VNC console in your Hostry VPS control panel and run <span class=\"highlighted\">sudo do-release-upgrade<\/span> again; it reattaches to the upgrade&#8217;s running screen session.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Recreate the virtualenv with the new Python version<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A venv cannot move to another minor version in place, so build a new one beside the old one and install the frozen pins. Use 6a for the uv route and 6b for a new 24.04 VPS. After an in-place do-release-upgrade, rebuild each venv with the venv commands from 6b.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:14px\"><strong>6a. On the 22.04 server, with uv<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># as deploy, the user app.service runs as\ncurl -LsSf https:\/\/astral.sh\/uv\/install.sh | sh\nsource $HOME\/.local\/bin\/env\nuv python install 3.12\nuv venv --python 3.12 \/srv\/app\/.venv-312\nuv pip install --python \/srv\/app\/.venv-312\/bin\/python --only-binary :all: -r \/srv\/app\/requirements-310.lock\n\/srv\/app\/.venv-312\/bin\/python --version<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">uv downloads a CPython build from Astral&#8217;s python-build-standalone project and keeps it in <span class=\"highlighted\">~\/.local\/share\/uv\/python<\/span>, so install it as the account the service runs under. If the service runs as another user, such as www-data, create a shared directory first with <span class=\"highlighted\">sudo install -d -o deploy -g deploy -m 755 \/opt\/uv\/python<\/span> and put <span class=\"highlighted\">export UV_PYTHON_INSTALL_DIR=\/opt\/uv\/python<\/span> in deploy&#8217;s profile, so installs and later upgrades land there. It produced Python 3.12.16 on uv 0.13.7. The new venv has no pip inside; use <span class=\"highlighted\">uv pip<\/span>, or create it with <span class=\"highlighted\">&#8211;seed<\/span> if your deploy scripts call pip.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><span class=\"highlighted\">&#8211;only-binary :all:<\/span> makes old pins fail fast. NumPy 1.24.4, for example, has no 3.12 wheels, and uv stops with &#8220;has no usable wheels&#8221; before it attempts a long source build. Raise only those pins to the first release with cp312 wheels, and leave the general upgrade for a later change.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:14px\"><strong>6b. On a new Ubuntu 24.04 VPS<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Harden the new server (203.0.113.10 here) first, then copy the application code and step 4&#8217;s lock file to it. Leave the old venvs behind, since they only work on the original interpreter, and restore a fresh database dump separately:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh -t deploy@203.0.113.10 'sudo install -d -o deploy -g deploy \/srv\/app'\nrsync -a --exclude '.venv*' \/srv\/app\/ deploy@203.0.113.10:\/srv\/app\/<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt update &amp;&amp; sudo apt install -y python3-venv\npython3 --version\npython3 -m venv \/srv\/app\/.venv-312\n\/srv\/app\/.venv-312\/bin\/pip install --only-binary :all: -r \/srv\/app\/requirements-310.lock<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expect <span class=\"highlighted\">Python 3.12.3<\/span>; Ubuntu patches it under that number the same way it does 3.10.12. On 24.04 a plain <span class=\"highlighted\">sudo pip install<\/span> outside a venv fails with &#8220;externally-managed-environment&#8221; because of the <span class=\"highlighted\">EXTERNALLY-MANAGED<\/span> marker in <span class=\"highlighted\">\/usr\/lib\/python3.12<\/span>. That protects the system interpreter, so keep <span class=\"highlighted\">&#8211;break-system-packages<\/span> out of your scripts. Before copying anything over, start with the <strong><a href=\"https:\/\/hostry.com\/blog\/secure-new-linux-vps-first-30-minutes\/\">first 30 minutes of hardening on a fresh VPS<\/a><\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:14px\"><strong>6c. Test before anything switches<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cd \/srv\/app\n\/srv\/app\/.venv-312\/bin\/python -m pytest -q<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Swap in your own test runner if it is not pytest. On a new VPS, also start the app and use it under a staging hostname while 22.04 keeps serving. Move on to step 7 once the tests pass and, on a new VPS, the staging check too.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:14px\"><strong>7. Point the systemd unit and cron at the new venv<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl edit app.service\n# in the editor:\n[Service]\nExecStart=\nExecStart=\/srv\/app\/.venv-312\/bin\/gunicorn --bind 127.0.0.1:8000 app.wsgi:application\n\nsudo systemctl restart app.service<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">An empty <span class=\"highlighted\">ExecStart=<\/span> line clears the original command; without it systemd refuses a second ExecStart for a normal service. The drop-in lands in <span class=\"highlighted\">\/etc\/systemd\/system\/app.service.d\/override.conf<\/span>, and <span class=\"highlighted\">systemctl edit<\/span> reloads the daemon when you save. Console scripts such as gunicorn carry the absolute path of their own venv in the first line, which is why renaming <span class=\"highlighted\">.venv-312<\/span> to <span class=\"highlighted\">.venv<\/span> later would break them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For cron, open the deploy user&#8217;s table with <span class=\"highlighted\">crontab -e<\/span>, or the file under <span class=\"highlighted\">\/etc\/cron.d\/<\/span> from step 3, and change each <span class=\"highlighted\">\/srv\/app\/.venv\/bin\/python<\/span> to the new path. Then run one of those jobs by hand as deploy, because cron mails its errors to an address nobody may be reading.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Check the switch, then retire the old environment<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>systemctl is-active app.service\nsudo readlink -f \/proc\/$(systemctl show -p MainPID --value app.service)\/exe\njournalctl -u app.service --since \"-15min\" -p warning --no-pager\ncurl -fsS http:\/\/127.0.0.1:8000\/healthz<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The second line names the binary the main process really runs: <span class=\"highlighted\">\/usr\/bin\/python3.12<\/span> on 24.04, or a path ending in <span class=\"highlighted\">cpython-3.12.16-linux-x86_64-gnu\/bin\/python3.12<\/span> under uv. A 3.10 path means the unit still starts the old venv; check the override with <span class=\"highlighted\">systemctl cat app.service<\/span>. Use a URL that touches the database in place of <span class=\"highlighted\">\/healthz<\/span>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Keep the way back open for a week. How you roll back depends on the route:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Same server with uv: the old <span class=\"highlighted\">\/srv\/app\/.venv<\/span> is still there. Run <span class=\"highlighted\">sudo rm \/etc\/systemd\/system\/app.service.d\/override.conf<\/span>, <span class=\"highlighted\">sudo systemctl daemon-reload<\/span> and a restart, then change the cron path back. Delete the old venv once nothing has needed it.<\/li><li>New 24.04 VPS: the 22.04 server is still running, so point DNS or the reverse proxy back at it.<\/li><li>In-place upgrade: restore from the backup you took before running do-release-upgrade.<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Under the uv route, interpreter updates are your job. Python 3.12 gets security releases until October 2028; the <strong><a href=\"https:\/\/docs.astral.sh\/uv\/concepts\/python-versions\/\" target=\"_blank\" rel=\"noopener\">uv documentation on managed Python versions<\/a><\/strong> says each uv release carries a fixed list of available builds. Run <span class=\"highlighted\">uv self update<\/span> first, then <span class=\"highlighted\">uv python upgrade 3.12<\/span>, then restart the service. Venvs created with <span class=\"highlighted\">&#8211;python 3.12<\/span> follow the new patch release through uv&#8217;s minor-version link; one pinned to an exact patch such as 3.12.16 stays where it is. On 24.04 apt does the updating: <span class=\"highlighted\">apt list &#8211;upgradable 2&gt;\/dev\/null | grep python3.12<\/span> shows a pending build, and <span class=\"highlighted\">sudo systemctl restart app.service<\/span> after it installs loads the patched binary.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:14px\"><strong>Related maintenance on the same server<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A 24.04 server is also where the <strong><a href=\"https:\/\/hostry.com\/blog\/upgrade-ubuntu-24-04-to-26-04\/\">later move from 24.04 to 26.04 and its pre-flight checks<\/a><\/strong> begins. If the same 22.04 box hosts PostgreSQL 14, its <strong><a href=\"https:\/\/hostry.com\/blog\/postgresql-14-end-of-life-upgrade\/\">PostgreSQL 14 deadline on November 12<\/a><\/strong> falls in this window too; give each upgrade its own maintenance slot.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"python-310-docker-images\">python:3.10 Docker images after end of life<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">On November 5, 2026 the official image&#8217;s supported tags on Docker Hub ran from 3.11 to 3.15 plus a 3.16 pre-release, with no 3.10 among them. The <strong><a href=\"https:\/\/github.com\/docker-library\/faq\" target=\"_blank\" rel=\"noopener\">docker-library FAQ<\/a><\/strong> explains what that means: a dropped tag stays available to pull and is never rebuilt again. <span class=\"highlighted\">python:3.10-slim<\/span> is frozen with its last interpreter and its last Debian base, and <span class=\"highlighted\">docker build &#8211;pull<\/span> changes nothing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Change the <span class=\"highlighted\">FROM<\/span> line to <span class=\"highlighted\">python:3.12-slim<\/span>, rebuild, run the tests inside the container and redeploy. Name the minor version in the tag. A bare <span class=\"highlighted\">python:3<\/span> moves to the next minor whenever upstream ships one, and Ubuntu&#8217;s fixes on the host never reach the interpreter inside the image.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Build the 24.04 environment on a <strong><a href=\"https:\/\/hostry.com\/products\/vps\/\">Hostry VPS with the Ubuntu 24.04 image<\/a><\/strong> and test the migration there before you switch anything. When the checks above pass, move DNS and keep the old server until the week is out. Plans in the <strong><a href=\"https:\/\/hostry.com\/services\/vps\/\">VPS catalog<\/a><\/strong> began at $6 per month on November 5, 2026, with monthly billing and resources you can scale later.<\/p>\n<!-- AddThis Advanced Settings generic via filter on the_content --><!-- AddThis Share Buttons generic via filter on the_content -->","protected":false},"excerpt":{"rendered":"<p>Upstream Python 3.10 ended on October 1, 2026, but Ubuntu 22.04 keeps patching its python3.10 package until May 2027. Find the apps and virtualenvs th&#8230;<\/p>\n","protected":false},"author":6,"featured_media":2943,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1,15],"tags":[],"class_list":["post-2952","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-virtual-private-server","category-linux-server"],"read_time":9,"_links":{"self":[{"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/posts\/2952","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/users\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/comments?post=2952"}],"version-history":[{"count":1,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/posts\/2952\/revisions"}],"predecessor-version":[{"id":2955,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/posts\/2952\/revisions\/2955"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/media\/2943"}],"wp:attachment":[{"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/media?parent=2952"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/categories?post=2952"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/tags?post=2952"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}