A new VPS takes minutes to deploy, and RunCloud has it serving a site not long after. That makes it easy to leave half finished. The site loads, so everything looks fine, until enquiry emails land in spam or the hostname quietly resets after a reboot.
This guide shows how to set up a new Ubuntu VPS with RunCloud for WordPress hosting, step by step. It is the checklist we run on every server before a client site goes near it. Our servers are on Vultr and DigitalOcean, so those are the examples used, but the RunCloud steps are the same on any provider. Throughout, swap example.com, web1 and 203.0.113.10 for your own domain, server name and IP address.
Create the server
- Choose a dedicated CPU plan. Shared CPU plans are cheaper, but your sites compete with other customers for processor time, and a busy neighbour slows you down. On Vultr, dedicated plans are the Optimized Cloud Compute range. On DigitalOcean, they are the Dedicated CPU Droplets, such as General Purpose and CPU-Optimized.
- Size for the busiest site the server will host, not the average.
- Pick the location closest to the people visiting the sites.
- Select a fresh Ubuntu LTS release that RunCloud supports. Check before you deploy, as panels often trail the newest release. The server must be clean, with no other web server or panel installed.
- Turn on automatic backups. It is far cheaper than the alternative.
- Add your SSH keys so nobody needs to log in with a password.
- Attach a firewall. Vultr calls it a Firewall Group and DigitalOcean a Cloud Firewall. Open ports 22, 80 and 443, plus 34210 for the RunCloud agent.
- Enter the hostname and label. Use the full name you are about to create in DNS, such as
web1.example.com.
Set the timezone
Cron jobs and log timestamps follow server time. Set it now, or every scheduled task and every log line will be offset from the hours you actually work.
sudo dpkg-reconfigure tzdata
sudo service cron restart
timedatectl
sudo reboot
The first command walks you through choosing a region and city. Restarting cron makes it pick up the change, and timedatectl confirms the result before you reboot.
Connect the server to RunCloud
- In RunCloud, choose Connect a new server, then Manual Installation, and pick your provider.
- Choose the installation type, web server and database. These are fixed once the server is built, so decide before you go further.
- Enter a server name and the static IP address.
- Copy the script RunCloud generates, log in to the server as root over SSH, paste it in and let it run. It takes a few minutes.
- When it finishes, the terminal shows the database root password and the
runcloudsystem user password. Save both in your password manager before you close the window.
Then do two things before adding any real sites.
Set a default web application. When a request arrives by IP address, or for a domain the server does not recognise, RunCloud serves the default web app. Without a neutral one, a mistyped or leftover DNS record can end up showing somebody else’s website. Create an empty web app named default, open it, go to Tools and choose Set as Default Web App. Each server has one.
Set the command line PHP version. Match it to the version your sites run, so WP-CLI and scheduled scripts behave the same way the website does. Open the server’s Settings page, choose the version under PHP CLI version, then confirm it over SSH.
php -v
Point DNS at the server
Give the server its own name on a domain you control, then add these four records at your DNS host. We keep ours on a separate utility domain so client DNS is never involved.
web1.example.comValue203.0.113.10A record (wildcard) Sends every name beneath it to the server.Name*.web1.example.comValue203.0.113.10MX record Gives the hostname a valid mail route.Nameweb1.example.comValue10 web1.example.comTXT record (SPF) Lets this server send mail as its own hostname.Nameweb1.example.comValuev=spf1 a mx ip4:203.0.113.10 ~allHow wildcard DNS records work
The asterisk stands in for any name. With *.web1.example.com in place, newsite.web1.example.com and client-staging.web1.example.com both resolve to the server straight away, with no DNS change and no waiting. That makes it ideal for staging and pre-launch sites.
- It does not cover the bare name.
web1.example.comstill needs its own A record, which is why the table has both. - Specific records win. If
shop.web1.example.comhas its own record, the wildcard is ignored for that name. - DNS only gets the request to the server. You still add the hostname to the right web app in RunCloud and issue an SSL certificate for it.
- Staging sites are public. Anyone who guesses the name can load it, so keep them out of search engines and behind a login where it matters.
One caution on SPF: mail sent from a client’s domain is checked against that domain’s SPF record, not this one. The record above only covers mail sent as the server’s own hostname.
Install the basics
sudo apt update
sudo apt install htop sshpass
htop gives a readable live view of CPU, memory and processes. sshpass lets a migration script open password-based SSH connections without prompting. Only install it if you need it, and prefer SSH keys wherever the other end allows them.
Keep PHP errors off the screen
A PHP warning printed on a live page looks bad and can reveal file paths. RunCloud installs several PHP versions side by side, each with its own php.ini, so check them all rather than only the one you are using today.
grep -H "^display_errors" /etc/php*rc/php.ini
Every version should report the setting as off. On our RunCloud servers it has been off out of the box, so this is a check rather than a fix. Errors still go to the log files, which is where you want them.
Add monitoring
RunCloud can notify you about load, memory and disk from the server’s own dashboard, so switch those on. We also install an external agent so alerts do not depend on the panel. We use StatusCake and alert at 80% on CPU, memory and disk. One quirk to save you a search: the first run of its install command can stop with a “no crontab” error for the agent’s user. Running the same command a second time completes the install.
Allowlist the new IP address
A new server means a new IP address, and anything locked to the old one will fail quietly. Work through every service that restricts access by IP:
- API keys restricted by IP, such as server-side mapping or geocoding keys.
- Backup storage policies. If your offsite bucket only accepts known addresses, add this one in CIDR form:
203.0.113.10/32. The/32means exactly one address. - Anything else with an allowlist, including payment gateways, SMTP relays and remote databases.
Set the hostname, reverse DNS and mail identity
Receiving mail servers compare three things: the name your server announces, the DNS record for that name, and the reverse DNS record for its IP address. When all three agree, mail is far more likely to reach the inbox.
- Set the hostname and confirm it.
sudo hostnamectl set-hostname web1.example.com hostnamectl - Stop it resetting on reboot. Cloud images rewrite the hostname at startup unless told otherwise. Open
/etc/cloud/cloud.cfgand changepreserve_hostnamefromfalsetotrue.# This will cause the set+update hostname module to not operate (if true) preserve_hostname: true - Set reverse DNS. The full hostname must already have an A record pointing at this IP address. On Vultr, open the server, go to Settings, then IPv4, and edit the Reverse DNS field on the existing line. DigitalOcean has no field to edit. It builds the reverse record from the Droplet’s name, so rename the Droplet to the full hostname.
- Check Postfix is installed. A fresh RunCloud server may not have a mail server, and PHP cannot send mail without one. If this command is not found, install Postfix and choose Internet Site when asked, with the full hostname as the system mail name.
postconf mail_version sudo apt install postfix - Tell Postfix its name. Edit
/etc/postfix/main.cfso both of these lines match the hostname, then reload.myhostname = web1.example.com smtp_helo_name = web1.example.comsudo postfix reload
Check the result from any machine. This should return your hostname:
dig +short -x 203.0.113.10
Send a test email
Do not assume mail works. Send a message to the address mail-tester.com gives you and read the report. It checks SPF, reverse DNS, the HELO name and blocklists in one pass.
On a WordPress site, a plugin such as WP Test Email will send the message. Without WordPress, save this as mail.php in a web app’s public folder, load it once in a browser, then delete it.
<?php
$to = 'you@example.com';
$subject = 'Server mail test';
$message = 'Hello from the new server.';
$headers = 'From: webmaster@example.com' . "\r\n" .
'Reply-To: webmaster@example.com' . "\r\n" .
'X-Mailer: PHP/' . phpversion();
mail($to, $subject, $message, $headers);
echo 'Test email sent.';
If nothing arrives, check that sendmail_path in /etc/php*rc/php.ini points at /usr/sbin/sendmail.
Do you need an SMTP service as well? You can route each site’s mail through a dedicated SMTP provider with a plugin. We find that overkill for most sites. With the hostname, reverse DNS and SPF lined up as above, mail sent straight from the server works well, and there is one less plugin, account and set of credentials to maintain on every site.
Add health checks, and roll them out with Ansible
Three small scripts catch most of the problems that monitoring dashboards miss. Each runs from cron every five minutes and stays silent unless something is wrong. Cron emails whatever a job prints, so no output means no email.
- Stolen CPU. “Steal” is processor time the hypervisor gave to another customer, shown in the
stcolumn ofvmstat. A dedicated CPU plan should show almost none. Sustained steal means a noisy neighbour, and it is your cue to ask the host to move you or to upgrade off a shared plan. - Mail queue size. Counts the messages waiting in
mailq. A growing Postfix queue points to a delivery problem or a compromised form sending spam. - PHP-FPM and memory errors. Searches the logs for sites hitting their PHP worker limit (
max_children) and for processes killed for running out of memory.
We used to paste these in by hand on each new server. Now an Ansible playbook does it. Ansible connects over SSH, so there is nothing to install on the server. One short playbook copies the scripts into place, adds the cron entries and sets the MAILTO address for alerts, with the thresholds kept as variables.
The benefit shows up later. Every server gets identical scripts, a new server is covered with one command, and when you change a threshold or fix a script you run the playbook again and it updates every server at once. Ansible only changes what differs, so running it twice is safe.
Sources and documentation
- https://runcloud.io/docs/connect-any-cloud-provider-on-premise-server
- https://runcloud.io/docs/how-to-set-a-default-web-application-on-runcloud
- https://runcloud.io/docs/php-cli
- https://runcloud.io/docs/cheat-sheet-php
- https://docs.vultr.com/
- https://docs.digitalocean.com/products/networking/dns/how-to/manage-records/
- https://datatracker.ietf.org/doc/html/rfc4592
- https://cloudinit.readthedocs.io/en/latest/reference/modules.html
- https://www.postfix.org/postconf.5.html
- https://docs.ansible.com/ansible/latest/collections/ansible/builtin/cron_module.html