Managing your own VPS has some big advantages:

  • Web pages typically load faster and more reliably than on shared hosting.
  • You can resolve problems quickly and access log files without waiting for a support ticket to be answered.
  • You have complete control – install any software package you want and any number of websites or email accounts.
  • Secure shell (SSH) access allows fast secure file transfers, remote backups and command-line control.
  • Full isolation from other users greatly reduces your vulnerability to being hacked, or your IP address being blacklisted.
  • You can give private accounts to friends or customers.
  • You avoid expensive management fees and control panel licenses.
  • It’s easier to move everything to an upgraded server or backup server if necessary.
  • You gain a lot of knowledge about how web servers work.

Some disadvantage are:

  • A VPS is typically more expensive than “free” or “shared” hosting.
  • Some technical ability is needed, and time to learn server management skills.
  • Some regular effort is needed to monitor and maintain the server to keep it secure and reliable.
  • Although you have guaranteed minimum resources (RAM, CPU, disk space, bandwidth) on a VPS, the maximum available may be less than on shared hosting. 

Getting started

Before you can load websites onto a VPS you will need:

  • An account with a web hosting service that gives you administrative access to a VPS with sufficient resources to run your web sites. This typically involves paying a monthly fee – it’s best to avoid long-term contracts until you’ve confirmed that the server satisfies your needs. Some guidelines for choosing a good host are below. You will be given at least one unique IP address for the server and an administrator username and password that allow you to install an operating system and reboot the server. Such accounts can usually be created in minutes but may take longer if payment has to be verified.
  • Administrative access to a domain name for each web site. This typically involves paying a small yearly fee to a domain registrar. They will give you a username and password that allow you to change the Domain Name System (DNS) settings for your domain. It can take 24 hours or more for these new settings to completely propagate around the global domain name system. In the meantime you can access your VPS directly by IP address. Be careful about buying your domain name from the same company that hosts your sites, because that makes it much harder to move to a new host if there’s an account dispute or the hosting company goes bust.
  • A suitable operating system installed on the server – some guidelines for choosing one are below. Usually the hosting service will install a standard operating system for you – if not, you may have to load the OS from an “iso” file. You would normally load the “server” (not “desktop”) edition of an operating system in this situation.
  • If the server is remote (no screen or keyboard access) you will need to install client programs such as PuTTY and WinSCP on Windows or Filezilla on a Mac to send commands and files to your VPS over a secure shell (SSH) connection. You can usually also enter commands from a console window at the hosting company website, which is useful if you lock yourself out of the SSH connection.
If it helps, my current VPS host is TransIP in Amsterdam and uses the KVM hypervisor, my domain registrars are Namecheap and, my nameservers are at Cloudflare and my operating system is Ubuntu 22.04 LTS – but you should choose your own depending on your requirements.

Choosing a hosting company

When choosing a hosting company look for reliability, good network connectivity and good support as well as cost. You’re unlikely to get these from a PC under your desk. Redundant network connectivity and power supplies are very desirable, also good physical security and some sort of backup system. It’s hard to find objective reviews but is a good place to start.

Here are some suggested things to check when moving to a new VPS hosting company:

  • How much RAM and disk space is offered? Life will be easier if you have at least 2 GB of RAM and 20 GB of disk space.
  • Are there any restrictions on the amount of data traffic allowed per month? If so, try to find out how much your sites typically used in the past.
  • What hypervisor do they use to run VPS instances? “Native” or “bare metal” hypervisors such as Xen or KVM are preferable to “hosted” hypervisors like OpenVZ or Virtuozzo because they allow access to kernel-level commands like “ipset” that may be needed to block denial of service or spam attacks. They also have better isolation from other users.
  • Do they offer IPv6 connectivity? It’s not essential yet but is likely to be soon.
  • Do they have a good reputation for fast and helpful support? Try searching for reports of past problems from other users.
  • Is there a trial period or monthly billing option? If possible, monitor the reliabililty and responsiveness of a new host for a month or two using free services like uptime robot and loader. If page load times are more than 2 seconds or downtime is more than a few minutes a month you’re unlikely to be happy with the hosting.
  • Do they offer automated off-site backups? If not, what are your backup plans? How hard is it to restore a full or partial backup?
  • Does the data centre have good environmental credentials? Hosting uses a lot of electricity.

Location typically doesn’t matter much – sites will load a little faster if the server is in a country close to your main users and payment and support may be a little easier if the server is in a timezone close to you (the administrator), but other factors such as price and reliability are often more important than location. Local legislation about privacy, libel, censorship, copyright and taxation may also be relevant. I have used hosts in the UK, Europe, the US and Australia without problems.

Choosing an operating system and control panel

I prefer so-called LAMP hosting (Linux, Apache, MariaDB, PHP) because it’s open-source (cheap, patchable, secure) and compatible with popular content management systems. Any of these Webmin supported systems are reasonably complete and secure. Windows hosting is typically more expensive and not described here (or supported by Virtualmin) but may be necessary for sites that are scripted using Microsoft ASP. (Alternative databases and http servers such as PostgreSQL, MySQL, nginx and OpenLiteSpeed are outside the scope of this tutorial but well worth considering in some situations.)

You might not need any control panel at all if your server has only one user and that user is comfortable with using the command line. For most people though, I recommend open-source Virtualmin for configuring and managing the server, rather than a licensed control panel such as cPanel or Plesk because it’s full-featured, it’s the cheapest way to set up multiple sites and you retain full control of the server – you can still edit config files by hand, whereas other control panels override them. The rest of this tutorial assumes you are using Virtualmin.

For managing the content of individual sites, I recommend using a well-supported Content Management System (CMS) such as WordPress. Even very basic sites need to be updated regularly and a content management system makes this much easier, as well as giving you better Search Engine Optimisation (SEO), access to themes and plugins that add useful functions such as search forms, contact forms with spam protection, image galleries, event calendars, online shops and so on. Proprietary systems or “website builders” generally have more limited features and can be very difficult to move to a new host or add new features.


If you are installing the operating system from scratch, you may be asked to select some configuration options. If in doubt, accept the default values. For example, when installing Ubuntu 22.04 LTS you may be asked to choose the following:

  • Keyboard layout, language and country – choose the best ones for you, the administrator. Other users and websites can have their own settings.
  • Network connection – keep the automatically assigned (DHCP) one for now. If you’re creating a new virtual machine you will probably need to select “Bridged” mode (rather than NAT) first in the network settings for the virtual machine.
  • Hostname – choose a “fully qualified” name, such as a subdomain of a domain you own, like
  • HTTP proxy – none.
  • Real name, Username and Password – choose values appropriate for you personally. Usernames are conventionally lowercase because that’s faster to type. Other administrators can be added later. Your password should be long, random and unique (and stored in a password manager). The administrator password should NOT contain “special” characters (punctuation) because they may not be accepted by the recovery console. Do NOT use “root” as your username or give the root user a password because that’s a significant reliability and security risk (Ubuntu won’t allow it anyway). Once logged in as a member of the “sudo” group you can easily use the command “sudo -i” to change to root when necessary.
  • Partition disks – I suggest selecting Guided – use entire disk and setup encrypted LVM. This gives you encrypted backups which is good for security, but it does mean you will need to enter a password each time you reboot the VPS.
  • Encryption passphrase – avoid punctuation or accents since some consoles don’t support them. It should be long and unique (but no longer than 20 characters, again because some consoles don’t support them). Note: Some consoles don’t support entering this password from a phone or tablet – you will need to use a laptop or PC with a real keyboard.
  • Updates – I suggest selecting Install security updates automatically because manually supervising updates is a good idea in case they break something, but delaying security updates is risky.
  • Software selection – all you need at this stage is OpenSSH server, it’s better to allow the Virtualmin installation script to install the rest.
  • Install the GRUB boot loader – yes.

Note: To access the “setup encrypted LVM” option you may need to select “Install OS manually” rather than using a standard image.

Reboot the machine when prompted. Enter the encryption passphrase you chose above if you see a request similar to Please unlock disk vda6_crypt:.

Initial security patches

For security reasons, the first thing you should do after starting the server for the first time is login at the console using the administrator username and password (as configured above or given to you by the hosting company) and install the latest operating system patches. On Ubuntu and other Debian-based systems you can do this with the following commands:

sudo apt update
sudo apt full-upgrade

The “sudo” part is necessary when you are logged in as a normal user rather than superuser “root”. It effectively allows any user (who is also a member of the “sudo” group) to run commands as root. It will prompt you for your password the first time you use it in a session.

Why not just log in as root in the first place? Because there’s a significantly greater risk of the root account being compromised, and because it’s easy to accidentally do harm if you’re constantly logged in with superuser privileges.

Checking timezone and locale

Check that your server has an appropriate timezone set. It is usually most convenient to set it to the zone in which the main administrator is located, since it avoids the need to translate timestamps in log files. You can check the current timezone and list possible settings with these commands:

timedatectl list-timezones

Then change the setting with a command like this:

sudo timedatectl set-timezone Europe/London

Check that your server has an appropriate locale set (used for date formats and password checks):


You should see a list of settings such as LANG=en_GB.UTF-8. If it’s not set correctly, you can find and install an appropriate language pack and set the locale with the following commands (using British English as an example):

sudo apt search language-pack
sudo apt install language-pack-en
sudo update-locale LANG=en_GB.UTF-8 LC_MESSAGES=en_GB.UTF-8
sudo reboot

Installing the Virtualmin control panel

To download and install Virtualmin, follow their latest instructions or simply run the two commands below, which will take a few minutes to complete. You might be asked to change the hostname to a “fully qualified” name – that means a subdomain of a domain you own, for example

sudo /bin/sh

To connect to the Virtualmin web interface and complete the installation, point your browser to port 10000 on the server. The address to use is shown when the installation script completes. For example, if the IP address of your server was, the address to visit would be

You will get a warning about an untrusted certificate, which is safe to ignore for now. Login with username and password you used above, or the administration username and password given to you by your hosting company.

You will be taken through a post-installation wizard. The default answers are usually fine but I would advise selecting No for Enable virus scanning with ClamAV, select the “RECOMMENDED” size for MariaDB database size, select Skip check for resolvability beside “Primary Nameserver” and Only store hashed passwords for the “Password storage mode”. Click Re-check configuration when prompted.

Visit Virtualmin > System Settings > Features and Plugins to deselect any features you won’t use. Personally I deselect the following modules to save memory usage:

  • BIND DNS domain – if you only have one VPS it’s better to use a pair of free nameservers from Cloudflare.
  • Webalizer reporting and Awstats reporting – uses a lot of resources. I use Matomo or Google analytics.
  • PostgreSQL database – I only use MySQL (or MariaDB) because most scripts require that.
  • ProFTPd virtual FTP – I use secure file transfers (SCP or SFTP) via SSH instead, for security, speed and reliability.
  • DAV login – I also find this slow and insecure compared with SSH.
  • Spam filtering and Virus filtering – ClamAV is ineffective and hogs memory, and the default spam filtering happens too late. I use Amavis and ImunifyAV instead.

If you have the ability to take a “snapshot” backup of your entire server now is a good time to make one, because this is a good place to return to if you mess things up.

Connecting to the Secure Shell

Assuming you now have a remote server running a freshly-installed Linux operating system and Virtualmin but nothing else and you are configuring it from a local Windows PC, the best way to manage it is by connecting to the secure shell (SSH) using PuTTY for the command line and WinSCP for file transfers. This is generally faster and more convenient than the hosting company console we were using above or the Webmin file manager – for example, it allows commands to be cut and pasted. Install and run PuTTY on your Windows PC and put the IP address of your VPS (which the hosting company will tell you) where it says “Host Name (or IP address)”, leave the Port set to 22 and select Connection type: SSH.

Enter the administrator username and password you chose above (or given to you by your host) then click the Open button. Some hosts allow you to use a previously generated authentication key (see below) in which case the password is not needed.

The first time you connect to your new server you will see a warning that the server’s host key is not cached. Click “Yes” to save the key and connect. Enter your admin username and password when prompted.

Regenerate host keys (if necessary)

If your operating system has been installed from a standard hosting company image, the root SSH keys may be the same as every other VPS they host, which is a significant security risk. If in doubt, you can regenerate the keys at any time with the following commands (which won’t interrupt an existing SSH session):

sudo /bin/rm -v /etc/ssh/ssh_host_*
sudo dpkg-reconfigure openssh-server

Using authentication keys for SSH

Authentication keys are significantly more secure than passwords, and by default on Ubuntu this is the only way to log in as the root user and the only way to perform unattended backups, so it’s worth learning to use them.

First, generate a public/private key pair for yourself using a program such as PuTTYgen on Windows. The default RSA key type is no longer considered secure – I suggest choosing key type EdDSA using curve Ed25519 (255 bits). It’s good practice to protect private keys with a passphrase. I store that passphrase in KeePassXC and use Pageant from PuTTY to unlock the key automatically as required.

Next, copy the public key from the top window in PuTTYgen to your VPS – paste it into a file called authorized_keys inside a folder called .ssh in your user’s home folder. This file must be visible only to you, the admin user. You can use WEbmin > Tools > File Manager or conosle commands such as those below to create the folder and file with necessary permissions.

cd ~
mkdir .ssh
chmod 0700 .ssh
cd .ssh
sudo nano authorized_keys
[right-click to paste your public key on one line and save using Ctrl+O then Ctrl+X]
chmod 0600 authorized_keys

Don’t disclose the private key to anyone! To use PuTTY to connect to your server, make sure Pageant is running and your encrypted key has been added to it, then click Open. If it’s working you should NOT be prompted for a password when you try to connect, though you will get a warning about being an unrecognised host the first time, which you can safely ignore.

To use WinSCP to connect to your server and manage files graphically, click New Session, select File Protocol: SCP, put the IP address of your server in Host name, enter your User name, leave the password field blank and again make sure Pageant is running.

If you have a lot of files to manage for different users, it’s sometimes more convenient to connect as user “root”, by adding a key to file /root/.ssh/authorized_keys. If you do this, be very careful not to damage essential system files, and also make sure that any files you change are left owned by the correct user and group rather than by root. This is easy to forget and can cause all sorts of weird symptoms.

Once you are sure you can login successfully without a password, you can go to Webmin > Servers > SSH server > Authentication and set “Allow authentication by password?” to “No”.

Note that some administrators change the port the SSH server listens on from 22 to something higher, to reduce hacking attempts and associated log file entries. I do NOT recommend this because it’s only a temporary solution that doesn’t ultimately improve security at all, and can actually make it worse – whichever port you use will be found eventually by port scanners. I do however recommend setting up fail2ban to reduce server load.

Note that if you don’t like the advertising in the “message of the day” you can remove it by going to /etc/update-motd.d/ and deleing 10-help-text50-motd-news and 88-esm-announce.

To set up automated backups from your VPS to another server via SSH (using the rsync command, for example), you would generate a public/private key pair on the VPS itself, using a command such as the following, with a blank password.

ssh-keygen -t ed25519

The public key will typically be saved in  /root/.ssh/ from where it can be copied to the authorized_keys file on the remote server. 


Check IP addresses

By default IP addresses will be allocated automatically using DHCP. You can check your IP addresses (IPv4 and IPv6) at Webmin > Networking > Network Configuration > Network Interfaces. This has the big advantage that you can still access the server if for example a backup image is restored to a new server or there’s a network problem. However, a changed hosting IP address will cause all hosted sites to appear offline until their DNS records are changed. It’s normal to be allocated a /64 group of IPv6 addresses but only one IPv4 address.

Note: The Webmin script omits the package “net-tools” which is needed to correctly display the information in Network Configuration > Routing and Gateways > Active configuration. You can install it with the following command:

sudo apt install net-tools

Check hostname

Check your hostname under Webmin > Networking > Network Configuration > Hostname and DNS Client – Webmin might shorten this, you want the complete “fully qualified” name. Some hosting companies overwrite this information every time a VPS is rebooted – you may need to contact them to get it changed. Do NOT accept a generic name provided by the hosting company if you will be sending email from the server because spam filters will block it and you can’t easily generate SSL certificates for it.

Your hosting provider should allow you to set the “Reverse DNS” settings (or “PTR” record) for each IP address they have assigned you. At TransIP you can find this in the control panel at Manage > Network Information > Reverse DNS. Spam filters often check that this is set correctly for outgoing mail, so it’s worth configuring.

Install SSL certificates

You can install free SSL certificates from LetsEncrypt for your VPS and all the websites it hosts. You will have to create DNS records (A and AAAA) that point to your VPS address if they (or equivalent wildcard records) don’t already exist. The name of these DNS records will be just the subdomain part, so if your server hostname is the DNS records would be called just “server”. Go to Virtualmin > Create Virtual Server and create a new site with the same name as the hostname of your VPS (if the installation script hasn’t already created it).

The LetsEncrypt certificate should be genearted automatically, but if not go to Virtualmin > Server Configuration > Manage SSL Certificate > Let’s Encrypt and click Request Certificate. A button will appear to allow you to copy this certificate to Webmin, Usermin, Postfix and Dovecot. Your server now has free certificates installed, and they will update automatically every 2 months.

We’re done! Everything you need to host websites and emails should be working. All the stuff below is just enhancements to make things better. You should probably have a play with things before embarking on the next steps, following the “if it ain’t broke, don’t fix it” principle.

Additional security

The default settings that we have installed so far provide a reasonable level of security, as long as you choose hard-to-guess unique passwords and install security patches quickly (preferably automatically). Nevertheless, your VPS will be attacked constantly by hackers and spammers so you should remove as many vulnerabilities as possible and check your log files regularly.

Automatic kernel updates

You can register for an Ubuntu Pro account at and get a free personal token good for five machines. Let’s call it [myprokey]. Then you can attach the server using a command like this.

sudo pro attach [myprokey]

This will allow kernel updates to be installed without rebooting the server.

Restrict Virtualmin logins

The Virtualmin interface is another way a hacker could get root access to your server. I strongly advise enabling two-factor authentication for any superuser or a user who is a member of the “sudo” group. You can enable this at Webmin > Webmin Configuration > Two-Factor Authentication. I find it easiest to select Google Authenticator as the provider but to actually use Authy to do the authentication (because it saves messing around with API keys and because you can recover your account if you lose your phone).

You can enforce strong passwords in Webmin > Webmin users > Password restrictions – I recommend setting a minimum length of at least 12 characters. There is a password generator and strength indicator built in to Virtualmin, but the defaults aren’t very good. I change the settings in Virtualmin > System Settings > Virtualmin Configuration > Defaults for new domains to increase the length to 20 characters and the allowed characters to the following set:


If you do this, the password generator will pass its own strength check(!) and will omit some notoriously ambiguous characters.

You can also restrict access to certain Webmin modules depending on the user, and it’s useful in any case to remove unwanted modules to de-clutter the Webmin menus. I suggest creating a group with a name like “Administrators” and adding yourself to that group. Then in Administrators > Available Webmin modules deactivate almost everything in the “Hardware” and “Cluster” groups and everything that appears in Un-used Modules in the menu bar at the left. I move the “System Time” module to the “System” menu by going to Webmin > Webmin Configuration > Reassign Modules. While you’re there, visit the “Time server sync” tab in System Time and configure a time server name (I use and enable Synchronize on schedule. This is important, because if the server time drifts too far out, two-factor authentication won’t work.

Disable unnecessary services

The proftpd, clamav-daemon, clamav-freshclam and named services are usually not necessary and can be disabled to save memory by going to Webmin > Bootup and Shutdown and clicking Start at boot time > No > Save and then Stop Now. You can also disable the ftp ports 20 and FTP and port 2222 in Webmin > Networking > Firewalld. (Do NOT disable DNS port 53 for either TCP or UDP because although it might appear to work, responses to outgoing queries can be blocked if they’re too large.)

Mitigate cookie hijacks

In your global PHP configuration at Webmin > Tools > PHP Configuration you can set the list of PHP configuration files by clicking the settings cogwheel at the top left and changing the list to the versions you use (the defaults are wrong). For example:

/etc/php/*/fpm/php.ini=Configuration for scripts run via php-fpm
/etc/php/*/cli/php.ini=Configuration for command-line scripts
/etc/php/*/cgi/php.ini=Configuration for cgi scripts

To mitigate cookie hijacks add these settings:

session.cookie_samesite = “Lax”
session.cookie_httponly = 1
session.cookie_secure = 1

Sites will now run in php-fpm mode by default. To change the default for new sites go to Vitualmin > System Settings > Server Templates > Default settings > PHP options.

Restrict obsolete cipher suites – PCI DSS, HIPAA and NIST compliance

In Webmin > Servers > Apache Webserver > Global Configuration > Configure Apache Modules check that the “headers” and “expires” modules are enabled. They are required below, and are also often used in “.htaccess” files to provide “friendly” URLs and to control page cache timeouts. If you see the server’s default Apache page instead of your website these missing modules might be the cause.

To restrict the use of obsolete and insecure SSL ciphers, enable OCSP stapling and HTTP Strict Transport Security, and enable Cross-Site Scripting protection in browsers you can add these statements to the end of file /etc/apache2/apahe2.conf in the root of the server, also accessible from Webmin > Servers > Apache Webserer > Global Configuration > Edit Config Files. They can be checked with the ImmuniWeb and Qualys SSL Labs server tests and should be enough to get you an A+ rating. Don’t forget to click “Apply changes” afterwards (the refresh symbol at the top right of the Apache Webserver page).

# From
SSLProtocol             all -SSLv3 -TLSv1 -TLSv1.1
SSLSessionTickets       off

SSLUseStapling On
SSLStaplingCache “shmcb:logs/ssl_stapling(32768)”

# Additional after testing at
SSLHonorCipherOrder on
SSLSessionTickets off
Protocols h2 http/1.1
Header always set Strict-Transport-Security “max-age=63072000”
Header set X-Content-Type-Options nosniff
Header always set Referrer-Policy no-referrer-when-downgrade
Header always set Content-Security-Policy “upgrade-insecure-requests; default-src ‘self’; style-src * ‘unsafe-inline’; script-src ‘self’ ‘unsafe-inline’ ‘unsafe-eval’ blob: * * * * * * * * *; img-src * data:; font-src * data:; media-src ‘self’ *; frame-src ‘self’ * * * * * * *; connect-src ‘self’ *;”

You will probably need to tweak the Content-Security-Policy to your requirements. I recommend initially setting it to Content-Security-Policy-Report-Only and checking for errors in the “Inspect” utility by pressing F12 then Console in most browsers.

Similarly, at Webmin > Webmin Configuration > SSL Encryption check that all SSL protocols below TLSv1.2 are rejected and “Only strong PCI-compliant ciphers” are selected and similarly at Usermin Configuration > SSL Encryption

Be careful about restricting email connections too much in Postfix (/etc/postfix/ or Dovecot (/etc/dovecot/dovecot.conf) because there are many old email systems out there that can’t cope with newer ciphers and you could lose messages. You should nevertheless make sure that email login passwords won’t be visible in plain text. For outgoing mail go to  Webmin > Servers > Postfix Mail Server > SMTP Authentication and Encryption and check Require SASL SMTP authentication? and Disallow SASL authentication over insecure connections?. For incoming mail go to Webmin > Servers > Dovecot IMAP/POP3 Server > SSL Configuration and check Disallow plaintext authentication in non-SSL mode?.

By default, email reports of any system problems will be sent to user “root”. You can read them by going to Webmin > System > Users and Groups > root and clicking the Read Email button. It’s usually more convenient to forward them to an external email address. You can configure this by going to Webmin > Servers > Postfix Mail Server > Mail Aliases, selecting Create a new alias and setting Address to “root” and your email address in “Alias to”, “Email address”.

Replace Apache with OpenLiteSpeed

Virtualmin assumes you will use Apache as your webserver, but that’s not necessarily the best choice these days. Alternatives such as nginx, varnish and litespeed offer significant advantages in speed and memory usage. I recommend OpenLiteSpeed because it’s free and compatible with WordPress (specifically, it can understand permalinks in .htaccess files). There is currently no OpenLiteSpeed configuration module for Webmin but it comes with its own dashboard and by following the steps below you can install it as (almost) a drop-in replacement for Apache. You can freely switch back and forth between the two and in theory you could even run them in parallel on the same machine (on different ports or IP addresses), though that’s outside the scope of this tutorial.

The first step is to install and start OpenLiteSpeed, which can be done without interfering with Apache.

wget -O – | sudo bash
sudo apt update
sudo apt install openlitespeed
sudo systemctl start lsws
sudo systemctl enable lshttpd.service

Set the admin username and password using this command (or use the defaults set in /usr/local/lsws/adminpasswd).


If firewalld is enabled as described below, you will need to (at least temporarily) allow port 7080 and port 8088 at Webmin > Networking > FirewallD > Add allowed port and click the Reload FirewallD button.

You should now be able to see the OpenLiteSpeed WebAdmin page in a web browser at this address.

https://[server ipv4 address]:7080


  • If you see This site can’t be reached, check for typos and try disabling your firewall. Check DNS settings or accessing from a different browser or device.
  • If you see a Not secure warning in your browser, that’s normal at this stage because the certificate is “self-signed”. We will make it more secure later.

Log in and check the Server Error Log at the bottom of the dashboard. You should see lots of NOTICEs but no WARNINGs or ERRORs at this stage.

The next thing to check is the example hosted webpage at the address below. You may have to (temporarily) allow port 8088 through your firewall.

http://[server ipv4 address]:8088

Click the button under Test PHP to make sure PHP is working – you should see a “phpinfo” page with lots of information. You may notice that it uses PHP 7.4 which is no longer supported, but we will fix that later.

The first problem is that Apache normally runs as user “www-data” but OpenLiteSpeed runs as user “nobody”. This will cause permission errors if we try to access files created by one webserver using the other webserver. The easiest way to fix this is probably to make the OpenLiteSpeed server run as www-data. There’s no option to change this in the OpenLiteSpeed dashboard but you can edit the configuration file /usr/local/lsws/conf/httpd_config.conf manually – at the top of the file simply change both the user and group to www-data. Then “reinstall” the server to fix existing file permissions and restart.

apt install –reinstall openlitespeed
rm -rf /tmp/lshttpd
systemctl restart lsws

If this all works, the next step is to reconfigure OpenLiteSpeed to serve sites from the usual port 80 instead of port 8088. Since Apache will already be connected to port 80 (and port 443), we will have to stop and disable it first using the commands below. This will of course cause all currently hosted sites on this server to go offline temporarily. Do NOT uncheck the “Apache website enabled” setting in any site’s “Edit Virtual Server” settings in Virtualmin because that would cause all their html files to be removed. It’s theoretically possible to keep Apache running on a different port or IP address if some sites require it, but that’s outside the scope of this tutorial and I haven’t tested it.

sudo systemctl stop apache2
sudo systemctl disable apache2

In the OpenLiteSpeed WebAdmin dashboard change the default listener port at Listeners > Default > Address Settings > Port from 8088 to port 80. Click the Save button at the top right then the green Graceful Restart button (“graceful” means there’s no downtime for hosted sites). You should now be able to see the example page without adding ‘:8080’ to the end of the address (in other words, at http://[server ipv4 address]).


  • If you see This site can’t be reached, try clearing your browser cache, restarting your browser, using “Incognito” mode, forcing a refresh using Ctrl+F5 or using a completely different browser or a different device. 

If your server has an IPv6 address you should create a listener for this as well at Listeners > Add (the plus sign at the right). Give it a name such as “Default_IPv6” and use the same settings as the Default listener, but with IP Address set to [ANY] IPv6. Under Virtual Host Mappings add the Example host with Domains ‘*’ similarly to the Default listener. You can test the IPv6 address by pointing a web browser to http://[server ipv6 address] – note that you must surround an IPv6 address with square brackets, otherwise most browsers will just show you a search page.

We will also need at least one listener for SSL connections. Give it a name such as “Default_SSL”. In Address Settings select ANY IPv4 and Port 443 and set Secure to Yes. Save it, then in the SSL tab add links to Private Key File and Certificate File. You can copy these addresses from the Virtualmin default site (with the same name as the server hostname, something like that should already exist under Server Configuration > SSL Certificate. However, I recommend changing the copied name from ssl.cert to ssl.combined) and setting Chained Certificate to Yes. Under SSL Protocol select just TLS v1.2 and TLS v1.3. Select YES for Enable ECDH Key Exchange and Enable OCSP Stapling. Click Save and Graceful Restart. Check in the dashboard under Listeners that the status of all listeners is green.

Again, if your server has an IPv6 address you should create a similar listener for that, let’s call it “Default_SSL_IPv6”.

If all this works it’s time to set up our first site in OpenLiteSpeed, and I’ll assume we will use the “” site that matches the server’s hostname, or you could add a test site. Whichever site you use, make sure it works properly with Apache first (including SSL connections and logging). Then stop Apache and restart the LiteSpeed web server (lsws).

In OpenLiteSpeed > Virtual Hosts click Add (the plus sign at the right of Virtual Host List). Set Virtual Host Name to a unique name, which in Virtualmin would usually be the name of the owner or (for sub-servers) the domain name. In other words, the name of the parent folder of public_html – in our example, “server”. Set Virtual Host Root to the location of that folder – in our example, “/home/server”. Set Config File to the value recommended by the context help, $SERVER_ROOT/conf/vhosts/$VH_NAME/vhconf.conf. Set Follow Symbolic Link to If Owner Match and set Enable Scripts/ExtApps and Restrained to Yes. For External AppSet UID Mode choose DocRoot UID. Click Save and Graceful Restart

Now we have a bunch of new tabs to fill in, but the default values for most of them (which come from Server Configuration) are mainly fine and can be left “Not set”. In the General tab set Document Root to $VH_ROOT/public_html. Set the Domain Name if it’s not the same as the server name, in our case it should be “”.

Under the Log tab, set Use Server’s Log to No and File Name to match the setting in VIrtualmin > Server Configuration > Website Options > Webserver error log file. The expected location is something like “/var/log/virtualmin/server.mydomain.com_error_log”. The exact location doesn’t matter, as long as it has the right permissions. Click the plus sign for Access Log and set Log Control to Own Log File with the File Name again copied from Virtualmin. The format will default to the Apache format so nothing needs changing there.

In the SSL tab copy the location of the key file and certificate file from Virtualmin as described above when we set up the SSL Listener. If they don’t yet exist, you can create them as usual at Virtualmin > Server Configuration > SSL Certificate.

When we create a Virtual Host manually this way (not using a VHost Template) we have to manually connect the Listeners as well. It’s easy to forget! Go to Listeners and for each of the four we have created, add a Virtual Host Mapping, select the Virtual Host we have just created in the dropdown and fill in the domain (in our example “”).

You should now be able to visit the site in a browser at last, at an address like The visit should appear in the logs in Virtualmin and PHP and permalinks should work. And it should load a lot faster and use less memory than before!

There were a lot of configuration settings and plenty of opportunities for mistakes, however. If it doesn’t work, here are some things to check.

  • Check the OpenLiteSpeed > Dashboard > Server Error Log for errors or warnings.
  • Check that the site still works if you stop the OpenLiteSpeed server and start Apache instead. If not, check that DNS records are set up and there are no typos in the name.
  • Cache and certificate “pinning” problems are common. Try using a different browser (even a different device) and try “Incognito” mode. Clear the browser cache and cookies. Force a page refresh using Ctrl+F5.
  • Make sure all Virtual Hosts are connected to one or more Listeners.
  • Make sure the OpenLiteSpeed server has been restarted since the last configuration change.
  • Try setting file permissions to 0755 recursively. If this works, check and fix file ownership before setting permissions back to 0750.

Now the basics are working, we can make some improvements so the whole process is much faster.

If you’re likely to create multiple virtual hosts with similar configurations, you can create a template for this in OpenLiteSpeed > VHost Templates. Click the plus sign at the top right, choose a name (e.g. ‘Virtualmin’), set Template File to $SERVER_ROOT/conf/templates/virtualmin.conf and Mapped Listeners to Default, Default_IPv6, Default_SSL and Default_SSL_IPv6 (or whatever names you chose above). Click Save and then CLICK TO CREATE and Save again.

Click the VHost template you just created, then the General tab. Edit Base and set Default Virtual Host Root to /home/$VH_NAME to match the normal location used by Virtualmin on Ubuntu. Set Config File to $SERVER_ROOT/conf/vhosts/$VH_NAME/vhconf.conf as recommended by the pop-up help. Then edit Base2 and set Document Root to $VH_ROOT/public_html/, again to match the Virtualmin location.

In the Log tab set Use Server’s Log to No, set File Name to /var/log/virtualmin/$VH_NAME_error_log, set Log Level to WARNING. Under Access Log, for Log Control select Own Log File, set File Name to /var/log/virtualmin/$VH_NAME_access_log. Note that OpenLiteSpeed unfortunately won’t allow the use of $VH_DOMAIN here.

To execute PHP and other external apps with the correct permissions, we go to the Security tab and under External App Security set External App Set UID Mode to DoCRoot UID. In the File Access Conrtol tab set both Enable Scripts/ExtApps and Restrained to Yes.

Many sites (including WordPress) set rewrite rules in .htaccess files, so it’s a good idea to enable these by default. In the Rewrite tab set Enable Rewrite and Auto Load from .htaccess to Yes. Set the following Rewrite Rules.

RewriteCond %{HTTPS} off
RewriteRule ^/(?!.well-known)(.*)$ https://%{HTTP_HOST}/$1 [R]

In the SSL tab we will need to change the default certificate location for new sites (because OpenLiteSpeed doesn’t know what a Virtualmin “${ID}” is). In Virtualmin > System Settings > Server Templates > Default Settings > SSL website for domain change the Custom path to /etc/ssl/virtualmin/${DOM}/ssl.key. Repeat for the Settings For Sub-Servers template. Note that this is slightly less secure, because it’s a guessable location – if that worries you, you can keep using the random ${ID} but then you will have to “Instantiate” each site that the VHost Template creates to make it editable, then change the location manually to match Virtualmin.

Back in the OpenLiteSpeed VHost Template page, set the Private Key File to /etc/ssl/virtualmin/$VH_DOMAIN/ssl.key and the Certificate File to /etc/ssl/virtualmin/$VH_DOMAIN/ssl.combined with Chained Certificate set to Yes as before.

Now we will use this template to create a new virtual host, let’s call it “”. Add that site to Virtualmin in the usual way and set up DNS records for it. Check that it works with Apache. Then click the plus sign for Member Virtual Hosts and set the Virtual Host Name to the name of the folder (and user) that Virtualmin created, “test” in this example. Set the Domain Name to the address of the site, “” in this example. Click Save and Graceful restart.

If you point your browser to it should appear, without warnings.

We can fix the PHP 7.4 problem by installing later versions.

sudo apt install lsphp81 lsphp81-{common,curl,mysql,opcache,imap,imagick,intl,memcached}
sudo apt install lsphp82 lsphp82-{common,curl,mysql,opcache,imap,imagick,intl,memcached}

Warning: Do NOT try to remove any of the lsphp packages. There is a bug that makes a mess of the Apache PHP setup if you try this.

The default version for the whole OpenLiteSpeed server is set at Server Configuration > External App > Command. Change this to lsphp81/bin/lsphp. Maybe change the Name and Address to lsphp81 as well. Click Save and Graceful Restart, then click the plus sign and add a similar entry for lsphp82.

To increase PHP resources or improve security under OpenLiteSpeed, create a php.ini file in the appropriate folder, in our case /usr/local/lsws/lsphp81/etc/php/8.1/litespeed/php.ini. I add the following settings:

error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
log_errors = on
error_log = error_log

; For some sites that need it:
memory_limit = 512M
upload_max_filesize = 100M
post_max_size = 100M
max_input_vars = 2000

; To mitigate cookie hijacks
session.cookie_samesite = “Lax”
session.cookie_httponly = 1
session.cookie_secure = 1

; For Nextcloud, see
opcache.memory_consumption = 1536
opcache.interned_strings_buffer = 128
opcache.max_accelerated_files = 50000

To restart PHP, execute the following command (restarting lsws is not enough if PHP is “detached”, see

killall -9 lsphp

To get the full benefit we need to enable the LiteSpeed Cache, which is best achieved by installing a plugin of that name in every site you host. It’s possible to enable caching for the whole server by adjusting settings at Server Configuration > Modules > cache but settings are easier to adjust in site-specific plugins. The default settings work pretty well. You should see a huge improvement in speed and visitor capacity as soon as you enable it. The dashboard will show you a few optional things you can enable to squeeze a few more percentage points of improvement (Object Cache, Guest Mode, Auto Request Cron).

So, here’s a summary of the steps needed to migrate a Virtualmin site from Apache to OpenLiteSpeed.

  1. If the site is currently on a completely different server, migrate it using the usual Virtualmin tools (Create Virtual Server, Move Virtual Server, Transfer Virtual Server, Import Virtual Server, Migrate Virtual Server, Restore Virtual Server).
  2. In the OpenLiteSpeed dashboard, add a Member Virtual Host to the VHost Template. The Virtual Host Name and Virtual Host Root in OpenLiteSpeed should typically be the administrator username from Virtualmin and the same name under /home, but sub-servers or sites created on old versions of Virtualmin might be different.
  3. Check the SSL certificate paths and the log paths carefully. Again, subdomains and older versions of Virtualmin have different locations. If any are wrong, the simplest solution is to click the Instantiate button to create an individual Virtual Host and correct them there. If you do this, don’t forget to manually add a Virtual Host Mapping to all your Listeners to point to this new Virtual Host. Also don’t forget to do a Graceful Restart of OpenLiteSpeed.
  4. Change your DNS settings to point to the new server. Allow time for the changes to propagate. If you’re impatient you can add entries to the /etc/hosts file on the server and/or on your PC, typically at C:\Windows\System32\drivers\etc\hosts. Clear browser caches.
  5. You should be able to see your home page now, but pages that use permalinks may not be there. In WordPress, go to Settings > Permalinks and click Save Changes. Then do another Gracefult Restart of OpenLiteSpeed.
  6. Uninstall any old cache plugins in WordPress and install and activate the LiteSpeed Cache plugin.
  7. Check that everything still works with Apache running, in case of problems. Validate the server at Virtualmin > Limits and Validation.

You may have to tweak some .htaccess files to get them working. For example, the CiviCRM .htaccess file at /wp-content/uploads/civicrm/upload/.htaccess has been fixed but they forgot /wp-content/uploads/civicrm/ConfigAndLog/.htaccess and /wp-content/uploads/civicrm/custom/.htaccess. Here’s the code you may need to add to those .htaccess files.

<Files “*”>
# OpenLiteSpeed 1.4.38+
  <IfModule !authz_core_module>
    RewriteRule .* – [F,L]

Enabling a firewall and fail2ban

A firewall may not be as useful as you expect, because most services on a VPS have to be publicly accessible all the time and unnecessary services should be disabled anyway. Nevertheless, a firewall helps to protect against some types of denial of service (DoS) attack. Firewalld is set up and enabled by default by Webmin and needs no further configuration – it works for IPv6 as well as IPv4.

Fail2ban works with firewalld to automatically block the IP address of persistent offenders. It can no longer do much to protect against “brute force” password attacks because botnets simply change their IP address constantly to bypass it, but it is still sometimes useful for reducing server load. It can be configured at Webmin > Networking > Fail2ban Intrusion Detector. The default settings tend to do more harm than good though so they need some tweaks.

Note: Some versions of Webmin have a bug that prevents fail2ban starting automatically after a reboot. Enter the following command to fix that.

sudo systemctl enable fail2ban

First, I suggest going to Filter Action Jails and setting the postfix and dovecot jails to something large (like 10,000 attempts a day), because legitimate users sometimes misconfigure their email clients, and if they trigger a ban for a shared IP address they can end up blocking email for a whole building. The default ssh and webmin-auth jails can remain.

Next, I recommend copying wordpress-hard and wordpress-soft filters and jails from the WP Fail2Ban Redux plugin and adding an apache-404 filter and jail as described here.

The apache-404 filter is located at /etc/fail2ban/filter.d/apache-404.conf and the contents are below.


failregex = ^<HOST> – .* “(GET|POST|HEAD).*HTTP.*” 404  .*$
ignoreregex = .*(robots.txt|favicon.ico|jpg|png|FeedBurner)

Finally, fail2ban will be almost completely ineffective unless the expiry times are increased from 10 minutes to something like 20 days (which seems to be roughly the maximum that can be achieved).

In /etc/fail2ban/fail2ban.conf I change this setting:

dbpurgeage = 20d

In /etc/fail2ban/jail.conf I change these settings:

bantime  = 20d
findtime  = 20d
maxretry = 9

In /etc/fail2ban/jail.local I add these settings:

enabled = true
filter = wordpress-hard
logpath = /var/log/auth.log
port = http,https
backend = auto

enabled = true
filter = wordpress-soft
logpath = /var/log/auth.log
port = http,https
backend = auto

enabled = true
port = http,https
filter = apache-404
maxretry = 100
logpath = %(apache_access_log)s
findtime = 1d
backend = auto

I then create a new file /etc/fail2ban/paths-overrides.local with this content:

apache_error_log = /var/log/virtualmin/*_error_log
apache_access_log = /var/log/virtualmin/*_access_log

Note that Ubuntu creates a file /etc/fail2ban/jail.d/defaults-debian.conf that causes a duplicate sshd jail to be created. That file can be removed or the contents commented out.

I add the following entry to Webmin > System > System logs > Settings (cogwheel) > Other log files to show to make it easier to monitor fail2ban.

/var/log/fail2ban.log Fail2ban log

Virus scanner

Virus scanners for Linux unfortunately tend to be very expensive or very ineffective. The free version of ImunifyAV is the best I have found. It can be installed as described here and configured as described here. You will need to create a web host for it, which could be a subdomain or simply a page on a site you already have. You then need to create a configuration file to tell Imunify where that site is, and who the owner and group are. You can do that using commands like these:

sudo mkdir /etc/sysconfig
sudo mkdir /etc/sysconfig/imunify360
sudo nano /etc/sysconfig/imunify360/integration.conf

 The file should contain the UI path and the UI path owner, something like this:

ui_path = /home/[owner]/domains/[imav.domain]/public_html
ui_path_owner = [owner]:[group]

Note that the public_html folder must be empty, otherwise the installation script below will fail. Save the file and exit using Ctrl+O then Ctrl+X. Next, download and execute the deployment script: 

sudo wget -O
sudo bash

You will probably need to add the names of one or more administrators (sudo users) to the auth.admin file, one name per line.

sudo nano /etc/sysconfig/imunify360/auth.admin

(You can create or promote additional sudo users at Webmin > System> Users and Groups by setting their Shell to “/bin/bash” and adding group “sudo” from Secondary groups.) Save this file, then point a web browser to the web host you created above to view the Imunify dashboard.

You will probably want to configure a notification email, so that you’re informed when malware is found. ImunifyAV (the free version) doesn’t set anything by default. Detailed instructions are here but the simple version is that you first need to download an example “hook script” and make it executable by the “_imunify” group.

cd /etc/imunify360
curl -O
chown root:_imunify
chmod g+x

Edit this file to enable emails to your desired address.

MAIL_ENABLE=yes              # default no, change to “yes” for enabling
MAIL_TO=”[your email address]”  # for multiple email addresses, use commas

In the Imunify dashboard, click the “Settings” cogwheel and select the “Notifications” tab. Under “Custom scan: malware detected” and “User scan: malware detected” select “Enable script execution”, paste the location of the hook script (/etc/imunify360/ in this example) and Save Changes. You can test it by also enabling the notification script for Custom scan: started and doing a trial scan of the /home folder in Malware Scanner > SCAN > Start.

Restart the notification service and install the “jq” package.

systemctl restart imunify-notifier
sudo apt install jq

Click the “Scan all” button and come back a few days later to see the results. This checks all user files including emails.

Another useful scanner that doesn’t do much harm and can detect unwanted changed to system files can be installed using this simple command (a scheduled cron job will be created automatically).

sudo apt install rkhunter

Set disk quotas

To enable disk quotas, go to Webmin > System Disk Quotas and check that quotas are enabled for users and groups in the root folder ‘/’.

By default, “hard” disk quotas are enabled, which will prevent a user accidentally using up all the free disk space. You can change the default size in Virtualmin > System Settings > Server Templates > Default Settings > Mail for domain. I suggest 1 GB is a more reasonable limit than the default 50 MB these days. You can make the default quota “soft” in the same menu under Administrator user. You can set up email alerts in Webmin > System > Disk Quotas > Settings > Quota email messages.

Additional packages

You will sometimes need to install extra packages that are required by the specific programs you install. Often the installation program for a content management system will do a configuration check and inform you about missing packages or configuration settings that need to be changed. Some examples that I have found useful (install from the command line or from Webmin > Software Packages > Package from APT):

Needed by Virtualmin:

sudo apt install net-tools opendkim ntp
sudo apt install libauthen-libwrap-perl libauthen-oath-perl libxml-simple-perl
sudo apt install liblwp-protocol-https-perl libdigest-hmac-perl

Generally useful:

sudo apt install php8.1-{gd,zip,mbstring,intl,xml,mysql,uploadprogress,memcache,memcached}
sudo apt install curl logwatch automysqlbackup rkhunter jq

Needed particularly by WordPress (including for WooCommerce):

sudo apt install php8.1-{curl,soap,bcmath,imagick,ssh2}

Also the “wp” command line utility (useful for running cron jobs) can be installed, updated and tested using these commands:

curl -O
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
wp –info

Apache modules can be enabled and disabled in Webmin > Servers > Apache Webserver > Configure Apache Modules. I suggest enabling at least the following modules, which are often used in “.htaccess” files (but see below about possibly using LiteSpeed instead).

  • headers
  • expires

It’s also a good idea (for performance) to enable http2, which requires enabling module mpm_event (instead of mpm_prefork), cgid (instead of cgi, only required if cgi scripts are used otherwise leave disabled for better security) and disabling any Apache php modules (such as php8.0). If any of your sites ever use a proxy such as Cloudflare, it’s a good idea to enable module remoteip so that the correct IP address is shown in log files. Therefore, I recommend also enabling these Apache modules:

  • http2
  • mpm_event
  • cgid
  • remoteip

Don’t forget to click “Apply Changes” at the top right afterwards.

There is currently a Virtualmin bug that may cause Apache or PHP-FPM to fail to restart when Apache php modules are disabled like this. Old sites that used these modules may have inserted statements beginning “php_” into their configuration files, which must be removed. You can find them in /etc/apache2/sites-available/ and /etc/php/8.1/fpm/pool.d/.

Some applications (e.g. CiviCRM) will complain that the database lacks “Time Zone Tables”. The following command will load them. It will prompt for the MySQL administrator password – you can find this at Webmin > Servers > MariaDB Database Server > Change Administration Password.

mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root -p mysql

Installing web sites

Web sites can be created by going to Virtualmin > Create Virtual Server and entering the domain name you will use and an administrator password. The default settings are usually OK and come from Virtualmin > System Settings > Server Templates.

You may wish to enable IPv6 for new sites, otherwise LetsEncrypt SSL certificate signing will fail if the site has AAAA DNS records – you can fix this default behaviour in Virtualmin > System Settings > Virtualmin Configuration > Networking Settings.

You can also create “Sub-servers” and “Alias” servers. Alias servers are useful if for example you have both a “.com” and a “” domain – website visitors and mail users can use either domain and will see the same website or reach the same mailbox. Sub-servers are useful if one administrator is managing several different sites – if they all have the same owner then there’s no need to log in and out to move between them. 

It’s possible for many sites to share the main IP address of the server, you don’t need multiple IP addresses anymore (because all modern browsers support “SNI“).

To forward administrative email messages to an external address go to Virtualmin > Edit Virtual Server > Configurable Settings > Contact email.

The default limits for memory, upload size and execution times for PHP are quite restrictive and you will probably want to increase them. You change change the default settings at Webmin > Tools > PHP Configuration > /etc/php/8.0/fpm/php.ini > Manage > Resource Limits. I increase the “Maximum file upload size” and “Maximum HTTP POST size” to 100M and the maximum memory to 512M. Individual sites may need even higher limits, which can be set at Virtualmin > Services > PHP-FPM Configuration.

Installing WordPress

By popular demand, here are some instructions for what is often the final step – installing a Content Management System (WordPress) under Virtualmin.

First, create a virtual server (Virtualmin > Create Virtual Server) for the domain name that your WordPress website will use (let’s call it Specify a long unique administrator password and make sure the option “Setup SSL website too?” is checked under Enabled Features.

If your DNS records are already set up and have had time to propagate, you should be able to browse to You should see the message “Forbidden. You don’t have permission to access / on this server.” because there are no files there yet.

Next go to Virtualmin > Server Configuration > SSL Certificate > Let’s Encrypt and click the “Request Certificate” button. That will replace the self-signed certificate with a proper domain certificate, which avoids browser warnings.

Now open a file manager – I use WinSCP but you can also use Webmin > Others > File Manager. Browse to the folder /home/[your domain]/public_html. It’s probably empty. Download the latest version of WordPress from the download page to your PC and then upload it to the public_html folder (File > Upload to current directory) and unzip it (right click > Extract). A folder called “WordPress” should have been created. Open this folder, select all the contents (there’s a “Select All” button at the top) and cut and paste them up one level to the public_html folder.

Check the ownership of these files. If you are logged in as a server administrator, they may be owned by root:root instead of something like mydomain:mydomain. If so, go up one level, right-click on public_html, select Properties > Change Ownership and enter the existing values for the public_html folder but with the “Recursive” box checked.

Note that Virtualmin now adds a holding page called index.html to new sites – you will need to delete this file, because it takes priority over the default index.php file that WordPress uses.

Now if you browse to your site you should see a “Welcome to WordPress” message and some instructions. Click “Let’s Go!”. The next screen asks for the database name, which you will find in Virtualmin > Edit Databases, and the username and password, which you will find in the Passwords tab by clicking the little key symbol. Click “Submit”. If all is well, you can now fill in the site name, admin username and password to complete the installation and log in as an administrator.

To make connections to the site secure, you can go to Settings > General and change WordPress Address (URL) and Site Address (URL) from http:// to https://. 

Securing web sites

It’s risky to transmit passwords in plain text when logging in to a web site or a mailbox – especially on a public network such as an open Wifi connection. To minimise the chances of users doing this accidentally, I enforce encrypted connections for passwords everywhere.

You can enable SSL (secure sockets layer) connections for one site on your server simply by going to Virtualmin > Edit Virtual Server > Enabled features and checking the box “SSL website enabled?”. This will immediately allow encrypted connections to this site with a URL that begins “https” rather than “http”. However, the automatically-generated certificate is “self-signed” which means users will get browser warning that the connection can’t be trusted (because there might be a man-in-the-middle intercepting the encrypted traffic). The solution is to install a free LetsEncrypt certificate by going to Virtualmin > Server Configuration > SSL Certificate > Let’s Encrypt > Request Certificate. If you’ve done everything correctly your web site will now load with no warnings.

Now that all modern browsers support Server Name Indication (in particular, Internet Explorer on Windows XP is officially obsolete) it’s no longer necessary to have a separate IP address for each site that uses an SSL certificate. 

You can make sites even more secure (and improve your search engine ranking) by redirecting insecure connections to the secure version automatically. In Virtualmin > Server Configuration > Website Options select “Yes” for Redirect all requests to SSL site? To make this the default for new sites, set it in Virtualmin > System Settings > Virtualmin Configuration > SSL Settings.

The most common route by which hackers gain access to a WordPress site is through plugins and themes that haven’t been updated. Fortunately, WordPress can now update most of them automatically. Unfortunately, automatic updates are disabled by default. To enable them, go to Dashboard > Plugins, click the checkbox at the top left of the plugin table, and under “Bulk actions” select “Enable Auto-updates“. Then go to Appearance > Themes, click each theme individually and select “Enable Auto-updates”.

Other common security problems are weak passwords (the WordFence plugin has a useful setting for enforcing strong ones), human error, a compromised administrator’s computer and leaving forgotten and unmaintained “staging” sites on a server.

Of course, it’s important to keep all the server packages (including PHP) fully patched as well. Periodically (at least once a month) I manually run these commands from a command prompt:

sudo apt update
sudo apt full-upgrade
sudo apt autoremove

That last “autoremove” is important because if you don’t run it occasionally, the /boot partition (which is quite small) may fill up with old kernel versions. If that happens the machine  may fail to reboot, which makes it really hard to repair. 

Updates occasionally cause problems, so make sure you have a recent snapshot or backup. You may have to reverse the changes or even roll back the whole server, which is why I run the above commands manually, late at night.

Configuring DNS settings

You will need to change the Domain Name System (DNS) settings for each new site that is added to your VPS. Each DNS change can take 24 hours or more to propagate around the worldwide DNS network. If you’re impatient you can access the site in “preview” mode at Virtualmin > Services > Preview Website or you can access the site directly by IP address (by entering an address like in your web browser) or you can modify the “hosts” file on your computer (on Windows this is usually located at C:\Windows\System32\drivers\etc\hosts, on a Mac it’s at /etc/hosts and you need to be an administrator in both cases).

I recommend using Cloudflare for your DNS settings rather than the nameservers provided by your domain registrar or a nameserver set up on your own VPS, for these reaons:

  1. You are supposed to have at least two nameservers (in different locations) for redundancy and Cloudflare provides this for free.
  2. You can quickly switch on Cloudflare’s worldwide content delivery network for protection if you get hit with DDoS (distributed denial of service) attack or even just a big spike in visitors.
  3. DNS queries resolve quickly from any location and therefore improve website performance. DNS changes also propagate faster – often almost instantly.

You start by creating a Cloudflare account and clicking the blue “+ Add a Site” button. Enter your domain name (without the www) and wait a few seconds for Cloudflare to copy any existing DNS records from the old nameservers. Then select a plan (the free one is usually enough) and follow the instructions at your domain registrar to change your nameservers (“NS” records) to point to Cloudflare. For example, at Namecheap you would go to Domain List > Manage > Advanced DNS > Personal DNS Server and enter the two values copied from Cloudflare > [select domain] > DNS > Cloudflare Nameservers.

For a site at hosted at IPv4 address and IPv6 address 2001:db8::1 on a server called the essential records you would need to set up in Cloudflare > DNS would look like these:

Type Name Contents
A www
A mail
AAAA 2001:db8::1
AAAA www 2001:db8::1
AAAA mail 2001:db8::1
TXT v=spf1 ip4: ip6:2001:db8::/64 ~all
TXT 2020._domainkey v=DKIM1; k=rsa; t=s; p=[public key pasted from Server Configuration > Domainkey Options > DDKIM public key in PEM format without line breaks or quotes]

Note that by default, Cloudflare sets Proxy status to “Proxied” (orange clouds) – I recommend initially setting the status to “DNS only” (grey clouds) because there are some additional steps to take and some implications to be aware of before enabling proxying.

In particular, you may get a redirect loop if you have any redirections enabled at Virtualmin > Server Configuration > Website Options or in Server Configuration > Website Redirects or in Services > Configure SSL Website.

Also, Fail2ban may block Cloudflare unless you enable module remoteip in Webmin > Servers > Apache Webserver > Global Configuration > Configure Apache Modules and create a file /etc/apache2/conf-enabled/remoteip.conf with appropriate content as described in this tutorial on DevAnswers. That will enable the display of the correct originating IP address in your access logs. In short, you need to create a file /etc/apache2/conf-enabled/remoteip.conf with the following contents. Note that is is no longer necessary to edit the apache2.conf file as well.

RemoteIPHeader CF-Connecting-IP
RemoteIPTrustedProxy 2400:cb00::/32
RemoteIPTrustedProxy 2606:4700::/32
RemoteIPTrustedProxy 2803:f800::/32
RemoteIPTrustedProxy 2405:b500::/32
RemoteIPTrustedProxy 2405:8100::/32
RemoteIPTrustedProxy 2a06:98c0::/29
RemoteIPTrustedProxy 2c0f:f248::/32

Third party services (like Google Webmasters) sometimes ask you to validate your domain by adding additional CNAME or TXT records – if so, just add them the same way following their instructions.

  • Even if you don’t plan to use email for your domain, you are recommended to create at least the “webmaster@”, “postmaster@”, “hostmaster@” and “abuse@” incoming addresses for every domain so you can be notified about problems that might cause your site to fail or be blacklisted.
  • Note that you should normally have only one MX record and it should normally point to the mail server by hostname rather than by IP address.
  • See this FAQ about common mistakes that can delay or prevent delivery when setting up Sender Policy Framework (SPF) records.

Once DNS settings have propagated, files in the public_html folder will automatically be displayed to the world. By default, the file for the home page should be named index.html or index.php. A MySQL database is created for each site by default and can be managed at Virtualmin > Edit Databases. You can also install the useful database management tool phpMyAdmin on one of your sites from Virtualmin > Install Scripts.

DNS errors are so common that there’s a saying in the industry “It’s always DNS!”. Be careful, make backups and consult an expert if in doubt.

Adding PHP versions

PHP versions have a support life of only two years, whereas Ubuntu “Long Term Support” releases are supported for ten years. This isn’t necessarily a problem because Canonical backports PHP security patches from later PHP versions to keep the older version working for the lifetime of the Ubuntu LTS release. Nevertheless it’s good to keep up with the latest PHP versions because they are often faster and contain new features that modern websites may require.

Virtualmin supports running multiple PHP versions, but make sure you have a backup of the entire server before trying this, because it’s easy to mess up the PHP installation and bring all your sites down.

You have to enable an external repository (not managed by Ubuntu) by executing these commands:

sudo apt-get install software-properties-common
sudo add-apt-repository ppa:ondrej/php
sudo add-apt-repository ppa:ondrej/apache2
sudo apt update
sudo apt full-upgrade

You can then install packages for specific versions, for example:

sudo apt install php8.2-fpm php8.2-cgi php8.2-cli
a2enconf php8.2-fpm
systemctl restart apache2

To set a specific version as the default on the command line and check it:

sudo update-alternatives –set php /usr/bin/php8.0
sudo php -v

Check that your sites are still working. Then go to Virtualmin > System Settings > Re-Check Configuration to make Virtualmin recognise the new available PHP versions. You can now select the version used by each site at Virtualmin > Server Configuration > PHP Versions

It’s also possible to select different PHP “execution modes” for each site at Virtualmin > Server Configuration > Website Options – I recommend FPM because it’s significantly faster and uses less memory. This is now the default, but I’ve found that the settings set by Virtualmin result in excessive memory usage if your server hosts many sites. This can be avoided by adding the following settings to Virtualmin > Services > PHP-FPM Configuration for each site, which reduces the number and lifetime of PHP processes.

pm = ondemand
pm.process_idle_timeout = 10s
pm.max_requests = 200

To make the above settings the default for new servers, add them to Virtualmin > System Settings > Server Templates > Default Settings > PHP options > Additional FPM pool options and again in the Settings for Sub-Servers template. The Default PHP service maximum sub-processes setting is also very useful and affects the pm.max_children value. Note that the PHP documentation and Virtualmin documentation both incorrectly state that pm.max_children has no effect in the “ondemand” mode – it does work. 

Configuring email

Email users can be added at Virtualmin > Edit Users > Add a user to this server. Make sure your users have strong passwords to prevent accounts being hacked. A password policy can be set at Webmin > System > Users and Groups > Password restrictions.

By default, all mail will be stored on your VPS. Mail forwarding can be set up under Virtualmin > Edit User > Mail forwarding settings and is sometimes very useful, but it’s dangerous because any forwarded spam can get the whole server blacklisted. It’s also possible to set up filters in Usermin (using Procmail) but Procmail forwarding has multiple problems (including discarding messages without warning and looping) and should be avoided.

Make sure you have SPF, DKIM and DMARC records set in your DNS settings as described above, otherwise outgoing mail is very likely to be blocked by spam filters. 

Alternatively, users can configure their Gmail accounts to use POP3 to retrieve mail that is temporarily stored on your VPS. This is sometimes more reliable than forwarding, but the disadvantage is the polling interval can cause incoming mail to be delayed by up to an hour, which is sometimes very annoying. Using POP3 to retrieve mail to a personal PC is even worse – you can easily lose your entire mail archive (because pst files are hard to back up) and you can’t access it from a phone. 

If you do store email on the VPS, users can access it at the Usermin address (e.g. or you can install a more sophisticated application such as Roundcube or Squirrelmail, which are easily installed with “one click” from Virtualmin > Install Scripts.

Setting up SPF, DKIM and DMARC

To set up Sender Policy Framework (SPF) to help delivery of outgoing email, all you need to do is add an appropriate TXT record to the DNS settings of each of your hosted domains, as shown in the example above. See this FAQ about common mistakes.

To set up DomainKeys Identified Mail (DKIM), first visit Virtualmin > Email Settings > DomainKeys Identified Mail and click “Install Now” if necessary to install opendkim. Next, choose a simple “selector” (anything you like, typically the current year) and accept the other default options. Once the selector is chosen, you can’t change it except by disabling DKIM. Warning: Due to a Virtualmin bug, disabling DKIM will require you to completely uninstall opendkim and start again. This will require DNS records to be updated for all current domains. 

You can selectively list the domains you want to sign on that page, but it’s easier to just enable signing for every domain by default. To do this, create a file /etc/dkim-keytable in the root of your VPS containing the following content (set [your selector] to the one you chose):

default    %:[your selector]:/etc/dkim.key

Then create a file /etc/dkim-signingtable containing this single line:

*    default

Then add these two lines at the bottom of /etc/opendkim.conf in the root of your server:

SigningTable refile:/etc/dkim-signingtable
KeyTable /etc/dkim-keytable

While you’re there, I suggest changing the “Canonicalization” setting to relaxed, otherwise mail forwarded by Microsoft servers (in particular) is likely to be altered in transit and rejected.

Canonicalization relaxed/relaxed 

To set up Domain-based Message Authentication, Reporting, and Conformance (DMARC) you need an address to process the emailed reports (they’re not very human friendly) and a free account from Postmark is an easy way to do that, with a weekly summary and recommendations for fixes. It’s also an easy way to generate the DNS record you need. This will be a TXT record with a name like _dmarc.server1 and containing a string like this:

v=DMARC1; p=reject; pct=100; rua=mailto:[special key]; sp=none; aspf=r;

I no longer recommend setting up Sender Rewriting Scheme (SRS) because I no longer recommend forwarding email to destinations outside the server. Also SPF is widely ignored and fairly irrelevant now – use DKIM instead.

If everything is set up correctly, you should be able to send a test mail from an address hosted on your server to a Gmail account or Outlook Mail from Webmin > Webmin Configuration > Sending Mail. In Gmail or Thunderbird, you can click the test message when it arrives, then click the message menu (three dots) at the top right then “Show original” and you should see SPF, DKIM and DMARC all showing “PASS”. Then send an email from an address that is NOT hosted on your server but is forwarded via your server to a Gmail account. You should see SPF still pass and the “Return-path” or “Envelope from” header showing an encoded address.

The default size limit for emails is 10 MB which is a bit small these days. You can increase it (to say 25 MB, matching Gmail) at Webmin > Servers > Postfix Mail Server > General Resource Control

Autoconfiguration of email clients

Email clients like Microsoft Outlook, Apple Mail, Gmail and Thunderbird will do their best to detect the correct email settings to use, given only your email address and password. (Note that there is a configuration page in Virtualmin > Email Settings that was supposed to help with this, but it’s now ineffective and obsolete.)

They will try to connect to server names like “” for incoming and outgoing mail, so you have to make sure the “mail” subdomain resolves correctly in your DNS settings – similar to adding records for “www”. Postfix now supports “SNI” in versions 3.4 and later (Ubuntu 19.10 and later), which allows this process to work. In Virtualmin > Services > Configure SSL Website > Edit Directives, check that there is a line similar to this near the top and if not, add it:


Repeat for Virtualmin > Services > Configure Website > Edit Directives (without the SSL). Click “Save and close” then “Apply changes” at the top right. Now when you return to Virtualmin > Server Configuration > SSL Certificate > Let’s Encrypt you should see the “mail” subdomain included in the list and you can click “Request Certificate” again to add it to the certificate.

Note that the correct port for encrypted outgoing mail connections is port 587, but some mail clients still use port 465, even though it has been deprecated for years. The configuration that enables that is in file /etc/postfix/ and appears as “-o smtpd_tls_wrappermode=yes”. It is normally enabled, but if it’s not working check that it’s not commented out, and also check that Webmin > Networking > FirewallD is not blocking service “smtps”.

Spam filtering

Virtualmin sets up Clamav virus scanning and Spamassassin spam filtering on incoming email by default. Unfortunately this setup is quite ineffective (doesn’t work with forwarded messages, appalling detection rates for attachments) and uses lots of memory. My preferred solution is to set up Postscreen and Amavis as described below to catch email spam before it causes server load and before messages are forwarded.

I no longer perform automatic virus scanning (as opposed to spam scanning) on emails because sadly almost all the free or reasonable cost Linux virus scanners are now useless or withdrawn. Most mail clients have their own virus protection, which is easier for users to find and configure.


The Postfix postscreen daemon is a useful first step in spam protection – it rejects misbehaving mail clients early, which provides additional protection against mail server overload. It’s disabled by default but easily enabled by adding the following lines to Webmin > Servers > Postfix Mail Server > Edit Config Files >

# Enable postscreen, see
postscreen_access_list = permit_mynetworks
postscreen_dnsbl_threshold = 2
postscreen_dnsbl_sites =
postscreen_dnsbl_action = enforce
postscreen_greet_action = enforce

# Get rid of “postscreen_cache: unable to get exclusive lock” errors
postscreen_cache_map = memcache:/etc/postfix/postscreen_cache
postscreen_cache_cleanup_interval = 0

Note that I have included tests from some reputable DNS blacklists – these can make a big difference to server load. Barracuda requires you to register your IP addresses (IPv4 only). Note also that I have enabled memcached to avoid a problem with “unable to get exclusive lock” errors in the mail log. Memcache is also useful for caching plugins like W3 Total Cache in WordPress. You can install it with a simple APT command:

sudo apt install memcached php-memcache php-memcached

(Warning: Centos does not currently secure memcache properly by default but Ubuntu is OK).

Create a simple file called /etc/postfix/postscreen_cache containing these contents:

memcache = inet:
key_format = postscreen:%s

Then make these modifications to /etc/postfix/ (same Webmin screen):

# Enable postscreen, see
# 2. Comment out the “smtp inet … smtpd” service
# 3. Uncomment the new “smtpd pass … smtpd” service and duplicate any “-o parameter=value” entries from the smtpd service that was commented out in the previous step
# 4. Uncomment the new “smtp inet … postscreen” service
# 5. Uncomment the new “tlsproxy unix … tlsproxy” service
# 6. Uncomment the new “dnsblog unix … dnsblog” service
#smtp inet n – y – – smtpd -o smtpd_sasl_auth_enable=yes
smtp      inet  n       –       y       –       1       postscreen
smtpd     pass  –       –       y       –       –       smtpd -o smtpd_sasl_auth_enable=yes
dnsblog   unix  –       –       y       –       0       dnsblog
tlsproxy  unix  –       –       y       –       0       tlsproxy

Finally restart postfix, send a few test messages in and out and check the mail log at Webmin > System > System Logs or using this command:

tail -f /var/log/mail.log


Amavis spam filtering is easy to install from standard Ubuntu packages (there is no Webmin module for this):

sudo apt install amavisd-new pyzor razor opendkim
sudo apt install arj bzip2 cabextract cpio rpm2cpio file gzip lhasa nomarch pax rar unrar p7zip-full unzip zip lrzip lzip liblz4-tool lzop unrar-free

It must be enabled by uncommenting these lines in file /etc/amavis/conf.d/15-content_filter_mode

@bypass_spam_checks_maps = (
   \%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re);

By default, quarantined messages are saved in /var/lib/amavis/virusmails. I prefer to save them in a mailbox where they can be viewed more easily to check that wanted emails aren’t being blocked, and then automatically deleted after 30 days. To set this up, go to Webmin > System > Users and Groups > amavis and click the Login to Usermin button. In Mail > Manage Folders click Add A Folder Of Type “Local Mail File” and give it a name such as “Quarantine”. Use the Auto-Clearing button to enable automatic deletion.

In file /etc/amavis/conf.d/50-user add the following lines.

$sa_tag_level_deflt         = -999;  # always add spam info headers
$QUARANTINEDIR = ‘/var/lib/amavis/Maildir/.Quarantine/new’; 
$quarantine_subdir_levels = 0; # disable quarantine dir hashing
$spam_quarantine_method = ‘local:spam-%b-%i-%n’; # Don’t gzip files

# Prevents {RelayedOpenRelay} warning
@local_domains_maps = 1;

Note that there are currently some bugs in the Webmin “SpamAssassin Mail Filter” module in this situation. The “Spam Classification” options are ineffective because they are overridden by Amavis. To get the “Apply Changes” button to work, you need to go to Webmin > Servers > SpamAssassin Mail Filter > Settings (cogwheel) > SpamAssassin and other daemon process names to restart after changes and change the settings to these:

spamassassin amavis

In the same settings page, click Setup Procmail For SpamAssassin if you see a warning message that Procmail is not set up.

In file /etc/postfix/ add this:

content_filter = smtp-amavis:[]:10024

At the end of file /etc/postfix/ add this:


smtp-amavis unix    –   –   –   –   2   smtp
    -o smtp_data_done_timeout=1200
    -o smtp_send_xforward_command=yes
    -o disable_dns_lookups=yes
    -o max_use=20 inet  n    –    –    –    –    smtpd
    -o content_filter=
    -o local_recipient_maps=
    -o relay_recipient_maps=
    -o smtpd_milters=
    -o smtpd_restriction_classes=
    -o smtpd_delay_reject=no
    -o smtpd_client_restrictions=permit_mynetworks,reject
    -o smtpd_helo_restrictions=
    -o smtpd_sender_restrictions=
    -o smtpd_recipient_restrictions=permit_mynetworks,reject
    -o smtpd_data_restrictions=reject_unauth_pipelining
    -o smtpd_end_of_data_restrictions=
    -o mynetworks=
    -o smtpd_error_sleep_time=0
    -o smtpd_soft_error_limit=1001
    -o smtpd_hard_error_limit=1000
    -o smtpd_client_connection_count_limit=0
    -o smtpd_client_connection_rate_limit=0
    -o receive_override_options=no_header_body_checks,no_unknown_recipient_checks

Also add the following (indented) two lines immediately below the “pickup” transport service:

    -o content_filter=
    -o receive_override_options=no_header_body_checks

Restart amavis and postfix and check the configuration using these commands:

systemctl restart amavis
systemctl restart postfix

Installing Razor, Pyzor and DCC

The additional spam filtering solutions Razor, Pyzor and DCC (Distributed Checksum Clearinghouse) can together make a big difference to spam filtering. They can be enabled or disabled in file /etc/spamassassin/v310.pre. 

loadplugin Mail::SpamAssassin::Plugin::DCC
loadplugin Mail::SpamAssassin::Plugin::Pyzor
loadplugin Mail::SpamAssassin::Plugin::Razor2

To install Razor:

sudo apt install razor
razor-admin -create
razor-admin -register

To install and test Pyzor:

sudo apt install pyzor
pyzor ping

To install and test DCC:

tar xfvz dcc.tar.Z
cd dcc-2.3.168
CFLAGS=”-O2 -fstack-protector” DCC_CFLAGS=”-O2 -fstack-protector” ./configure && make && make install
cdcc info

This text from GTUBE can be pasted into a mail message and sent to one of the accounts on your VPS to test that spam filtering works.


To install and test OpenDMARC:

There’s a good tutorial here but here’s a summary of the required steps.

Run this command to install and run the package that checks the published DMARC policy of incoming emails. Choose “No” if asked whether to generate a database. If you see an error message after the first install command, restarting the server may help.

sudo apt install opendmarc
systemctl status opendmarc
systemctl enable opendmarc

In file /etc/opendmarc.conf set the following values:

AuthservID OpenDMARC
Socket local:/var/spool/postfix/opendmarc/opendmarc.sock
IgnoreAuthenticatedClients true
RequiredHeaders true
SPFSelfValidate true

Run these commands to create a socket file and a user with the right permissions:

sudo mkdir -p /var/spool/postfix/opendmarc
sudo chown opendmarc:opendmarc /var/spool/postfix/opendmarc -R
sudo chmod 750 /var/spool/postfix/opendmarc/ -R
sudo adduser postfix opendmarc
sudo systemctl restart opendmarc

In /etc/postfix/ find the line beginning smtpd_milters  and add to the end of it ,local:opendmarc/opendmarc.sock. Do the same for non_smtpd_milters  so the result looks something like this.

smtpd_milters = inet:localhost:8891,local:/opendmarc/opendmarc.sock
non_smtpd_milters = inet:localhost:8891,local:/opendmarc/opendmarc.sock

In file /etc/spamassassin/ add these lines:

header CUSTOM_DMARC_FAIL Authentication-Results =~ /dmarc=fail/
header CUSTOM_DMARC_QUARANTINE Authentication-Results =~ /dmarc=quarantine/

Send yourself an email and examine the headers to check that it’s working.

If you find that you’re getting a lot of spam from an identifiable sender, you can modify the filtering at Webmin > Servers > Spamassassin Mail Filter > Header and Body Tests > Switch to advanced mode. The default settings in Webmin don’t work, but with a bit of research into regular expressions you can get good results. For example, to reduce spam from emails using the top-level domain ‘best’, try creating a rule called something like FROM_BEST_DOMAIN for header From:addr that matches regular expression /\.best$/i with a score of 1 point. 

Reporting spam

Spammers now change their sending address and name every few seconds to avoid spam filters. They also change the contents of their messages and hide key words in images. This means that traditional spam filters based on keyword lists and Bayesian training using a “Spam” or “Junk” folder are becoming ineffective (and I no longer recommend using a Spam folder at all, because wanted messages get lost there). What does work are the reputation services (like DCC and Razor and the DNS blacklists) that respond in real time to reports of the latest offenders. But where do these reputation services get their data? They rely on users reporting messages as spam. 

There’s no point reporting old spam – the spammers will have moved on. But fresh, persistent, annoying spam is worth reporting, and there are two methods I use for that.

Webmin has a built-in reporting system but it’s a bit hard to find. Go to Webmin > Servers > Read User Mail and find the user and the message you want to report. At the right you’ll find an orange “Report Spam” button. This uses the Spamassassin reporting system.

The second method I use is to copy the message headers to This system is good at removing obfuscation attempts and revealing the true source of spam.

Fixing a blacklisted server

It’s common for a new mail server to be blacklisted by some recipients until it has built up a good reputation (Microsoft servers including and are notorious for this). If this happens, mails to some specific destinations will start bouncing. All is not lost – you can simply redirect outgoing messages to those destinations via a trusted third party server for a while. You will need to set up an account at a reliable mail provider (I use Amazon SES) and register all the domains you host, so the external mail service knows how to sign your messages with DKIM and how to handle bounces. It sounds complicated, but it’s really quite easy.

For example, to set up an account at Amazon Simple Email Service, follow their Developer Guide, in particular the sections on “Setting up”, “Moving out of the sandbox”, “Obtaining SMTP credentials“, “Integrating with Postfix” and Bring your own DKIM. Make sure the desired region is set consistently at the top right next to your username!

If you already have DKIM records set up in your DNS settings you don’t need to change any DNS settings (or wait for them to propagate), you can just copy the private key and selector that Amazon needs from Virtualmin > Email Settings > DomainKeys Identified Mail > DKIM Private Key in PEM format. Make sure you remove all the line feeds!

Create a file called something like /etc/postfix/sasl_passwd that is only visible to the root user (chmod 0600) containing a single line with the SMTP username and key you were given above. It will look something like this.

[]:587 smtp_username:smtp_password

(Where smtp_username and smtp_password are the ones you were given by Amazon.)

Create another file called something like /etc/postfix/transport containing lines like these:

/.*@hotmail.*/i relay:[]:587
/.*@outlook.*/i relay:[]:587
/.*@live.*/i relay:[]:587
/.*@msn.*/i relay:[]:587

Add lines similar to these to /etc/postfix/

transport_maps = pcre:/etc/postfix/transport
smtp_sasl_auth_enable = yes
smtp_sasl_security_options = noanonymous
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_tls_note_starttls_offer = yes

Finally, run these commands to make the changes take effect.

sudo postmap /etc/postfix/sasl_passwd
sudo systemctl restart postfix

Testing and monitoring

You should check log files regularly at Webmin > System > System Logs for the whole system and at Virtualmin > Logs and Reports for individual sites for signs of problems or malicious activity. I find it useful to include these logs in the System Logs screen by clicking the cogwheel at the top left.

/var/log/fail2ban.log Fail2ban log
/var/log/mysql/error.log MySQL error log
/var/log/php8.1-fpm.log PHP-FPM 8.1
/var/log/php8.2-fpm.log PHP-FPM 8.2

There are a number of very useful free services that can be used to monitor your sites:

  • Logwatch – Analyses logs and sends a daily digest to the administrator. Needs installation.
  • Dnssy – checks all your DNS settings
  • F8lure – pings your server once a second to check for network problems or CPU overload, alerts when down
  • Mxtoolbox – alerts when blacklisted, can also “port scan” your firewall and other useful DNS queries
  • Uptime Robot – checks how fast your pages load once a minute, alerts when down. A phone app is available for “push” notifications. Include a check for a keyword on the page that only appears when PHP is working, otherwise you might miss warnings when PHP is down.
  • Loader – simulates many simultaneous users
  • Matomo (formerly Piwik) – Similar to Google Analytics but hosted on your own server, shows how your visitors behave
  • SSL Labs server test – Checks SSL installation
  • Postmark – Free application that receives and parses DMARC reports and emails you the results weekly

I suggest enabling these scheduled update and validation checks:

  • Webmin > System > Software Package Updates > Scheduled checking options
  • System Information > Virtualmin Packages > Scheduled checking options
  • Virtualmin > Limits and Validation > Scheduled Validation
  • Webmin > System > Scheduled Cron Jobs – create a job that runs regular virus scans (such as rkhunter)
  • Webmin > Others > System and Server Status – Enable Scheduled Monitoring of Postfix, SSH, Webmin, Dovecot, Apache, MySQL, Webmin, Free Memory, Load Average and Disk space

If you want to test your setup at home before paying for commercial hosting, you can easily do so using a virtual machine. VirtualBox is free and easy to use and runs on Windows, MacOS, Linux and Solaris. Create a virtual machine with at least 1 GB of RAM and 20 GB of disk space and set the network mode to “bridged”. Then download the operating system you plan to use as an “iso” file, mount it as a virtual CD, reboot the virtual server and follow the installation prompts.

Tuning performance and memory usage

If your server starts running slowly or crashing, go to the Webmin dashboard and click on any of the dials in the System Information area that are red, to find out which processes or sites are using the most resources. Then go to Virtualmin > Logs and Reports > Apache Access Log to see what is going on. You may find an automated attack is the cause, and that can usually be blocked with an appropriate Fail2ban jail.

I strongly suggest testing your site with a (free) service such as Loadimpact to ensure it can withstand a sudden spike in traffic. If you find problems, check the settings below.

Tune pm.max_children

Try increasing or decreasing the value of pm.max_children in Virtualmin > Services > PHP-FPM Configuration > Edit Configuration Manually. If it’s too high the whole server may crash or freeze under load. If it’s too low you may see warnings like these in your PHP logs (e.g. /var/log/php8.0-fpm.log):

WARNING: [pool xxxxxxxxxxxx] server reached max_children setting (10), consider raising it

To find the corresponding configuration file, make a note of the pool number (xxxxxxxxxxxx in the example) then find the matching file with a .conf extension in the pool.d folder, such as /etc/php/8.0/fpm/pool.d/xxxxxxxxxxxx.conf. Change the pm.max_children value there then restart the PHP server – simply click the restart symbol beside it on the Webmin dashboard.

Tune PHP OPcache

An easy way to check the PHP OPcache settings is to use the WordPress plugin WP OPcache. Values can be adjusted by searching for variables such as opcache.memory_consumption in your php.ini file (e.g. /etc/php/8.1/fpm/php.ini). Note that these settings will affect every site that uses that version of PHP on your server.

; For Nextcloud, see

; For Newspaper theme
max_input_vars = 2000

Tune MySQL

Tuning databases is a big subject but a good place to start is to check the error log at /var/log/mysql/error.log to see if there are any warnings or suggestions about low resources.

Some distributions (but not Ubuntu 22.04) enable “bin logs” in MySQL or MariaDB by default and these can take up a huge amount of disk space in /var/lib/mysql. They are only required if the database is being replicated to another server, and can be safely disabled by adding the statment skip-log-bin to the bottom of /etc/mysql/mysql.conf.d/mysqld.cnf. Note that it is NOT safe to delete the bin logs manually.

Increase swap size

The default size of “swap” memory may be a bit small. You can increase it to say 8 GB using these commands:

sudo swapoff -a
sudo fallocate -l 8G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
sudo cp /etc/fstab /etc/fstab.bak
echo ‘/swapfile none swap sw 0 0’ | sudo tee -a /etc/fstab


Your websites could vanish without warning, even at a large reputable host. It has happened to me more than once. Common causes are denial of service attacks, your site being hacked, the host going out of business, power or network failure, an expired credit card or simple human error. Your hosting provider may be swamped with calls and unresponsive when this happens. If you have a recent off-site backup and control of your domain names you can recover everything within a couple of hours – if not, recovery may be lengthy or impossible. Backups are important!


Backup fast-changing MySQL database contents at least daily using an automated script. In Ubuntu, this is easily done by installing automysqlbackup.

sudo apt install automysqlbackup

I change the default location by editing configuration file /etc/default/automysqlbackup and changing the BACKUPDIR setting so they can be found more easily and included in backups of the /home folder (made with Duplicacy, see below).


Make sure you set the ownership to root and permissions to 0600 on this folder and all subfolders.

Note that recent versions of Ubuntu no longer include the username and password where it’s expected by this script. You’ll need to manually add them to /etc/mysql/debian.cnf until the automysqlbackup script is fixed. You can find them in Webmin > Servers > MariaDB Database Server > Change Administrator Password.

Virtualmin backups

Virtualmin can do scheduled backups of all files, database contents, email and settings. Set them up at Virtualmin > Backup and Restore > Scheduled Backup. I give the backup files a name like incremental_%Y-%m-%dT%H%M.

I use this to send backups to a Synology NAS device via SFTP. You will need to create a public SSH key for the root user as described above, and copy it to the authorized_keys file for the backup user on the NAS. Setting up a NAS is outside the scope of this tutorial, but I create an encrypted folder for this purpose in Control Panel > Shared Folder and a backup user in Control Panel > User & Group who only has access to that folder.

Webmin settings can also be saved, see Webmin > Backup Configuration Files > Scheduled backups. The filename I use for these is simply the weekday backup_%A because it’s a fairly primitive backup with no rotation, and this effectively gives a 7 day history.

Other methods

The Virtualmin backup system can in theory send backups to Amazon S3 or Backblaze but I find them unreliable. I prefer to use Duplicacy for cloud backups as described in a blog post here. I use it to back up to spare OneDrive storage that is effectively free for me.

Your VPS host may offer snapshot backups (TransIP includes snapshots of the whole VPS every 4 hours in their BladeVPS/X4 plan, which are sometimes a lifesaver), but remember they are likely to vanish if your hosting provider does.

A final word about security

My no. 1 tip for keeping a VPS secure is to keep it constantly updated with security patches (including all WordPress plugins). Most hacks happen through known vulnerabilities that are easily exploited.

My no. 2 tip is to set up daily off-site backups, including database contents. Human error (accidentally deleting files) is an even bigger threat than hackers and in any case it’s impossible to make a VPS 100% secure or reliable so you need to be able to recover quickly.

My no. 3 tip is to enforce long unique passwords and limit login attempts on every account that can upload files or modify the server. A typical 8 character “random” password can in some circumstances be cracked in less than 30 seconds but a 10 character password typically takes months.

My no. 4 tip is to keep an eye on log files using a utility like logcheck or logwatch and set up monitors at Webmin > Others > System and Server Status so you’re warned quickly if anything is wrong.


Locale – Ubuntu community help

Virtualmin installation instructions

Postfix Postscreen – How to enable and configure it to prevent spam

Setting Up DKIM And SRS In Postfix

Preventing backscatter (non-delivery records) from forwarded spam

More VPS tutorials

Guide to starting a hosting business

Firstsiteguide Web Hosting Services Explained

The Perfect Server tutorials from HowtoForge, using ISPConfig as a control panel

Mozilla SSL Configuration Generator 

Sender Policy Framework FAQ – Common mistakes

How big is your haystack? – Passwords need to be at least 10 characters

Set up OpenDMARC

Scroll to Top