<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ShoHost</title>
    <description>The latest articles on DEV Community by ShoHost (@shohost).</description>
    <link>https://dev.to/shohost</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4185739%2F917cbf19-80d6-4fa9-9932-76d5560cb0e8.png</url>
      <title>DEV Community: ShoHost</title>
      <link>https://dev.to/shohost</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shohost"/>
    <language>en</language>
    <item>
      <title>WP-Cron Is Not a Cron: Moving WordPress to a Real Scheduler on cPanel</title>
      <dc:creator>ShoHost</dc:creator>
      <pubDate>Mon, 26 Oct 2026 05:09:09 +0000</pubDate>
      <link>https://dev.to/shohost/wp-cron-is-not-a-cron-moving-wordpress-to-a-real-scheduler-on-cpanel-5327</link>
      <guid>https://dev.to/shohost/wp-cron-is-not-a-cron-moving-wordpress-to-a-real-scheduler-on-cpanel-5327</guid>
      <description>&lt;p&gt;WordPress has a scheduler called WP-Cron, and it handles more than most people realise: scheduled posts, WooCommerce order cleanups, backup plugins, security scans, update checks. The catch is in how it runs. WP-Cron isn't a real cron job. It only fires when someone visits your site.&lt;/p&gt;

&lt;p&gt;That one design choice causes two opposite problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quiet sites miss jobs.&lt;/strong&gt; If nobody visits at 3 AM, the 3 AM backup doesn't start until the first visitor arrives. Scheduled posts show "Missed schedule".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Busy sites run it too often.&lt;/strong&gt; On every page load, WordPress checks whether anything is due and may spawn a loopback request to &lt;code&gt;wp-cron.php&lt;/code&gt;. Under traffic, that adds up to a lot of pointless PHP work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix is the same on almost any cPanel account: switch off the visit-triggered version and call WP-Cron from the server's own scheduler instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Turn off the page-load trigger
&lt;/h3&gt;

&lt;p&gt;Open &lt;code&gt;wp-config.php&lt;/code&gt; and add this line above &lt;code&gt;/* That's all, stop editing! */&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'DISABLE_WP_CRON'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This doesn't remove WP-Cron. Scheduled events still exist; they just stop firing on page loads. Don't save this change without doing step 2, or nothing scheduled will run at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Add a real cron job in cPanel
&lt;/h3&gt;

&lt;p&gt;In cPanel, open &lt;strong&gt;Advanced &amp;gt; Cron Jobs&lt;/strong&gt;, pick a schedule, and add a command. Every 5 minutes is a sensible default for most sites; every 15 is fine for a small blog.&lt;/p&gt;

&lt;p&gt;The most reliable command runs WordPress through PHP directly, so it doesn't depend on DNS, SSL or a firewall letting a request back in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /home/USERNAME/public_html &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; /usr/local/bin/php wp-cron.php &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;USERNAME&lt;/code&gt; and the path with your own. If WP-CLI is available on your account, this is even cleaner, because it only runs events that are actually due:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /home/USERNAME/public_html &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; wp cron event run &lt;span class="nt"&gt;--due-now&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check which PHP binary matches your site's PHP version. On many cPanel servers the default &lt;code&gt;php&lt;/code&gt; in cron is a different version from the one your site uses, which is a classic "works in the browser, fails in cron" cause. Your host's docs or &lt;code&gt;which php&lt;/code&gt; over SSH will tell you the right path.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Check it's actually running
&lt;/h3&gt;

&lt;p&gt;Install a cron viewer plugin such as WP Crontrol for a few days, or run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp cron event list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the "next run" column. If events keep sliding into the past and never run, the cron command is failing. Remove &lt;code&gt;&amp;gt; /dev/null 2&amp;gt;&amp;amp;1&lt;/code&gt; temporarily and set a cron email address in cPanel so you can see the error.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. A few things to watch
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multisite:&lt;/strong&gt; each site has its own queue. With WP-CLI, loop over &lt;code&gt;wp site list --field=url&lt;/code&gt; and pass &lt;code&gt;--url=&lt;/code&gt; to each run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavy jobs:&lt;/strong&gt; if a backup plugin runs for 20 minutes, a 5-minute cron can start overlapping runs. Most backup plugins lock themselves, but check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staging copies:&lt;/strong&gt; clone a site and you clone &lt;code&gt;DISABLE_WP_CRON&lt;/code&gt;. Add the cron job on staging too, or turn the constant off there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This takes ten minutes and makes scheduled tasks predictable, which is usually what people actually wanted when they scheduled them. If you'd rather start on hosting that already gives you cPanel cron, SSH and a choice of PHP versions out of the box, take a look at &lt;a href="https://shohost.com/wp-hosting" rel="noopener noreferrer"&gt;ShoHost's cPanel WordPress hosting&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>cron</category>
      <category>webdev</category>
    </item>
    <item>
      <title>First 30 Minutes on a New Windows RDP Server: A Hardening Checklist</title>
      <dc:creator>ShoHost</dc:creator>
      <pubDate>Sun, 25 Oct 2026 05:09:23 +0000</pubDate>
      <link>https://dev.to/shohost/first-30-minutes-on-a-new-windows-rdp-server-a-hardening-checklist-26h9</link>
      <guid>https://dev.to/shohost/first-30-minutes-on-a-new-windows-rdp-server-a-hardening-checklist-26h9</guid>
      <description>&lt;p&gt;A Windows server with Remote Desktop open to the internet starts getting password guesses within hours of going online. Bots scan the whole IPv4 range for port 3389 and try &lt;code&gt;Administrator&lt;/code&gt; with every leaked password they have. None of this is targeted at you, which is exactly why the basics stop most of it.&lt;/p&gt;

&lt;p&gt;Here's the checklist I run on any fresh Windows RDP box before installing a single app. It takes about half an hour and needs nothing beyond what ships with Windows.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Patch before anything else
&lt;/h3&gt;

&lt;p&gt;Open Windows Update, install everything, reboot, and check again. Fresh images are often a few months behind, and RDP-related vulnerabilities have been some of the most exploited Windows bugs of the last few years. Don't put this off until "after setup".&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Stop using the built-in Administrator account
&lt;/h3&gt;

&lt;p&gt;Every brute-force tool tries &lt;code&gt;Administrator&lt;/code&gt; first. Create your own admin account with a long passphrase (16+ characters, never reused anywhere):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$pw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Read-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AsSecureString&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Password"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;New-LocalUser&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"opsadmin"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$pw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PasswordNeverExpires&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Add-LocalGroupMember&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Group&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Administrators"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Member&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"opsadmin"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sign out, connect again as the new account, and only once that works, disable the built-in one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Disable-LocalUser&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Administrator"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a name that isn't obvious. &lt;code&gt;admin&lt;/code&gt;, &lt;code&gt;user&lt;/code&gt; and your company name are all on the guess lists.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Turn on account lockout
&lt;/h3&gt;

&lt;p&gt;By default, Windows lets an attacker keep guessing. A lockout policy slows them down to a crawl:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/lockoutthreshold:5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/lockoutwindow:30&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/lockoutduration:30&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That locks an account for 30 minutes after 5 bad passwords within 30 minutes. You can set the same thing in &lt;code&gt;secpol.msc&lt;/code&gt; under Account Policies &amp;gt; Account Lockout Policy. Run &lt;code&gt;net accounts&lt;/code&gt; afterwards to confirm the values stuck.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Make sure Network Level Authentication is on
&lt;/h3&gt;

&lt;p&gt;With NLA, the client has to authenticate before Windows builds a full login session, which cuts the load from bots and blocks some pre-login attacks. It's on by default on current Windows, but images and old guides sometimes switch it off. Check it in System Properties &amp;gt; Remote ("Allow connections only from computers running Remote Desktop with Network Level Authentication"), or from PowerShell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;UserAuthentication&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A value of &lt;code&gt;1&lt;/code&gt; means NLA is required. If it's &lt;code&gt;0&lt;/code&gt;, set it back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Set-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;UserAuthentication&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Limit who can reach the port (only if your IP is static)
&lt;/h3&gt;

&lt;p&gt;If you always connect from an office with a fixed IP, tell Windows Firewall to accept RDP from that address only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Set-NetFirewallRule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-DisplayGroup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Remote Desktop"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-RemoteAddress&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;203.0.113.10&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the example address with yours. Be careful here: if your home or office IP changes, this rule locks you out too. If your connection uses a dynamic IP, skip this step and rely on the lockout policy and a strong passphrase.&lt;/p&gt;

&lt;p&gt;About changing the RDP port: it cuts log noise but it isn't real security, and some hosted plans only open the standard RDP port upstream. Check with your provider before you touch it.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Look at who's knocking
&lt;/h3&gt;

&lt;p&gt;Failed logons are logged as event ID 4625 in the Security log. A quick look tells you whether the bots have found you yet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-WinEvent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-FilterHashtable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="nx"&gt;LogName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Security'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4625&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-MaxEvents&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;TimeCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Account'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Source'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see hundreds of attempts against names like &lt;code&gt;admin&lt;/code&gt; or &lt;code&gt;test&lt;/code&gt;, the lockout policy and the renamed admin account are doing their job. If you see attempts against your real admin name, that name has leaked somewhere, so change it along with the password.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Treat it as a server, not a laptop
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Keep Microsoft Defender on and check it with &lt;code&gt;Get-MpComputerStatus&lt;/code&gt; (look for &lt;code&gt;RealTimeProtectionEnabled : True&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Don't browse random sites or open email attachments on the server. Most "my RDP got hacked" stories start with a download, not a brute-forced password.&lt;/li&gt;
&lt;li&gt;Give each person their own login instead of sharing one admin password over chat, and remove accounts the day someone stops needing them.&lt;/li&gt;
&lt;li&gt;Keep copies of important files somewhere other than the server itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  8. Write down what you changed
&lt;/h3&gt;

&lt;p&gt;A short note with the admin account name, the lockout settings and any firewall rules saves a lot of guesswork six months later, especially when someone else has to log in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where to run it
&lt;/h3&gt;

&lt;p&gt;All of this works on any Windows machine you can reach over RDP. If you want a remote Windows desktop without buying hardware, &lt;a href="https://rdp.shohost.com/" rel="noopener noreferrer"&gt;ShoHost's Private Admin RDP plans&lt;/a&gt; let you install any Windows OS with admin access. Plans start at 2 vCore, 2 GB RAM and 50 GB SSD and go up to 8 vCore with 16 GB RAM, all on a 200 Mbps port with unlimited bandwidth. Only the RDP port is open, which suits a remote desktop but not a public web server. The entry plan comes to ₹410.97 a month, all fees included. Questions before you order? Write to &lt;a href="mailto:support@shohost.com"&gt;support@shohost.com&lt;/a&gt; or WhatsApp +91 94880 91432, 9 AM to 12 midnight IST.&lt;/p&gt;

</description>
      <category>windows</category>
      <category>security</category>
      <category>rdp</category>
      <category>sysadmin</category>
    </item>
    <item>
      <title>Your WHMCS Cron Stopped. Nobody Noticed for a Week.</title>
      <dc:creator>ShoHost</dc:creator>
      <pubDate>Sun, 25 Oct 2026 05:09:06 +0000</pubDate>
      <link>https://dev.to/shohost/your-whmcs-cron-stopped-nobody-noticed-for-a-week-2828</link>
      <guid>https://dev.to/shohost/your-whmcs-cron-stopped-nobody-noticed-for-a-week-2828</guid>
      <description>&lt;p&gt;A dead WHMCS cron doesn't throw an error page. Invoices simply stop going out, overdue services stay active, and the first alarm is usually a client asking why they never got a bill.&lt;/p&gt;

&lt;p&gt;When it happens, the first job is to work out which kind of failure you have, because the fixes are different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never called vs. called and crashing.&lt;/strong&gt; The Cron Status report behind the Automation Status badge tells you. If Cron Setup and Invocation Frequency fail, cron never reaches WHMCS. If only Daily Cron Completing fails, it starts and dies partway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run it by hand, loudly.&lt;/strong&gt; Running &lt;code&gt;cron.php all -F -vvv&lt;/code&gt; from SSH as the account that owns WHMCS prints the real &lt;code&gt;[ERROR]&lt;/code&gt; line instead of a coloured badge. Do it once, not on a schedule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The usual suspect is PHP.&lt;/strong&gt; The bare &lt;code&gt;php&lt;/code&gt; in a crontab is whatever comes first in cron's PATH, which often isn't the version your site uses, and it may not have the ionCube Loader.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the clock too.&lt;/strong&gt; If CLI PHP and web PHP disagree on timezone, the daily run can keep missing your Time of Day hour.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"Works in SSH, fails in cron" almost always comes down to that different environment.&lt;/p&gt;

&lt;p&gt;The guide covers ten causes in the order we'd check them on a real server, with the commands, sample output, a post-fix checklist and prevention tips. Read &lt;a href="https://blog.shohost.com/fix-whmcs-cron-not-running/" rel="noopener noreferrer"&gt;the full WHMCS cron troubleshooting walkthrough&lt;/a&gt; on the ShoHost blog.&lt;/p&gt;

</description>
      <category>whmcs</category>
      <category>php</category>
      <category>cron</category>
      <category>hosting</category>
    </item>
    <item>
      <title>Self-Hosting n8n with Docker Compose: Five Settings to Get Right on Day One</title>
      <dc:creator>ShoHost</dc:creator>
      <pubDate>Wed, 21 Oct 2026 05:12:54 +0000</pubDate>
      <link>https://dev.to/shohost/self-hosting-n8n-with-docker-compose-five-settings-to-get-right-on-day-one-i96</link>
      <guid>https://dev.to/shohost/self-hosting-n8n-with-docker-compose-five-settings-to-get-right-on-day-one-i96</guid>
      <description>&lt;p&gt;Getting n8n running in Docker takes five minutes. Keeping it running for a year without losing credentials, webhooks or disk space takes a few extra settings that the quick-start skips. Here's a Compose setup with the five I'd set on day one.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Compose file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${PG_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pg_data:/var/lib/postgresql/data&lt;/span&gt;

  &lt;span class="na"&gt;n8n&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.n8n.io/n8nio/n8n&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:5678:5678"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DB_TYPE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresdb&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${PG_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;N8N_ENCRYPTION_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${N8N_ENCRYPTION_KEY}&lt;/span&gt;
      &lt;span class="na"&gt;N8N_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n.example.com&lt;/span&gt;
      &lt;span class="na"&gt;N8N_PROTOCOL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;
      &lt;span class="na"&gt;WEBHOOK_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://n8n.example.com/&lt;/span&gt;
      &lt;span class="na"&gt;GENERIC_TIMEZONE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Asia/Kolkata&lt;/span&gt;
      &lt;span class="na"&gt;EXECUTIONS_DATA_PRUNE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
      &lt;span class="na"&gt;EXECUTIONS_DATA_MAX_AGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;168&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;n8n_data:/home/node/.n8n&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pg_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;n8n_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put &lt;code&gt;PG_PASSWORD&lt;/code&gt; and &lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt; in a &lt;code&gt;.env&lt;/code&gt; file next to it (&lt;code&gt;openssl rand -hex 32&lt;/code&gt; makes a good key), then &lt;code&gt;docker compose up -d&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Pin your own encryption key
&lt;/h3&gt;

&lt;p&gt;n8n encrypts every saved credential with &lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt;. If you don't set one, it generates a key inside the data volume. Rebuild the container without that volume and every credential becomes unreadable. Set it yourself and back it up somewhere other than the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use Postgres, not the default SQLite
&lt;/h3&gt;

&lt;p&gt;SQLite is fine for a test. Once you have busy webhooks and thousands of executions, Postgres handles concurrent writes better and is far easier to back up with &lt;code&gt;pg_dump&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Tell n8n its public URL
&lt;/h3&gt;

&lt;p&gt;Behind a reverse proxy, n8n doesn't know its own address, so webhook nodes show &lt;code&gt;localhost:5678&lt;/code&gt; URLs that nothing outside can reach. &lt;code&gt;WEBHOOK_URL&lt;/code&gt; fixes that. Bind the port to &lt;code&gt;127.0.0.1&lt;/code&gt; as above and let Caddy or Nginx handle HTTPS in front.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Prune old executions
&lt;/h3&gt;

&lt;p&gt;Every run is stored with its input and output data. Without pruning, the database grows until the disk fills. &lt;code&gt;EXECUTIONS_DATA_MAX_AGE: 168&lt;/code&gt; keeps one week of history; raise it if you need longer audits.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Set the timezone
&lt;/h3&gt;

&lt;p&gt;Schedule Trigger nodes use &lt;code&gt;GENERIC_TIMEZONE&lt;/code&gt;. Leave it unset and your "9 AM" workflow runs at 9 AM UTC, which is 2:30 PM in India.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backups and updates
&lt;/h3&gt;

&lt;p&gt;A nightly &lt;code&gt;pg_dump&lt;/code&gt; of the n8n database plus a copy of your &lt;code&gt;.env&lt;/code&gt; is enough to rebuild the whole instance. To update, run &lt;code&gt;docker compose pull &amp;amp;&amp;amp; docker compose up -d&lt;/code&gt;, but read the n8n release notes first, because major versions sometimes change node behaviour.&lt;/p&gt;

&lt;h3&gt;
  
  
  If you'd rather skip the server work
&lt;/h3&gt;

&lt;p&gt;All of the above is yours to maintain: OS patches, Docker updates, backups and HTTPS renewals. If you want a private n8n instance without looking after the box, &lt;a href="https://shohost.com/n8n-self-hosting" rel="noopener noreferrer"&gt;ShoHost's managed n8n hosting&lt;/a&gt; runs each instance in isolated Docker on its own KVM plan, with no per-step execution limits. Support is on WhatsApp (+91 94880 91432) and email (&lt;a href="mailto:support@shohost.com"&gt;support@shohost.com&lt;/a&gt;), 9 AM to 12 midnight IST.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>docker</category>
      <category>automation</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Five People, Five Full Admins: Fixing WHMCS Staff Access</title>
      <dc:creator>ShoHost</dc:creator>
      <pubDate>Wed, 21 Oct 2026 05:12:31 +0000</pubDate>
      <link>https://dev.to/shohost/five-people-five-full-admins-fixing-whmcs-staff-access-2in2</link>
      <guid>https://dev.to/shohost/five-people-five-full-admins-fixing-whmcs-staff-access-2in2</guid>
      <description>&lt;p&gt;Most WHMCS installs start with one staff account: the owner's, with every permission switched on. Then a support hire gets the same login "just for now", a freelancer keeps the account they used to fix a module, and a billing assistant leaves without anyone switching them off. A year later five people can do anything, and the Admin Log can't tell you who did what.&lt;/p&gt;

&lt;p&gt;The good news is that WHMCS already has the tools to fix this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Administrator roles.&lt;/strong&gt; WHMCS ships with Full, Sales and Support Only, and you can add custom roles such as Billing or Module debug. Keep Full to one or two people.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the permission types.&lt;/strong&gt; List and View are low risk. Delete and Configure are the dangerous ones, and some Create permissions quietly need Manage too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invite, don't hand out passwords.&lt;/strong&gt; Send Invite Link lets new staff pick their own credentials, so no password travels through chat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offboard properly.&lt;/strong&gt; Disable old accounts and move API credentials off personal admins before someone leaves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Roles still can't stop someone who has a valid password, which is where a second login factor comes in.&lt;/p&gt;

&lt;p&gt;The full guide has a sample role plan for a small hosting team, the permissions worth singling out and an offboarding checklist. Read &lt;a href="https://blog.shohost.com/whmcs-staff-admin-roles/" rel="noopener noreferrer"&gt;our guide to WHMCS admin roles, least privilege and offboarding&lt;/a&gt; on the ShoHost blog.&lt;/p&gt;

</description>
      <category>whmcs</category>
      <category>security</category>
      <category>hosting</category>
      <category>devops</category>
    </item>
    <item>
      <title>Encrypted Nightly Server Backups to an S3 Storage Box with rclone</title>
      <dc:creator>ShoHost</dc:creator>
      <pubDate>Tue, 20 Oct 2026 05:08:31 +0000</pubDate>
      <link>https://dev.to/shohost/encrypted-nightly-server-backups-to-an-s3-storage-box-with-rclone-348k</link>
      <guid>https://dev.to/shohost/encrypted-nightly-server-backups-to-an-s3-storage-box-with-rclone-348k</guid>
      <description>&lt;p&gt;Most server backup plans fail in one of two ways: the backups live on the same machine they're meant to protect, or nobody has ever tried a restore. This walkthrough fixes both with rclone, an S3-compatible storage box and one cron job. Everything here works with any S3 endpoint; I'll use &lt;a href="https://shohost.com/sdrive" rel="noopener noreferrer"&gt;ShoHost SDrive, an S3-compatible storage box&lt;/a&gt; as the example target because it also speaks SFTP, WebDAV and SMB and keeps its own snapshots.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Install rclone
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://rclone.org/install.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;bash
rclone version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Add the storage box as a remote
&lt;/h3&gt;

&lt;p&gt;Grab the S3 endpoint, access key and secret key from your storage provider's dashboard, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rclone config create sdrive s3 &lt;span class="nv"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Other &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;access_key_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_ACCESS_KEY &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;secret_access_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_SECRET_KEY &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://YOUR_S3_ENDPOINT

rclone &lt;span class="nb"&gt;mkdir &lt;/span&gt;sdrive:server-backups
rclone lsd sdrive:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you'd rather not use S3, the same box over SFTP works too: &lt;code&gt;rclone config create sdrive sftp host=YOUR_HOST user=YOUR_USER key_file=~/.ssh/id_ed25519&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Encrypt before anything leaves the server
&lt;/h3&gt;

&lt;p&gt;An rclone &lt;code&gt;crypt&lt;/code&gt; remote wraps the bucket so file contents and names are encrypted on your side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rclone config create sdrive-crypt crypt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;remote&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sdrive:server-backups &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'LONG_RANDOM_PASSPHRASE'&lt;/span&gt; &lt;span class="nt"&gt;--obscure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store that passphrase somewhere other than the server. Lose it and the backups are unreadable, which is the point.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The backup script
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# /usr/local/bin/offsite-backup.sh&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;STAMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/backups/db

&lt;span class="c"&gt;# Consistent MySQL/MariaDB dump without locking InnoDB tables&lt;/span&gt;
mysqldump &lt;span class="nt"&gt;--single-transaction&lt;/span&gt; &lt;span class="nt"&gt;--all-databases&lt;/span&gt; | &lt;span class="nb"&gt;gzip&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /var/backups/db/all-&lt;span class="nv"&gt;$STAMP&lt;/span&gt;.sql.gz

&lt;span class="c"&gt;# Databases: copy new dumps, keep 30 days remote&lt;/span&gt;
rclone copy /var/backups/db sdrive-crypt:db &lt;span class="nt"&gt;--log-file&lt;/span&gt; /var/log/rclone-backup.log
rclone delete sdrive-crypt:db &lt;span class="nt"&gt;--min-age&lt;/span&gt; 30d

&lt;span class="c"&gt;# Site files: mirror, but move changed/deleted files into a dated folder instead of losing them&lt;/span&gt;
rclone &lt;span class="nb"&gt;sync&lt;/span&gt; /var/www sdrive-crypt:www &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--backup-dir&lt;/span&gt; sdrive-crypt:www-archive/&lt;span class="nv"&gt;$STAMP&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--transfers&lt;/span&gt; 4 &lt;span class="nt"&gt;--log-file&lt;/span&gt; /var/log/rclone-backup.log

&lt;span class="c"&gt;# Local dumps older than 7 days&lt;/span&gt;
find /var/backups/db &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.sql.gz'&lt;/span&gt; &lt;span class="nt"&gt;-mtime&lt;/span&gt; +7 &lt;span class="nt"&gt;-delete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--backup-dir&lt;/code&gt; is the part people skip. Plain &lt;code&gt;sync&lt;/code&gt; deletes remote files that were deleted locally, so a bad deploy or ransomware on the server would be mirrored straight into your backup. With &lt;code&gt;--backup-dir&lt;/code&gt;, anything overwritten or removed is kept under a dated folder.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Schedule it
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;700 /usr/local/bin/offsite-backup.sh
&lt;span class="nb"&gt;sudo &lt;/span&gt;crontab &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="c"&gt;# 02:30 every night&lt;/span&gt;
30 2 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /usr/local/bin/offsite-backup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Prove the restore works
&lt;/h3&gt;

&lt;p&gt;A backup you haven't restored is a guess. Once a month:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /tmp/restore-test
rclone copy sdrive-crypt:db /tmp/restore-test &lt;span class="nt"&gt;--max-age&lt;/span&gt; 2d
&lt;span class="nb"&gt;gunzip&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; /tmp/restore-test/&lt;span class="k"&gt;*&lt;/span&gt;.sql.gz &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"dump OK"&lt;/span&gt;
rclone check /var/www sdrive-crypt:www &lt;span class="nt"&gt;--one-way&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rclone check&lt;/code&gt; compares sizes and hashes between the server and the remote, so you find a silent failure now rather than on the day you need the files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the storage box helps
&lt;/h3&gt;

&lt;p&gt;rclone handles encryption and retention, but a second safety net on the storage side is worth having. Snapshots on the box let you roll back even if a compromised server pushes bad data during a sync. That's the main reason I like pointing this setup at a storage box rather than a plain bucket. SDrive includes snapshots and versioning on its plans, and there's a small free tier if you want to test the script before committing.&lt;/p&gt;

&lt;p&gt;That's the whole setup: one remote, one encrypted wrapper, one script, one cron line and a monthly restore test.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>backup</category>
      <category>devops</category>
      <category>sysadmin</category>
    </item>
    <item>
      <title>Passkey, Authenticator App or Email Code: Picking 2FA for WHMCS Staff</title>
      <dc:creator>ShoHost</dc:creator>
      <pubDate>Tue, 20 Oct 2026 05:07:40 +0000</pubDate>
      <link>https://dev.to/shohost/passkey-authenticator-app-or-email-code-picking-2fa-for-whmcs-staff-1l54</link>
      <guid>https://dev.to/shohost/passkey-authenticator-app-or-email-code-picking-2fa-for-whmcs-staff-1l54</guid>
      <description>&lt;p&gt;Every WHMCS staff login should ask for something after the password. The harder question is what that something should be, because the three common options don't protect you equally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Email codes&lt;/strong&gt; are the easiest to roll out and the weakest for admins. Whoever gets into the staff mailbox can usually reset the WHMCS password and read the login code too, and NIST SP 800-63B says email shall not be used for out-of-band authentication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authenticator apps (TOTP)&lt;/strong&gt; are what WHMCS ships with as Time-Based Tokens, and they're a big step up from a password alone. Their weak spot is phishing: a look-alike login page can collect the 6-digit code and replay it to your real login before it expires.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Passkeys&lt;/strong&gt; close that gap. The browser only offers a passkey on the domain it was created for, so a fake page gets nothing to relay. WHMCS doesn't include passkey sign-in in its built-in two-factor list, which is why this needs an add-on.&lt;/p&gt;

&lt;p&gt;The full guide covers how each method behaves at a WHMCS login, where email codes still make sense (clients and backup), what to do about lost phones and clock drift, and a simple policy you can copy for your team. Read &lt;a href="https://blog.shohost.com/whmcs-passkeys-vs-totp/" rel="noopener noreferrer"&gt;our full passkeys vs TOTP vs email codes comparison for WHMCS staff&lt;/a&gt; on the ShoHost blog.&lt;/p&gt;

</description>
      <category>whmcs</category>
      <category>security</category>
      <category>webauthn</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why Your WHMCS Admin Login Needs More Than a Password</title>
      <dc:creator>ShoHost</dc:creator>
      <pubDate>Mon, 19 Oct 2026 08:40:33 +0000</pubDate>
      <link>https://dev.to/shohost/why-your-whmcs-admin-login-needs-more-than-a-password-4b0i</link>
      <guid>https://dev.to/shohost/why-your-whmcs-admin-login-needs-more-than-a-password-4b0i</guid>
      <description>&lt;p&gt;If you run a hosting business on WHMCS, your admin area is the most valuable login you own. It controls client data, invoices, payment gateways and every service you sell. That is exactly why bots hammer WHMCS admin login pages with brute-force and credential-stuffing attempts every day.&lt;/p&gt;

&lt;p&gt;A single reused or leaked admin password can lead to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fraudulent orders and refunds&lt;/li&gt;
&lt;li&gt;Leaked client details&lt;/li&gt;
&lt;li&gt;Changed payment settings and chargebacks&lt;/li&gt;
&lt;li&gt;Lost trust that is hard to win back&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Basic steps like a strong password and limiting failed logins help, but a password on its own is still a single point of failure.&lt;/p&gt;

&lt;p&gt;We wrote a full breakdown of the main risks and the practical steps to lock down your WHMCS admin area on our blog: &lt;a href="https://blog.shohost.com/harden-whmcs-admin-area/" rel="noopener noreferrer"&gt;How to harden your WHMCS admin area&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want stronger login protection without the hassle, &lt;a href="https://shohost.com/whmcs-secure-auth" rel="noopener noreferrer"&gt;ShoSecure Auth for WHMCS&lt;/a&gt; is our in-house module that adds an extra security layer to WHMCS logins, so a stolen password is not enough to get in. It costs $12.10 a year, all fees included, and comes with a 15-day free trial and a 7-day money-back guarantee.&lt;/p&gt;

</description>
      <category>whmcs</category>
      <category>security</category>
      <category>webhosting</category>
      <category>php</category>
    </item>
    <item>
      <title>WhatsApp Notifications for WHMCS</title>
      <dc:creator>ShoHost</dc:creator>
      <pubDate>Thu, 15 Oct 2026 18:39:59 +0000</pubDate>
      <link>https://dev.to/shohost/how-to-add-whatsapp-notifications-to-whmcs-step-by-step-4982</link>
      <guid>https://dev.to/shohost/how-to-add-whatsapp-notifications-to-whmcs-step-by-step-4982</guid>
      <description>&lt;p&gt;Want your WHMCS clients to get WhatsApp messages the moment an invoice is created, a payment lands or a ticket gets a reply? Email alone gets missed. WhatsApp gets read.&lt;/p&gt;

&lt;p&gt;Building this yourself means Meta Business approval, message templates, PHP hooks, error handling and ongoing maintenance every time WHMCS or the WhatsApp API changes.&lt;/p&gt;

&lt;p&gt;The ShoHost WHMCS WhatsApp Module does it out of the box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatic WhatsApp alerts for invoices, payments, orders and support tickets&lt;/li&gt;
&lt;li&gt;OTP and verification messages&lt;/li&gt;
&lt;li&gt;Bulk campaigns to your client list&lt;/li&gt;
&lt;li&gt;Delivery tracking&lt;/li&gt;
&lt;li&gt;Works with the latest WHMCS versions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It costs $2/month, with a 15-day free trial and a 7-day refund.&lt;/p&gt;

&lt;p&gt;See all features, requirements and setup details on the &lt;a href="https://shohost.com/whmcs-whatsapp-module" rel="noopener noreferrer"&gt;ShoHost WHMCS WhatsApp Module&lt;/a&gt; page.&lt;/p&gt;

</description>
      <category>whmcs</category>
      <category>php</category>
      <category>whatsapp</category>
      <category>webhosting</category>
    </item>
  </channel>
</rss>
