<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[HPanel Blog]]></title><description><![CDATA[Tutorials and guides for running websites, email, DNS, SSL, Docker, Node.js and Python apps on your own server with HPanel. https://hpanel.net/]]></description><link>https://hpanelnet.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 04 Nov 2026 04:03:47 GMT</lastBuildDate><atom:link href="https://hpanelnet.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[cgroups Resource Isolation for Shared Hosting: How Per-Account CPU, RAM, IO and Process Limits Work]]></title><description><![CDATA[How Linux cgroups isolate shared hosting accounts: per-account CPU, memory, IO and process limits, plus filesystem jails, MySQL limits and packages.]]></description><link>https://hpanelnet.hashnode.dev/cgroups-resource-isolation-shared-hosting</link><guid isPermaLink="true">https://hpanelnet.hashnode.dev/cgroups-resource-isolation-shared-hosting</guid><category><![CDATA[Linux]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Web Hosting]]></category><category><![CDATA[performance]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[HPanel]]></dc:creator><pubDate>Mon, 26 Oct 2026 18:53:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adf973801ed43c9b9f0380a/01ee9f88-0a28-46d2-afd2-b51add38ecd6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every shared hosting provider has seen the "noisy neighbour" problem. One account runs a badly written import script, a WordPress cron storm or a crypto miner from a hacked plugin, and suddenly <strong>every</strong> site on the server is slow. Customers who did nothing wrong open tickets, and you spend the evening finding the culprit in <code>top</code>.</p>
<p>The fix is <strong>resource isolation</strong>: giving every hosting account hard limits on CPU, memory, disk IO and processes, so a single account can only ever use its own share. On Linux, the kernel feature behind this is <strong>control groups (cgroups)</strong>.</p>
<p>This article explains:</p>
<ul>
<li>what cgroups are and how CPU, memory, IO and process limits work under <strong>cgroup v2</strong>,</li>
<li>which limits matter on a shared hosting server, and which <em>aren't</em> handled by cgroups at all,</li>
<li>the extra layers you need beyond cgroups (filesystem jails, per-account PHP pools, database limits),</li>
<li>how to choose sensible per-package limits.</li>
</ul>
<p>Along the way I'll show how this is packaged in <strong>HPanel</strong>, the self-hosted hosting control panel I work on, which builds per-account cgroups isolation into the panel itself.</p>
<blockquote>
<p><strong>Disclosure:</strong> I'm on the HPanel team. HPanel specifics come from the public <a href="https://hpanel.net/features.php">features page</a>. The cgroup v2 details are standard Linux kernel behaviour.</p>
</blockquote>
<hr />
<h2>What cgroups actually are</h2>
<p>A <strong>control group</strong> is a set of processes that the kernel accounts for and limits together. Under <strong>cgroup v2</strong> (the unified hierarchy used by current Ubuntu LTS releases), cgroups appear as directories under <code>/sys/fs/cgroup</code>. Each directory exposes small files called <em>interface files</em>: you write a value to set a limit and read a file to see usage.</p>
<p>On a systemd-based server you can look at the tree with:</p>
<pre><code class="language-bash">systemd-cgls          # the cgroup tree
systemd-cgtop         # live usage per cgroup, like top
cat /sys/fs/cgroup/cgroup.controllers   # which controllers are available
</code></pre>
<p>For hosting, the idea is simple: <strong>put every process belonging to one hosting account (PHP-FPM workers, cron jobs, SSH sessions, Node or Python apps) into that account's cgroup</strong>, then set limits on that cgroup. The kernel enforces them, so no cooperation from the customer's code is needed.</p>
<hr />
<h2>The four limits that matter most</h2>
<h3>1. CPU: <code>cpu.max</code></h3>
<p><code>cpu.max</code> takes two numbers, a <strong>quota</strong> and a <strong>period</strong> in microseconds:</p>
<pre><code class="language-bash"># allow 1 full CPU: 100 ms of CPU time per 100 ms period
echo "100000 100000" &gt; /sys/fs/cgroup/&lt;account&gt;/cpu.max

# allow 2 CPUs
echo "200000 100000" &gt; /sys/fs/cgroup/&lt;account&gt;/cpu.max
</code></pre>
<p>When the account uses up its quota within a period, its processes are <strong>throttled</strong> until the next period. They slow down instead of being killed. That's exactly what you want for a busy site: it gets slower, but the rest of the server doesn't notice.</p>
<p>There's also <code>cpu.weight</code>, a relative share used when the CPU is contended. Hard caps (<code>cpu.max</code>) give predictability. Weights give fairness without wasting idle CPU. Many setups use both.</p>
<h3>2. Memory: <code>memory.max</code> and <code>memory.high</code></h3>
<pre><code class="language-bash">echo 1G &gt; /sys/fs/cgroup/&lt;account&gt;/memory.high   # start reclaiming/throttling here
echo 2G &gt; /sys/fs/cgroup/&lt;account&gt;/memory.max    # hard ceiling
</code></pre>
<ul>
<li><code>memory.high</code> is a <strong>soft</strong> limit. Above it the kernel reclaims memory aggressively and slows the group down.</li>
<li><code>memory.max</code> is the <strong>hard</strong> limit. If the group can't get back under it, the kernel's OOM killer acts <strong>inside that cgroup only</strong>. The account's own process dies, not your MariaDB server.</li>
</ul>
<p>That last point is the whole value of memory isolation. Without it, a runaway PHP process in one account can push the entire server into swap or trigger the OOM killer on something critical.</p>
<h3>3. Disk IO: <code>io.max</code></h3>
<pre><code class="language-bash"># limit reads and writes on device 8:0 to 10 MB/s and 1000 IOPS
echo "8:0 rbps=10485760 wbps=10485760 riops=1000 wiops=1000" \
  &gt; /sys/fs/cgroup/&lt;account&gt;/io.max
</code></pre>
<p>IO is the limit people forget. On SSD and NVMe servers, CPU is rarely the first bottleneck. More often it's a backup job, a big <code>tar</code>, or a plugin hammering the disk that makes every other site's database queries crawl. Throughput (<code>rbps</code>/<code>wbps</code>) <strong>and</strong> operation rate (<code>riops</code>/<code>wiops</code>) limits prevent these "IO storms".</p>
<h3>4. Processes: <code>pids.max</code></h3>
<pre><code class="language-bash">echo 128 &gt; /sys/fs/cgroup/&lt;account&gt;/pids.max
</code></pre>
<p>This caps how many processes and threads the account can have. It's the defence against <strong>fork bombs</strong> and runaway scripts that spawn workers in a loop. Once the limit is reached, new <code>fork()</code> calls fail inside that account, and the rest of the server keeps running.</p>
<hr />
<h2>What cgroups <em>don't</em> cover</h2>
<p>Some per-account limits that hosting customers think of as "resources" are handled by other mechanisms:</p>
<table>
<thead>
<tr>
<th>Resource</th>
<th>Mechanism</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Disk space</strong></td>
<td>Filesystem quotas (block quotas per user)</td>
</tr>
<tr>
<td><strong>Inodes</strong> (number of files)</td>
<td>Filesystem inode quotas</td>
</tr>
<tr>
<td><strong>Monthly bandwidth</strong></td>
<td>Traffic accounting (web server/log based) plus enforcement</td>
</tr>
<tr>
<td><strong>Database load</strong></td>
<td>Database-level limits (queries/connections per user)</td>
</tr>
<tr>
<td><strong>Seeing other users' files</strong></td>
<td>Permissions + a filesystem jail (chroot / mount namespaces)</td>
</tr>
<tr>
<td><strong>PHP concurrency</strong></td>
<td>Per-account PHP-FPM pool size (<code>pm.max_children</code>)</td>
</tr>
</tbody></table>
<p>A complete isolation setup combines cgroups with these. That's why "just enable cgroups" isn't the whole answer.</p>
<hr />
<h2>The other isolation layers</h2>
<h3>Filesystem jail</h3>
<p>Even with CPU and memory capped, a compromised account shouldn't be able to <em>see</em> the rest of the server. A filesystem jail gives each user a restricted view, typically built with chroot and mount-based isolation, so they can't browse other users' home directories, system binaries or server configs.</p>
<h3>Per-account PHP-FPM pools</h3>
<p>If all sites share one PHP-FPM pool running as one user, both performance and security isolation are gone: one slow site exhausts the shared workers, and one webshell can read every site's files. A <strong>separate pool per account</strong>, running as that account's user, with its own <code>pm.max_children</code>, fixes both.</p>
<h3>Database limits</h3>
<p>The database server is shared by definition. A per-user <strong>MySQL governor</strong> limits queries, connections and updates per hour so that one account's heavy reporting query can't slow every other site's database.</p>
<hr />
<h2>How this looks in a hosting control panel</h2>
<p>Setting all of this by hand across hundreds of accounts doesn't scale. You need packages, defaults and a UI that shows usage. In <strong>HPanel</strong>, isolation is part of the core (no extra kernel or extra license), and every hosting account gets hard limits on:</p>
<ul>
<li><strong>CPU</strong>: a specific CPU core count per account, hard-enforced,</li>
<li><strong>RAM</strong>: hard memory limits per account,</li>
<li><strong>IO</strong>: per-account disk IO limits in MB/s, to prevent IO storms,</li>
<li><strong>Disk and inodes</strong>: hard disk quotas with real-time usage tracking and inode enforcement,</li>
<li><strong>Bandwidth</strong>: per-account tracking and enforcement,</li>
<li><strong>Processes</strong>: a maximum number of concurrent processes, to stop fork bombs and runaway scripts.</li>
</ul>
<p>On top of that, HPanel provides a <strong>CageFS-style filesystem jail</strong> (users can't see other users' files, system binaries or server configs), <strong>per-account PHP-FPM pools</strong> with per-package PHP worker counts and priority, and a <strong>MySQL governor</strong> with per-user limits on queries, connections and updates per hour.</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/6adf973801ed43c9b9f0380a/83976c7b-46c5-46d8-8aa6-2fc67b6161a4.png" alt="Per-account resource usage in the HPanel admin panel" />
<em>The HPanel Resource Manager: per-account limits and live usage.</em></p>
<h3>Packages: limits as a product</h3>
<p>Limits only make sense if they're tied to what the customer bought. In HPanel, limits are defined in <strong>hosting packages</strong> (disk, bandwidth, domains, subdomains, email accounts, databases, FTP, addon domains, CPU, RAM, IO, processes and PHP workers) and assigned to accounts. There are one-click presets to start from:</p>
<table>
<thead>
<tr>
<th>Preset</th>
<th>CPU</th>
<th>RAM</th>
</tr>
</thead>
<tbody><tr>
<td>Light</td>
<td>1 CPU</td>
<td>512 MB</td>
</tr>
<tr>
<td>Standard</td>
<td>2 CPU</td>
<td>1 GB</td>
</tr>
<tr>
<td>Power</td>
<td>4 CPU</td>
<td>2 GB</td>
</tr>
<tr>
<td>Unlimited</td>
<td>VPS-style</td>
<td>VPS-style</td>
</tr>
</tbody></table>
<p><img src="https://cdn.hashnode.com/uploads/covers/6adf973801ed43c9b9f0380a/cb59c3fd-cab1-4657-b456-c982e88b9ccc.png" alt="Hosting packages with resource limits in HPanel" />
<em>Hosting packages define each account's limits.</em></p>
<p>Customers see their own usage in the user panel, which cuts down on "why is my site slow?" tickets.</p>
<p><img src="https://hpanel.net/screenshots/user/10.png" alt="Disk usage view in the HPanel user panel" />
<em>Disk usage breakdown in the user panel.</em></p>
<p><img src="https://hpanel.net/screenshots/user/16.png" alt="Performance view in the HPanel user panel" />
<em>Per-account performance and caching settings.</em></p>
<hr />
<h2>Choosing sensible limits</h2>
<p>Some practical guidance for setting package limits:</p>
<ol>
<li><strong>Start from your hardware, not your marketing.</strong> Add up what you're promising. If 50 accounts each get 2 GB RAM on a 32 GB server, you're relying on most of them being idle. Some overcommit is normal on shared hosting, but know your ratio.</li>
<li><strong>CPU caps should allow bursts.</strong> Page loads are spiky. A 1-CPU cap is usually fine for a small WordPress site. A 0.25-CPU cap makes every uncached request feel slow.</li>
<li><strong>Set <code>memory.high</code> below <code>memory.max</code></strong> (when you control both) so accounts get throttled before they get OOM-killed.</li>
<li><strong>Don't forget IO.</strong> If you only cap CPU and RAM, the next incident will be disk-bound.</li>
<li><strong>Keep process limits generous but finite.</strong> Real sites need dozens of processes (PHP workers, cron, mail). Hundreds is usually fine. "Unlimited" is how fork bombs win.</li>
<li><strong>Match PHP workers to RAM.</strong> PHP-FPM <code>max_children</code> × average PHP memory per process must fit inside the account's memory limit, or the account will hit its own ceiling under load.</li>
<li><strong>Watch, then adjust.</strong> Use live usage data for a couple of weeks before tightening anything.</li>
</ol>
<hr />
<h2>Debugging a throttled account</h2>
<p>When a customer says "my site is slow", isolation changes the question from "what's wrong with the server?" to "is this account hitting its limits?". On a generic cgroup v2 system you can check directly:</p>
<pre><code class="language-bash">cat /sys/fs/cgroup/&lt;account&gt;/cpu.stat        # nr_throttled, throttled_usec
cat /sys/fs/cgroup/&lt;account&gt;/memory.events   # high, max, oom_kill counters
cat /sys/fs/cgroup/&lt;account&gt;/io.stat         # bytes and ops per device
cat /sys/fs/cgroup/&lt;account&gt;/pids.current    # current process count
</code></pre>
<p>Rising <code>nr_throttled</code> means the CPU cap is binding. <code>oom_kill</code> events mean the memory limit is too low for the workload, or there's a leak. In a control panel, the same information should appear as per-account usage graphs, which is where you want your support team looking.</p>
<hr />
<h2>FAQ</h2>
<p><strong>Do cgroups slow down the server?</strong>
The accounting overhead is small. cgroups are what systemd already uses to manage every service on a modern Ubuntu server. The "slowdown" people notice is the limit doing its job: an account that hits its CPU cap gets throttled.</p>
<p><strong>Is cgroups isolation the same as running each account in a VM or container?</strong>
No. Accounts still share one kernel and one set of system services. cgroups limit <em>resources</em>. Namespaces, chroot jails and permissions limit <em>visibility</em>. Together they give strong practical isolation for shared hosting, but not the hard boundary of separate virtual machines.</p>
<p><strong>Can a customer raise their own limits?</strong>
Not when the limits are set on the cgroup by root, which is the point. Customers change limits by moving to a bigger package.</p>
<p><strong>What about Node.js, Python and Docker workloads?</strong>
They need the same treatment. Long-running app processes are exactly the kind that can creep up in memory over days. In HPanel, Docker container CPU and memory limits come from the hosting package, and containers count against the user's disk quota, so app hosting follows the same rules as PHP sites.</p>
<p><strong>Do I need a separate kernel or add-on for this?</strong>
Not with cgroup v2 on current Ubuntu LTS releases. The controllers are in the standard kernel. What you need is the tooling to apply limits per account, consistently, and to show usage.</p>
<hr />
<h2>Wrapping up</h2>
<p>Resource isolation turns shared hosting from "everyone suffers together" into "each account lives within what it bought". <strong>cgroups</strong> provide the kernel-level enforcement for CPU, memory, IO and processes. Filesystem quotas, a filesystem jail, per-account PHP pools and database limits complete the picture.</p>
<p>You can build all of it yourself. If you'd rather have it as the default on every account, <a href="https://hpanel.net/">HPanel</a> includes per-account cgroups limits, a filesystem jail, per-account PHP-FPM pools and a MySQL governor in every plan, with <strong>one flat license per server and no per-account fees</strong>. The setup guide is in the <a href="https://hpanel.net/docs.php">documentation</a>.</p>
<p><strong>Ready to isolate every account on your server? <a href="https://hpanel.net/pricing.php">Buy now</a>.</strong></p>
]]></content:encoded></item><item><title><![CDATA[Push-to-Deploy Node.js and Python Apps on Your Own Server with a Self-Hosted Control Panel]]></title><description><![CDATA[How Git auto-deploy, PM2, Gunicorn, Docker and per-account isolation fit together when you host apps on your own VPS with the HPanel control panel.]]></description><link>https://hpanelnet.hashnode.dev/push-to-deploy-nodejs-python-self-hosted-control-panel</link><guid isPermaLink="true">https://hpanelnet.hashnode.dev/push-to-deploy-nodejs-python-self-hosted-control-panel</guid><category><![CDATA[Devops articles]]></category><category><![CDATA[nodeJS Developer]]></category><category><![CDATA[Python 3]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[self-hosted]]></category><dc:creator><![CDATA[HPanel]]></dc:creator><pubDate>Mon, 26 Oct 2026 18:20:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6adf973801ed43c9b9f0380a/0e6544eb-0a2e-4097-b675-d10b30a0fd86.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Platform-as-a-service products made <code>git push</code> deployments feel normal. Once you move to your own VPS, though, you usually end up building the same pipeline by hand: a process manager, an Nginx reverse proxy, TLS certificates, a webhook receiver, a deploy script, a rollback plan, and some way to stop one app from eating the whole server.</p>
<p>This post walks through how that pipeline looks inside <strong>HPanel</strong>, a self-hosted web hosting control panel. HPanel puts Git auto-deploy, a Node.js app manager (PM2), a Python app manager (virtualenv + Gunicorn) and Docker into the same panel that handles domains, DNS, SSL and email. We'll look at what each piece does, how they connect, and what to put in your repo so deployments are predictable.</p>
<blockquote>
<p><strong>Disclosure:</strong> I'm on the HPanel team. Everything about <a href="https://hpanel.net/">HPanel</a> below comes from the product's public <a href="https://hpanel.net/features.php">features page</a> and <a href="https://hpanel.net/docs.php">docs</a>. The code is plain Node/Python that works anywhere.</p>
</blockquote>
<hr />
<h2>The problem we're solving</h2>
<p>A "simple" production deploy on a bare VPS usually involves all of this:</p>
<ol>
<li><strong>Code delivery:</strong> pulling the right branch when you push.</li>
<li><strong>Build steps:</strong> <code>npm ci</code>, <code>npm run build</code>, <code>pip install -r requirements.txt</code>, migrations.</li>
<li><strong>A process manager:</strong> keeping the app alive, restarting it on crash, running more than one instance.</li>
<li><strong>A reverse proxy:</strong> routing <code>api.example.com</code> to <code>127.0.0.1:3001</code> with TLS.</li>
<li><strong>Secrets:</strong> environment variables that live outside the repo.</li>
<li><strong>Rollback:</strong> getting back to the last good version when a deploy fails.</li>
<li><strong>Isolation:</strong> making sure one app (or one customer) can't starve everything else on the box.</li>
</ol>
<p>You can glue all of that together with shell scripts and systemd units, and many of us have. The trade-off is that every new app means writing the glue again, and every customer on a shared server multiplies it. A hosting control panel that treats these as first-class features turns most of the list into settings.</p>
<hr />
<h2>How HPanel splits the work</h2>
<p>HPanel has three panel levels: <strong>Admin</strong>, <strong>Reseller</strong> and <strong>User</strong>. Everything in this article happens in the <strong>User panel</strong>, the same place a hosting customer manages their files, email and databases. Each user gets:</p>
<ul>
<li><strong>Git Deploy</strong>: auto-deploy from GitHub, GitLab or Bitbucket.</li>
<li><strong>Node.js App Manager</strong>: deploy Node apps with PM2 and multiple Node versions.</li>
<li><strong>Python App Manager</strong>: Django, Flask, FastAPI and other WSGI/ASGI apps with virtualenv and Gunicorn.</li>
<li><strong>Docker Manager</strong>: per-user containers.</li>
<li><strong>Web terminal</strong>: a browser-based shell that runs as the user's Linux account.</li>
</ul>
<p><img src="https://hpanel.net/screenshots/user/20.png" alt="HPanel Git deployment settings for GitHub, GitLab and Bitbucket" />
<em>Git Deployment in the HPanel User panel: connect GitHub, GitLab or Bitbucket repositories.</em></p>
<p>Underneath, the account is isolated with <strong>Linux cgroups</strong>: per-account CPU, RAM, IO, disk, inode, bandwidth and process limits from the hosting package. There's also a chroot filesystem jail and a per-user MySQL governor. That isolation matters a lot once you let people run long-lived processes.</p>
<hr />
<h2>Part 1: Git auto-deploy</h2>
<p>According to HPanel's features page, Git Deploy supports:</p>
<table>
<thead>
<tr>
<th>Capability</th>
<th>What it means in practice</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Multi-repo</strong></td>
<td>Connect several repositories, each deploying to a different domain or directory</td>
</tr>
<tr>
<td><strong>Private repos</strong></td>
<td>Personal access token authentication for GitHub, GitLab and Bitbucket</td>
</tr>
<tr>
<td><strong>Webhook auto-deploy</strong></td>
<td>Push to Git → automatic deployment</td>
</tr>
<tr>
<td><strong>Domain-based deploy path</strong></td>
<td>Pick a domain plus an optional subdirectory as the target</td>
</tr>
<tr>
<td><strong>Pre/post-deploy scripts</strong></td>
<td>Run <code>npm install</code>, <code>composer install</code>, build commands, migrations</td>
</tr>
<tr>
<td><strong>Automatic rollback</strong></td>
<td>Failed deployments roll back to the last working version</td>
</tr>
<tr>
<td><strong>Disk quota protection</strong></td>
<td>Checks repo size against available space before deploying</td>
</tr>
<tr>
<td><strong>Deployment history</strong></td>
<td>A log of every deployment with commit hash, status and timing</td>
</tr>
</tbody></table>
<p>A typical setup looks like this:</p>
<ol>
<li><strong>Connect the repository.</strong> For a private repo, create a personal access token with read access to the repo on your Git host and add it in the panel. Use a token scoped to one repo, or a read-only token, rather than your account password.</li>
<li><strong>Pick the target.</strong> For example, <code>main</code> → <code>~/public_html</code> for a static frontend, or <code>main</code> → <code>~/api.example.com</code> for an API.</li>
<li><strong>Add the webhook.</strong> HPanel gives you a webhook URL for the repo. Paste it into your Git host's webhook settings, triggered on push.</li>
<li><strong>Define the deploy scripts.</strong> This is where your build steps go (examples below).</li>
</ol>
<p>From then on, a <code>git push</code> to the tracked branch triggers a deploy. If the scripts fail, HPanel rolls back to the last working version, and the deployment history records the commit hash and status either way.</p>
<h3>Make your repo deploy-friendly</h3>
<p>The panel can only be as reliable as your build. A few habits help a lot:</p>
<p><strong>Pin your toolchain.</strong> Commit a lockfile (<code>package-lock.json</code>, <code>poetry.lock</code>, or pinned <code>requirements.txt</code>) so a deploy never resolves different dependency versions than your tests did.</p>
<p><strong>Make deploy scripts idempotent.</strong> A script that can run twice safely is a script you can retry:</p>
<pre><code class="language-bash"># post-deploy (Node.js)
set -euo pipefail
npm ci --omit=dev
npm run build --if-present
</code></pre>
<pre><code class="language-bash"># post-deploy (Python / Django)
set -euo pipefail
source venv/bin/activate        # path depends on your app's virtualenv
pip install -r requirements.txt
python manage.py migrate --noinput
python manage.py collectstatic --noinput
</code></pre>
<p><code>set -euo pipefail</code> matters here. It makes the script exit non-zero on the first failure, so a broken build is reported as a failed deploy (and rolled back) instead of quietly half-succeeding.</p>
<p><strong>Keep secrets out of Git.</strong> Both the Node.js and Python app managers let you set environment variables from the dashboard UI. Read config from <code>process.env</code> / <code>os.environ</code> and commit a <code>.env.example</code> with dummy values only.</p>
<hr />
<h2>Part 2: Node.js apps with PM2</h2>
<p>HPanel's Node.js App Manager covers:</p>
<ul>
<li><strong>Multiple Node.js versions</strong>, selectable per app (18, 20, 22+),</li>
<li><strong>PM2 process management</strong> with auto-restart, cluster mode and log management,</li>
<li><strong>Environment variables</strong> set from the dashboard,</li>
<li><strong>Automatic Nginx config</strong> routing a domain to the Node.js app (reverse proxy),</li>
<li><strong>Live logs and stats</strong>: real-time output, CPU and memory usage.</li>
</ul>
<p>You can upload app files or deploy from Git, so the Git Deploy pipeline above feeds straight into it.</p>
<p><img src="https://hpanel.net/screenshots/user/18.png" alt="HPanel Node.js app manager" />
<em>The Node.js app manager in the User panel.</em></p>
<h3>A minimal production-ready Express app</h3>
<p>The panel handles the proxy and the process manager. Your app only has to do three things: listen on the port it's given, behave well behind a proxy, and shut down cleanly.</p>
<pre><code class="language-js">// server.js
const express = require('express');

const app = express();
app.set('trust proxy', 1); // we're behind Nginx: honor X-Forwarded-* headers

app.get('/health', (req, res) =&gt; res.json({ ok: true }));
app.get('/', (req, res) =&gt; res.send('Hello from a self-hosted Node app'));

const port = process.env.PORT || 3000;
const server = app.listen(port, () =&gt; console.log(`listening on ${port}`));

// PM2 sends SIGINT on reload/stop: finish in-flight requests, then exit
process.on('SIGINT', () =&gt; server.close(() =&gt; process.exit(0)));
</code></pre>
<p>Why each line matters:</p>
<ul>
<li><strong><code>process.env.PORT</code></strong>: the app manager allocates the port and the reverse proxy targets it, so never hard-code the port.</li>
<li><strong><code>trust proxy</code></strong>: without it, <code>req.ip</code> and <code>req.protocol</code> show the proxy rather than the client, which breaks rate limiting and HTTPS redirects.</li>
<li><strong>Graceful shutdown</strong>: PM2 restarts the process on deploy. Closing the server first means in-flight requests aren't cut off.</li>
</ul>
<h3>Cluster mode, with care</h3>
<p>PM2's cluster mode runs several instances of your app behind one port, which helps on multi-core machines. Two things to remember:</p>
<ol>
<li><strong>State must live outside the process.</strong> In-memory sessions or caches won't be shared across instances. Use Redis or your database instead (HPanel already includes Redis for object caching).</li>
<li><strong>Your instance count is bounded by your package.</strong> cgroups enforce the account's CPU and RAM limits. Four instances on a package with a small memory limit will just compete for the same budget. Check the live CPU and memory stats after a deploy and size the instance count to match.</li>
</ol>
<hr />
<h2>Part 3: Python apps with virtualenv and Gunicorn</h2>
<p>The Python App Manager provides:</p>
<ul>
<li><strong>Multiple Python versions</strong>: 3.10, 3.11 or 3.12 per app,</li>
<li><strong>virtualenv isolation</strong>: each app gets its own environment and packages,</li>
<li><strong>pip dependency management</strong>: install from <code>requirements.txt</code> with one click,</li>
<li><strong>Gunicorn</strong>: configurable workers and timeout,</li>
<li><strong>Automatic Nginx reverse proxy</strong> for domain → app routing with SSL,</li>
<li><strong>Environment variables</strong> from the dashboard,</li>
<li><strong>Framework support</strong>: Django, Flask, FastAPI, Bottle, Tornado, and any WSGI/ASGI framework.</li>
</ul>
<p><img src="https://hpanel.net/screenshots/user/19.png" alt="HPanel Python app manager" />
<em>The Python app manager with quick-start templates.</em></p>
<h3>Flask (WSGI)</h3>
<pre><code class="language-python"># app.py
import os
from flask import Flask, jsonify
from werkzeug.middleware.proxy_fix import ProxyFix

app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)  # behind Nginx
app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]        # set in the dashboard

@app.get("/health")
def health():
    return jsonify(ok=True)
</code></pre>
<pre><code class="language-text"># requirements.txt
flask==3.0.*
gunicorn==22.*
</code></pre>
<p>Gunicorn's entry point is <code>app:app</code> (module <code>app</code>, object <code>app</code>).</p>
<h3>FastAPI (ASGI)</h3>
<p>FastAPI is an ASGI app. Under Gunicorn, the standard approach is Uvicorn's worker class:</p>
<pre><code class="language-python"># main.py
import os
from fastapi import FastAPI

app = FastAPI()

@app.get("/health")
def health():
    return {"ok": True, "env": os.environ.get("APP_ENV", "production")}
</code></pre>
<pre><code class="language-text"># requirements.txt
fastapi
uvicorn[standard]
gunicorn
</code></pre>
<p>The equivalent command line is <code>gunicorn main:app -k uvicorn.workers.UvicornWorker</code>. Configure the worker class according to how your app manager setup exposes Gunicorn options. (This part is generic Gunicorn/Uvicorn usage, not HPanel-specific.)</p>
<h3>Sizing workers</h3>
<p>The classic Gunicorn rule of thumb is <code>2 × cores + 1</code> workers. On shared hosting, though, the number of cores <em>your account</em> can use is set by the package's cgroup CPU limit, not by the machine. Start small (2–3 workers), watch memory, and raise the count only if requests queue. Set a sensible timeout so a stuck request gets recycled instead of holding a worker forever.</p>
<hr />
<h2>Part 4: When a container makes more sense</h2>
<p>Some workloads don't fit "a Node or Python process behind Nginx": a specific database version, a queue worker image, or a service you'd rather run as published. HPanel's <strong>Docker Manager</strong> lets users:</p>
<ul>
<li>pull images from Docker Hub, custom registries or private registries,</li>
<li>create, start, stop and remove containers, with environment variables, port mapping, volume mounts and restart policies,</li>
<li>view live container logs.</li>
</ul>
<p>Container CPU and memory limits come from the hosting package, and containers use the user's disk quota, so the same isolation rules apply.</p>
<p><img src="https://hpanel.net/screenshots/user/17.png" alt="HPanel Docker manager with quick templates" />
<em>Per-user Docker management with quick templates.</em></p>
<p>A pattern that works well: run the <strong>app</strong> through the Node.js or Python manager (so it gets the managed reverse proxy, SSL and Git deploy), and run <strong>supporting services</strong> such as Redis or a worker in containers.</p>
<hr />
<h2>Part 5: Putting it together, step by step</h2>
<p>Here's the complete flow for an API at <code>api.example.com</code>:</p>
<ol>
<li><strong>Add the domain or subdomain</strong> in the User panel. SSL is issued automatically through Let's Encrypt once DNS points at the server.</li>
<li><strong>Create the app</strong> in the Node.js or Python manager: choose the runtime version and set the environment variables (<code>SECRET_KEY</code>, <code>DATABASE_URL</code>, …).</li>
<li><strong>Create a database</strong> if you need one (MySQL is in the User panel, with phpMyAdmin SSO).</li>
<li><strong>Connect the repository</strong> in Git Deploy, pointing <code>main</code> at the app's directory, and add the post-deploy script.</li>
<li><strong>Add the webhook</strong> to your Git host.</li>
<li><strong>Push.</strong> Check the deployment history for the commit hash and status, then open the app manager's live logs.</li>
<li><strong>Break it on purpose once.</strong> Push a commit with a failing build and confirm that the deploy is marked failed and rolled back. It's better to learn how rollback behaves on a quiet afternoon than during an incident.</li>
</ol>
<hr />
<h2>Things to keep in mind</h2>
<ul>
<li><strong>The panel handles the plumbing; your app still owns correctness.</strong> Health endpoints, graceful shutdown and idempotent migrations are still up to you.</li>
<li><strong>Treat tokens like passwords.</strong> Use scoped, read-only repo tokens for private-repo deploys.</li>
<li><strong>Watch your limits.</strong> Per-account isolation protects neighbours, which also means <em>your</em> app will hit its own ceiling first. Use the live CPU and memory stats.</li>
<li><strong>Don't edit files on the server that Git Deploy manages.</strong> The next deploy will overwrite them. Change the repo instead.</li>
</ul>
<hr />
<h2>Wrapping up</h2>
<p>The pieces of a <code>git push</code> workflow (webhooks, builds, a process manager, a reverse proxy, TLS, rollback and isolation) aren't exotic. They're just tedious to rebuild for every app and every customer. Having them as built-in, per-account features of a self-hosted control panel means a new app takes a few settings instead of a new set of scripts, and the same setup works for a single developer VPS or a hosting company with hundreds of accounts.</p>
<p>HPanel runs on Ubuntu 22.04/24.04 LTS, is licensed as <strong>one flat license per server</strong> with no per-account fees, and includes every feature in every plan, including Git deploy, the Node.js and Python app managers, and Docker. If you want this workflow on your own server, <strong><a href="https://hpanel.net/pricing.php">buy now on the HPanel pricing page</a></strong>, then follow the <a href="https://hpanel.net/docs.php">installation docs</a>.</p>
<p><em>Questions about the deploy flow, or edge cases you've hit with PM2 or Gunicorn behind a panel? Leave a comment and I'll answer.</em></p>
]]></content:encoded></item></channel></rss>