VPS Tutorial

This tutorial is about setting up and managing a Virtual Private Server (VPS) for hosting websites and email. It is aimed at people who are considering moving from shared web hosting to VPS or cloud hosting (or even a dedicated server) in order to improve the speed and reliability of their sites, or moving from managed to unmanaged hosting to save cost. By managing the server yourself with the aid of an open-source control panel such as Virtualmin you can save a lot of money while avoiding most of the restrictions of shared hosting.

The same instructions can be used to set up a test server that runs in a window on your own local computer (Mac, Windows or Linux) using Hyper-V, VirtualBox or VMWare, or on a spare PC or laptop.


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 actually be less than on shared hosting. 

Getting started

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

  • 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. Console access is sometimes not possible from a phone.

If it helps, my current VPS host is TransIP in Amsterdam and uses the KVM hypervisor, my domain registrars are Namecheap and Gandi.net, 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 www.webhostingtalk.com 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, or preferably double that.
  • 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 allow email hosting? Many cloud hosts completely block mail ports to prevent spam abuse. You may or may not be able to convince them to unblock the ports by raising a support ticket. If not, you’ll have to use a third party for sending mail (I use Amazon SES) or even for receiving.
  • Do they offer IPv6 connectivity? It’s not essential yet but is likely to be soon and hosting is sometimes cheaper if you don’t need IPv4.
  • 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 “snapshot” backups in case you mess up the configuration? How hard is it to restore a full or partial backup?
  • Are all backups encrypted and stored off-site? It’s good practice to encrypt data “at rest” and could save you having to contact all your customers if backups are compromised or there’s a disaster or the host goes out of business (it has happened to me twice), but many hosts don’t offer this.
  • Can you install the operating system from your own “iso” image or from a virtual machine image? You will need this to make and restore your own whole-server backups, or to enable whole-disk LUKS encryption during installation. Many hosts don’t allow it.
  • Is the recovery console accessible from a phone? Believe it or not, many hosts can’t do this – the device must have a physical keyboard.
  • Is it possible to add admin users to your account? If not and you want to allow someone else access, you’ll have to share a password and disable 2FA. Ugh!
  • 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

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 server.mydomain.com.
  • 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 by default). See below for how to fix this after installation. 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 – again, 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 install the operating system manually from an attached “iso” image rather than using a standard installation from the hosting provider. Not all VPS hosts allow this.

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. Not all consoles allow cutting and pasting of commands, so you might have to type these manually. 

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. See below for how to fix this if it has already been set up.

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

You will need to reboot for this change to take effect.

sudo reboot

If you see unwanted console messages about “cloud-init” you can get rid of them with the following command.

sudo touch /etc/cloud/cloud-init.disabled

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

If you want to be able to transfer sites between servers or set up automated backups via SSH, you will need a public/private key pair on the VPS itself. You can easily generate them with the following command (leave the password blank).

ssh-keygen -t ed25519

The public key will typically be saved in  /root/.ssh/id_ed25519.pub from where it can be copied to the authorized_keys file on the remote server. Remember to remove it again when it’s no longer needed, because forgotten keys are a common route for malware to spread between servers.

Installing the Virtualmin control panel

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

wget https://software.virtualmin.com/gpl/scripts/install.sh
sudo /bin/sh install.sh

If you want to install nginx instead of Apache use the following command instead. Be careful, however – WordPress is NOT fully compatible with nginx unless nginx is configured on top of Apache as a “reverse proxy”, which is not what this command will do.

sudo /bin/sh install.sh --bundle LEMP

If you want to install OpenLiteSpeed instead of Apache, first install Apache as above then replace it later as described below (because Webmin doesn’t support OLS alone). It’s also possible to replace MariaDB with MySQL or PostgreSQL later.

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 probably get a warning about an untrusted (self-signed) 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 the MariaDB database, 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 and counts bots as visitors. I use Matomo or Google analytics.
  • PostgreSQL database – I only use MariaDB (or MySQL) because most scripts require that.
  • ProFTPd virtual FTP – I use secure file transfers (SCP or SFTP) via SSH instead, for security, speed and reliability.
  • 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.

From now on things are much easier because Webmin gives you a file manager and a terminal window that accepts cut and paste commands and so on.

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.

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 console 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/default/motd-news and setting ENABLED=0 then go to /etc/update-motd.d/10-help-text and comment out the printf lines at the bottom.

Removing root passwords

If your host has messed up and created root passwords, here’s how to fix that.

Create a new username for yourself in the Webmin control panel at System > Users and Groups > Create a new user (don’t delete the old root user). Most people just use their first name in lowercase, perhaps appended with the first letter of their last name if it’s a common name. Set Shell to /bin/bash, add a long unique password and under Secondary groups select sudo and click the right arrow so it appears under In groups. Click the green Create button. You should now be able to login as this user in the recovery console or in an SSH terminal and become root again by entering the following command. Check that this works!

sudo -i

Next we need to do the same in Webmin, because that has its own user called root with full administrator privileges. Go to Webmin > Webmin Users, click root and change the name and password. These can be the same as the unix username and password you just created above. Then logout from Webmin using the red exit icon at the bottom of the left sidebar and check you can log in to Webmin with your personal username. I’d recommend enabling two-factor authentication on this account at Webmin > Webmin Users > Two Factor Authentication.

Finally, disable the root password by going to Webmin > System > Users and Groups > root > Password and selecting No login allowed. I also advise disabling Allow authentication by password in Webmin > Servers > SSH Server > Authentication

Additional packages

You will sometimes need to install extra packages that are required by the specific programs you install. 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 liblwp-protocol-https-perl libdigest-hmac-perl

Generally useful:

sudo apt install curl ubuntu-pro-client logwatch automysqlbackup rkhunter jq memcached

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

curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
wp --info

Additional 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. Webmin currently ships with PHP 8.1 which is no longer actively supported.

Virtualmin supports running multiple PHP versions, but make sure you have a “snapshot” backup of the entire server before trying this, because it’s easy to mess up the PHP installation and bring all your sites down. Most VPS hosts provide an easy way to make and restore snapshots, although there is often a small cost involved for the storage.

You will have to either compile the later PHP versions from source (not very difficult), or (even easier) 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.3-{common,fpm,cgi,cli}

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

sudo update-alternatives --set php /usr/bin/php8.2
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
pm.max_children = 12

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. While you’re there, make sure the Default PHP execution mode is set to FPM and the Default PHP version is a particular version (“Latest” is probably too risky). 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. 

Additional PHP packages

Generally useful:

sudo apt install php-memcached php-apcu php-redis

sudo apt install php8.2-{gd,zip,mbstring,intl,xml,mysql,uploadprogress,gmp,memcached,redis,apcu}

Needed particularly by WordPress (including for WooCommerce):

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

Apache configuration

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), include (only if server-side include scripts are used) and disabling any Apache php modules (such as php8.2). 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 if necessary:

  • http2
  • mpm_event
  • cgid
  • include
  • remoteip

You may not be able to change the “mpm_event” setting until you have removed any php* modules. 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 by using “grep” to search in /etc/apache2/sites-available/ and /etc/php/8.2/fpm/pool.d/.

MySQL configuration

Some applications (e.g. CiviCRM) will complain that the database lacks “Time Zone Tables”. The following command will load them. You will first need to create a mysql administrator username and password for yourself at Webmin > Servers > MySQL Database Server > User Permissions > Create a new user. Restrict the Hosts to localhost and select all the permissions. Then type the following command, which will prompt for your password.

mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql mysql

You can use the mariadb-check or mysqlcheck command to check, repair, analyze and optimize databases, for example:

mysqlcheck -p --all-databases

You will be prompted for your password.


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: Usermin won’t connect using IPv6 until you go to Webmin > Usermin Configuration > Ports and Addresses and enable Accept IPv6 connections.

Note: The Webmin install.sh script omits the package “net-tools” which is needed to correctly display the network configuration (see “Additional packages” above).

If you’re running nginx instead of Apache2 or OpenLiteSpeed, I found it necessary to add these lines to /etc/sysctl.conf, otherwise nginx won’t automatically start after the server is rebooted.

net.ipv4.ip_nonlocal_bind = 1
net.ipv6.ip_nonlocal_bind = 1

Also, make sure apache2 is disabled because that can also cause the same problem.

systemctl disable apache2

Check hostname and DNS

Check your hostname under Webmin > Networking > Network Configuration > Hostname and DNS Client – Webmin might shorten this, you want the complete “fully qualified” name (something like server.mydomain.com). 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.

In the same screen. if you have followed my advice above and disabled the BIND DNS domain Virtualmin feature, you may want to change the DNS servers for better resiliency. The best ones to use are often the ones the hosting company supplies – check their documentation. For example, Vultr requires you to use their DNS resolvers at and 2001:19f0:300:1704::6 if you want to use their DDoS protection. The Cloudflare public DNS service is a good choice for the third option. You should preferably have a least two addresses in the list (but more than three may be ignored). IPv6 addresses can be used. 

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 server.mydomain.com 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 https://ubuntu.com/pro 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. If, like me, you prefer to use Authy to do the authentication (so you can recover your accounts if you lose your phone), you rather confusingly need to select Google Authenticator here, otherwise you have to mess around with API keys instead of using QR codes. Set it up for yourself by click your username at the bottom of the Webmin sidebar then Security and limits options.

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 set a random password, increase the length to 20 characters and the allowed characters to the following set (avoiding “lookalike” characters or those that are difficult to type on some keyboards). If you do this, the password generator will pass its own strength check!


If you click your username at the bottom left of the Webmin menu, a settings window will open. If you have followed the tutorial this far, it probably shows your username as “root”, which is very confusing (and dangerous, because it encourages password sharing). I prefer to change it to match my Webmin username.

You can restrict access to certain Webmin modules depending on the user, which de-clutters the Webmin menus and improves security. I suggest creating a group with a name like “Administrators” in Webmin > Webmin Users and enabling all modules except those in the “Hardware” and “Cluster” groups. Add yourself to that group in the same screen. Then in Administrators > Available Webmin modules deactivate 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 uk.pool.ntp.org) and enable Synchronize on schedule. This is important, because if the server time drifts too far, two-factor authentication may fail. You will probably need to install the ntp package first (see “Additional packages” above).

The Virtualmin menu bar can also be customised to some extent at Virtualmin > System Settings > Virtualmin Configuration > User interface settings (for example to hide the “Pro” features, show disk quotas and PHP versions).

Disable unnecessary services

The proftpd, clamav and named services are usually not necessary and can be uninstalled at disabled to save memory by going to Webmin > System > Software Packages or using the following commands. 

sudo apt remove proftpd-core clamav bind9

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 responses to outgoing DNS queries may sometimes be blocked if they’re too large.)

Mitigate cookie hijacks

To mitigate cookie hijacks add these settings at the bottom of the /fpm/php.ini configuration files at Webmin > Tools > PHP Configuration

session.cookie_samesite = "Lax"
session.cookie_httponly = 1
session.cookie_secure = 1

Don’t forget to restart PHP after making any congfiguration changes. You can easily do this in the Service Status section of the Webmin dashboard.

Restrict obsolete cipher suites

In Webmin > Servers > Apache Webserver > Global Configuration > Configure Apache Modules make sure the “headers” and “expires” modules are enabled (see above). 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 for PCI DSS, HIPAA and NIST compliance, 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 https://ssl-config.mozilla.org/#server=apache&version=2.4.41&config=intermediate&openssl=1.1.1k&guideline=5.7
SSLProtocol             all -SSLv3 -TLSv1 -TLSv1.1
SSLSessionTickets       off

SSLUseStapling On
SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"

# Additional after testing at https://www.immuniweb.com/websec/
SSLHonorCipherOrder on
SSLSessionTickets off
Protocols h2 http/1.1
Header set Strict-Transport-Security "max-age=63072000"
Header set X-Content-Type-Options nosniff
Header set Referrer-Policy no-referrer-when-downgrade
Header set X-Permitted-Cross-Domain-Policies "none"
Header set Content-Security-Policy-Report-Only default-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/main.cf) 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 accepted if 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 preferable to send them to an external email address because there’s no point reporting a mail problem to a non-working mail server! 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”.

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: If you have removed proftpd as advised above, you may need to delete the proftpd jail in /ect/fail2ban/jail.local before fail2ban will start.

First, I suggest going to Filter Action Jails and setting the postfix and dovecot jails to something large (like 1,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 https://repo.imunify360.cloudlinux.com/defence360/imav-deploy.sh -O imav-deploy.sh
sudo bash imav-deploy.sh

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 https://docs.imunify360.com/hook_script.sh -O
chown root:_imunify hook_script.sh
chmod g+x hook_script.sh

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/hook_script.sh 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.

Replace Apache with OpenLiteSpeed

(Optional) 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 - http://rpms.litespeedtech.com/debian/enable_lst_debian_repo.sh | bash
sudo apt update
sudo apt install openlitespeed
sudo systemctl start lshttpd
sudo systemctl enable lshttpd

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 (note http, not https). Have a look around – most options have a question mark symbol next to them that you can click to find out what they do.

http://[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

You can also test the IPv6 address by pointing a web browser to http://[server ipv6 address] – note that you must surround an IPv6 address with literal square brackets, otherwise most browsers will just show you a search page. 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 below.

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.

systemctl stop lshttpd
rm -rf /tmp/lshttpd /usr/local/lsws/logs
apt reinstall openlitespeed
systemctl start lshttpd
systemctl status lshttpd

Before we add sites, we need to check the server configuration which holds default settings for all the virtual sites. In Server Configuration > General check that the user and group are www-data and set CPU Affinity to 1 for better performance. In the Log tab I set Keep Days to 90 and Compress Archive to Yes

In the External App tab we update and configure PHP support (the “Lightspeed SAPI App”). Importantly, this is where the PHP_LSAPI_CHILDREN variable is set – it has to be “tuned”, otherwise the server may crash or run slowly under load.

We first fix the problem of the default configuration using an obsolete PHP version (7.4) by installing a later version.

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

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 lsphp82/bin/lsphp. Maybe change the Name and Address to lsphp82 as well. Click Save and Graceful Restart.

To increase PHP resources, enable error logging and improve security under OpenLiteSpeed, find the php.ini file, which in our case is located at /usr/local/lsws/lsphp82/etc/php/8.2/litespeed/php.ini. I add the following settings to the end of the file:

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

; For some sites that need it:
memory_limit = 1024M
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 https://help.nextcloud.com/t/nextcloud-23-02-opcache-interned-strings-buffer/134007/8
opcache.memory_consumption = 1536
opcache.interned_strings_buffer = 128
opcache.max_accelerated_files = 50000
output_buffering = 0
apc.enable_cli = 1

To restart PHP, execute the following command (restarting lshttpd is not enough if PHP is “detached”, see https://openlitespeed.org/kb/php-detached-mode/

killall -9 lsphp

Adding Listeners

If this all works, the next step is to add some OpenLiteSpeed “Listeners”, which specify the allowed IP addresses and ports. If Apache is running, it will already be using port 80 (and port 443 for secure connections), so for now I will assume we will temporarily use alternative ports for testing OpenLiteSpeed, such as 8080 and 8443. Don’t forget to enable them in the firewall as described above.

If you don’t mind disrupting existing sites and prefer to set the listener ports to the normal ones (80 and 443) immediately, you will need to first stop Apache and prevent it starting automatically at boot time 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.

sudo systemctl stop apache2
sudo systemctl disable apache2

In the OpenLiteSpeed WebAdmin dashboard go to Listeners > Add (the plus sign at the right) and call the new listener something like “IPv4”. Set the port to 8080 and Secure to No. Click the Save button at the top right.

In Servers > Security, if multiple sites are to be sharing this server set Follow Symbolic Link to If Owner Match and enable Check Symbolic Link and Force Strict Ownership for better security.

We will also need at least one listener for SSL connections. Give it a name such as “IPv4_SSL”. In Address Settings select ANY IPv4 and Port 8443 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 server.mydomain.com) that should already exist under Virtualmin > Manage Virtual Server > Setup 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. If your server has an IPv6 address you should create a similar pair of listeners for that. Click Save and Graceful Restart. Check in the dashboard under Listeners that the status of all listeners is green.

Create a Virtual Host Template for OpenLiteSpeed

For this tutorial I’ll assume we will add the site with a name like “server.mydomain.com” site that matches the server’s hostname because it probably already exists in Virtualmin, or you could add a test site. Whichever site you use, make sure it works properly with Apache first, including SSL connections and logging.

Usually there will be multiple virtual hosts with similar configurations, so we start by creating a template in OpenLiteSpeed > VHost Templates. (The process for creating a single virtual host is very similar and starts at OpenLiteSpeed > Virtual Hosts.) 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 IPv4, IPv4_SSL, IPv6, IPv6_SSL (or whatever names you chose above). Click Save and then CLICK TO CREATE and Save again.

Click the VHost template you just created, then in General > Base set Default Virtual Host Root to /home/$VH_NAME (we won’t actually use this, but OpenLiteSpeed insists on a name containing $VH_NAME). Set Config File to $SERVER_ROOT/conf/vhosts/$VH_NAME/vhconf.conf as recommended by the pop-up help. Then under Base2 set Document Root to $VH_ROOT/public_html/. Under php.ini Override set php_value error_log “$VH_ROOT/logs/php_log”.

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 DEBUG, Rolling Size to 10M, Keep Day to 90, Compress Archive to Yes. 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 in the File Access Control tab set Follow Symbolic Link to If Owner Match and both Enable Scripts/ExtApps and Restrained to Yes. Under External App Security set External App Set UID Mode to DoCRoot UID

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 and Log Level to 0. Save and then set the following Rewrite Rules to always redirect insecure http requests to https by default, except for the “well-known” folder which is used by LetsEnrypt when setting up certificates.

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

In the Context tab, create a Static instance with the URI simply set to “/” (without the quotes) and Accessible set to Yes

Add the following statements in the Header Operations box, for extra security (perhaps copied from the Apache configuration above).

set Content-Security-Policy-Report-Only "upgrade-insecure-requests; default-src 'self'; style-src 'self'; style-src-elem 'self'; script-src 'self'; img-src 'self'; font-src 'self'; media-src 'self'; frame-src 'self'; connect-src 'self';"
set Strict-Transport-Security "max-age=15768000"
set X-Content-Type-Options "nosniff"
set Referrer-Policy "no-referrer-when-downgrade"
set X-Robots-Tag "noindex,nofollow"
set X-Frame-Options "sameorigin"
set X-Permitted-Cross-Domain-Policies "none"
set X-XSS-Protection "1; mode=block"

In the SSL tab, set the Private Key File to $VH_ROOT/ssl.key and the Certificate File to $VH_ROOT/ssl.combined with Chained Certificate set to Yes. Enable OCSP Stapling and ECDH Key Exchange.

We will need to change the default SSL certificate location in Virtualmin because OpenLiteSpeed doesn’t know what a Virtualmin “${ID}” is. In Virtualmin > System Settings > Server Templates > Default Settings > SSL website for domain change the Template for private key path to File in home directory. Repeat for the Settings For Sub-Servers template. If any existing sites have been set up in different locations, you can move the keys and reconfigure the locations in Virtualmin > Services > Configure SSL Website > SSL Options.

Adding new sites to OpenLiteSpeed

Now we will use this template to create a new virtual host, let’s call it “test.mydomain.com”. First add that site to Virtualmin in the usual way if it’s not already there, and set up DNS records for it. Check that it works with Apache.

Then in OpenLiteSpeed go to VHost Templates, click the template name we created above (“Virtualmin” in our example) and click the plus sign for Member Virtual Hosts. Set the Virtual Host Name to the domain of the site we are adding, “test.mydomain.com” in this example. If the site uses email, you should also add the “mail” alias that Virtualmin needs in Domain Aliases (“mail.test.mydomain.com” in our example). A “www” alias is created automatically, so you don’t need to worry about that.

Set Member Virtual Host Root to the path of the folder immediately above public_html. In our example, this would probably be “/home/test”, or if it’s a sub-site, something like “/home/parent/domains/test.mydomain.com”.

Click Save and Graceful restart.

If you point your browser to https://test.mydomain.com:8443 the site should appear, without warnings. The visit should appear in the access logs in Virtualmin and PHP and permalinks should work


  • 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. 
  • 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.
  • Check the standard error log for PHP errors, especially ones about increasing LSAPI_CHILDREN. It is enabled at OpenLiteSpeed > Server Configuration > Log > Enable stderr Log and can be viewed at /usr/local/lsws/logs/stderr.log.
  • You can disable individual sites temporarily at OpenLiteSpeed > Dashboard > Virtual Hosts (visitors to that site will see a 403 “access denied” error).

You should enable and configure the LiteSpeed Cache for each site individually by installing the plugin of that name in every WordPress site you host. The default settings work pretty well – go to Presets and work your way up from Essentials, testing as you go. You should see a significant improvement in page load speed and visitor capacity. 

It’s also possible to enable the OpenLiteSpeed cache for sites that don’t or can’t a LiteSpeed Cache plugin. Go to OpenLiteSpeed > Server Configuration > Modules > Cache and set enableCache to 1. You can test whether a site is successfully cached at https://check.lscache.io/.

So, here’s a checklist of the steps needed to migrate a 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 should be the domain name and the  Member Virtual Host Root should point to the folder above public-html.
  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, you can adjust the certificate paths in Virtualmin > Services > Configure SSL Website > SSL Options and adjust log paths at Virtualmin > Server Configuration > Website Options. Alternatively, to fix a compatibility issue from the OpenLightSpeed side, click the Instantiate button to the right of the Virtual Host Name in VHost Templates > Member Virtual Hosts to create an individual Virtual Host and make changes 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]

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 can also create “Sub-servers” and “Alias” servers. Alias servers are useful if for example you have both a “.com” and a “.co.uk” 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 Users and click the user with the same name as the site at the top left (the administrator user) then Mail forwarding settings.

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.2/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 mydomain.com). 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 www.mydomain.com. 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 > Server Templates > Website for domain.

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 www.mydomain.com hosted at IPv4 address and IPv6 address 2001:db8::1 on a server called server.hostdomain.com the essential records you would need to set up in Cloudflare > DNS would look like these:

TXTmydomain.comv=spf1 ip4: ip6:2001:db8::/64 ~all
TXT2020._domainkeyv=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 a list of trusted proxy addresses. Note that is is no longer necessary to edit the apache2.conf file as well.

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.

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 (unreliable) 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, you can install an application such as Roundcube from Virtualmin > Install Scripts or use the built-in Usermin interface on port 20000 (e.g. https://www.mydomain.com:20000), which allows access to additional features such as mailbox rules, spam reporting and password changes.

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 VPScontaining 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]@dmarc.postmarkapp.com; 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 will try to guess the correct email settings to use based on your email address. (Don’t bother with the configuration page at Virtualmin > Email Settings > Mail Client Configuration, it doesn’t work.)

Virtualmin and some mail clients assume you will use the “mail” subdomain (e.g. “mail.mydomain.com”) for incoming and outgoing mail, so you have to make sure that 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:

ServerAlias mail.mydomain.com

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 LetsEncrypt certificate.

Note that the correct port for encrypted outgoing mail connections is port 587, but some mail clients still try to use port 465, even though it has been deprecated for years. The configuration that enables that is in file /etc/postfix/master.cf 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”.

Searching emails

Sometimes important information arrives by email, therefore many people find it essential to be able to search the contents of all the email messages in their archive. Popular email providers like Gmail and Outlook include this functionality by default, but smaller providers who host personal domains often don’t, and for some reason it’s not included in Virtualmin.

For searching to work, your messages must obviously be saved in an archive somewhere, that archive needs to be indexed (otherwise searches will be much too slow) and your mail client needs to be able to access the archive somehow.

Historically, the mail archive was often kept on a PC and indexed by a program such as Outlook or Thunderbird. The problem with that is you can’t access it from a phone or from a web browser. Another problem is that such archives are often excluded from backups (because they contain large files that change frequently), which means your archive can be lost without warning.

A better solution is to store and index the messages on a web server, and search them using the IMAP protocol. Ubuntu now includes a way to do this, which is easily installed with the following command.

sudo apt install dovecot-fts-xapian

You can enable it by adding text such as this to the end of /etc/dovecot/dovecot.conf

mail_plugins = $mail_plugins fts fts_xapian

plugin {
    fts = xapian
    fts_xapian = partial=3 full=20

    fts_autoindex = yes
    fts_enforced = yes

    fts_autoindex_exclude = \Trash

    # Index attachements
    fts_decoder = decode2text

service indexer-worker {
    # Increase vsz_limit to 2GB or above.
    # Or 0 if you have rather large memory usable on your server, which is preferred for performance)
    vsz_limit = 2G

service decode2text {
    executable = script /usr/libexec/dovecot/decode2text.sh
    user = dovecot
    unix_listener decode2text {
        mode = 0666

Run these commands to generate the index (which might take several minutes).

sudo systemctl restart dovecot
doveadm user '*' | awk '!/nobody|lsadm/{print $1}' | xargs -I {} doveadm index -u {} -q '*'

Then create a cron job like this to optimise the index periodically, say once a day.

doveadm user '*' | awk '!/nobody|lsadm/{print $1}' | xargs -I {} doveadm fts optimize -u {} -q '*'

Not all mail apps or webmail sites support “full text search” – I’ll test a few and recommend them here.

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 in any case, 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 > main.cf:

# Enable postscreen, see http://www.postfix.org/POSTSCREEN_README.html
postscreen_access_list = permit_mynetworks
postscreen_dnsbl_threshold = 2
postscreen_dnsbl_sites = zen.spamhaus.org bl.spamcop.net b.barracudacentral.org
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. Memcached is also useful for caching plugins like W3 Total Cache in WordPress. You can install it as described above under “Additional PHP packages”.

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/master.cf (same Webmin screen):

# Enable postscreen, see https://www.postfix.org/POSTSCREEN_README.html
# 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 (at the bottom, not the virus checks) 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. Go to Webmin > Servers > SpamAssassin Mail Filter and If you see a warning message about Procmail not being set up, click Setup Procmail For SpamAssassin and set Action for messages classified as spam to Deliver normally. On the same page, click the Settings cogwheel then go to SpamAssassin and other daemon process names to restart after changes and change the settings to these:

spamassassin amavis

In file /etc/postfix/main.cf add this:

# https://help.ubuntu.com/community/PostfixAmavisNew
content_filter = smtp-amavis:[]:10024

At the end of file /etc/postfix/master.cf add this:

# https://help.ubuntu.com/community/PostfixAmavisNew

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:

wget https://www.dcc-servers.net/dcc/source/dcc.tar.Z
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

(It’s normal to see some “not answering” servers.)

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.


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 www.spamcop.net. 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 outlook.com and hotmail.com 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.

[email-smtp.eu-west-1.amazonaws.com]: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:[email-smtp.eu-west-1.amazonaws.com]:587
/.*@outlook.*/i relay:[email-smtp.eu-west-1.amazonaws.com]:587
/.*@live.*/i relay:[email-smtp.eu-west-1.amazonaws.com]:587
/.*@msn.*/i relay:[email-smtp.eu-west-1.amazonaws.com]:587

Add lines similar to these to /etc/postfix/main.cf

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.2-fpm.log PHP-FPM 8.2
/var/log/php8.3-fpm.log PHP-FPM 8.3

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.2-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.2/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.2/fpm/php.ini). Note that these settings will affect every site that uses that version of PHP on your server.

; For Nextcloud, see https://help.nextcloud.com/t/nextcloud-23-02-opcache-interned-strings-buffer/134007/8

; 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!

Virtualmin backups

Virtualmin can make scheduled backups of all files, database contents, email accounts and settings and send them to an external SFTP server or an Amazon S3 bucket. 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 a Synology NAS device for storage, but any spare external storage will do. 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 control panel settings can also be saved from Webmin > Backup Configuration Files > Scheduled backups. The filename I use for these is simply the month name backup_%B because it’s a fairly primitive backup with no rotation, and this effectively gives a 12 month history.

Virtualmin (and other control panels) have a limited number of backup options and you should have more than one backup, so I’ll discuss some other options for manual and automatic backups.

Database backups

Copying files and emails to a backup location is easy, but how do you backup database contents? Ubuntu has a package called automysqlbackup for this purpose. Install it with this command.

sudo apt install automysqlbackup

I change the default database backup location by editing configuration file /etc/default/automysqlbackup and pointing the BACKUPDIR setting to a folder inside the /home folder. This makes it easier to backup database contents using file backup utilities such as Duplicacy (described below).


Set the ownership to root and permissions to 0600 on this folder and all subfolders, to prevent hosted sites viewing each others’ database backups.

Note that recent versions of Ubuntu no longer support /etc/mysql/debian.cnf, so you will need to replace all references to that file in the automysqlbackup configurationThe “root” mysql user no longer has a password either so you’ll need to create a new user (e.g. “automysqlbackup”) in Webmin > Servers > MariaDB Database Server > User permissions. Allow that user access to Select table dataLock tablesShow view and Create trigger and set Hosts to localhost. Generate a random password.

PASSWORD="[insert the password you just created]"
DBNAMES=`find /var/lib/mysql -mindepth 1 -maxdepth 1 -type d | cut -d'/' -f5 | grep -v ^mysql\$ | grep -v ^performance_schema$ | grep -v ^sys$ | tr \\\r\\\n ,\ `

Other backup 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 (but NOTE I no longer recommend SRS and local DKIM is becoming irrelevant)

Preventing backscatter (non-delivery records) from forwarded spam (but NOTE I no longer recommend forwarding emails)

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 (but NOTE the instructions here no longer appear to work – I have temporarily removed them above)

Scroll to Top