Installing WordPress on a VPS usually becomes a serious idea once the site has outgrown shared hosting: slow page loads at peak hours, a 503 error in the middle of a sales campaign, and resource limits whose edge nobody can show you. The fix is a server whose CPU, RAM and disk are all yours. This guide takes both paths step by step: a clean install from scratch on a LEMP stack, and a complete move of your existing site with no downtime, plus the default limits that wreck most migrations within the first hour.
Why does WordPress breathe easier on a VPS?
On shared hosting, your site shares the table with dozens of others: if a neighbor comes under attack or runs a heavy script, your site slows down. An ordinary VPS with Nginx and PHP-FPM gives you that performance back, as long as you set it up properly once:
- Dedicated resources: the RAM and CPU work for your site alone, and you can tune the object cache (Redis) and the number of PHP-FPM processes precisely for your own load.
- Full control: the PHP version, the Nginx modules, the firewall: it is all in your hands, not in a support ticket to your host.
- Scalability: running a big sale? Raise the server's resources for a while, then bring them back down.
Honesty requires one more point, though: moving to a VPS also means taking on the role of "server administrator". Operating system updates, SSL renewal, backups and closing ports are now nobody's job but yours.
What does a WordPress server need today?
Before the first command, know what WordPress asks for; these numbers have changed, and copying an old tutorial is exactly where people trip up. According to the official requirements page, the recommended baseline today has three parts: PHP 8.3 or higher, MariaDB 10.11 or higher or MySQL 8.0 or higher, and HTTPS on every install. Treat HTTPS as mandatory, not as a nice-to-have.
That directly affects which distribution you choose. The official Ubuntu 24.04 repositories carry exactly PHP 8.3 and MariaDB 10.11, so plain apt, with no third-party repository, meets both conditions. Ubuntu 22.04, however, still ships PHP 8.1 and MariaDB 10.6, both below today's baseline; a tutorial that installs php8.1-fpm is out of date.
The second prerequisite is the stack itself: Nginx installed and running, a domain whose A record points to the server's IP, and an SSL certificate. We cover all of that in our guide to setting up a website with Nginx on Ubuntu; do that first, then come back here.
Path one: installing WordPress on a VPS from scratch
The database and the PHP extensions
Install the PHP extensions in one go: gd and imagick for images, mbstring for multibyte text such as Persian or Arabic, zip for installing plugins and themes, and curl for talking to api.wordpress.org:
apt install mariadb-server php8.3-fpm php8.3-mysql php8.3-curl \
php8.3-gd php8.3-mbstring php8.3-xml php8.3-zip php8.3-imagick -y
mysql_secure_installation
Next, create a dedicated database and user. On Ubuntu, the database's root user authenticates through the Unix socket, so you log in with sudo and no password:
sudo mysql
CREATE DATABASE wpdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'YOUR-STRONG-PASSWORD';
GRANT ALL PRIVILEGES ON wpdb.* TO 'wpuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Don't compromise on utf8mb4: the old utf8 encoding only has room for three bytes per character and breaks four-byte characters such as emoji.
Downloading WordPress and setting file ownership
cd /var/www/example.ir/html
wget https://wordpress.org/latest.tar.gz
tar -xzf latest.tar.gz --strip-components=1
rm latest.tar.gz
chown -R www-data:www-data /var/www/example.ir/html
This archive always points to the latest stable release. Set permissions according to the WordPress hardening guide: 755 for directories and 644 for files. For wp-config.php, the same guide suggests 400 or 440, because it is the only file that contains the database password.
The Nginx server block and finishing the installer
Nginx doesn't read .htaccess files, so the permalink rule has to go directly into the server block; the WordPress documentation gives this same try_files line:
server {
server_name example.ir www.example.ir;
root /var/www/example.ir/html;
index index.php;
client_max_body_size 64m;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location = /wp-config.php { deny all; }
}
nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
systemctl reload nginx
Now open the domain in your browser: WordPress's five-minute installer comes up, asks for the database details and writes wp-config.php for you. Installing WordPress on a VPS really takes no more than fifteen minutes; the hard part comes afterwards.
The wp-config.php settings you need on day one
The installer writes the bare minimum, not what a real site needs. According to the wp-config.php reference, a few constants are worth adding:
define( 'WP_MEMORY_LIMIT', '256M' );
define( 'DISALLOW_FILE_EDIT', true );
define( 'FS_METHOD', 'direct' );
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
Each one has a specific reason. WP_MEMORY_LIMIT: WordPress's default is only 40 MB for a single-site install (and 64 MB for multisite), while its higher ceiling for admin tasks is 256 MB; a white screen while installing a heavy plugin often comes from right here. DISALLOW_FILE_EDIT disables the theme and plugin editors; the WordPress documentation describes it as equivalent to removing the edit_themes, edit_plugins and edit_files capabilities from all users. FS_METHOD set to direct stops the "FTP credentials" prompt during updates. And the debug trio writes errors to wp-content/debug.log instead of showing them to visitors.
There is one more job: the eight security keys (salts) that WordPress uses to sign session cookies. WordPress.org runs an official random generator for them:
curl https://api.wordpress.org/secret-key/1.1/salt/
define('AUTH_KEY', 'q7>Vb...');
define('SECURE_AUTH_KEY', 'H2!dR...');
define('LOGGED_IN_KEY', 'x9%Lm...');
define('NONCE_KEY', 'Tf4@z...');
Replace the eight matching lines with the eight lines of output. Changing these keys invalidates every open session, so after any security scare, regenerating them is the first thing to do.
Path two: migrating WordPress to a VPS
Moving WordPress to a new server has three parts: the files, the database and the configuration. Keep to this order and you can migrate without a single minute of downtime.
Step 1: Move the files
If your old host offers SSH access, rsync is the fastest and safest route; run it from the new server:
rsync -avz --progress user@old-host.com:public_html/ /var/www/example.ir/html/
sending incremental file list
wp-content/uploads/2026/05/hero.webp
wp-content/plugins/woocommerce/woocommerce.php
sent 41.02M bytes received 118.71K bytes 1.63M bytes/sec
The strength of rsync is that you can repeat it: bring over all the files today, then run it again at switch-over time so only the differences are transferred. If your old host only gives you a graphical control panel, create a ZIP archive in its File Manager and send it with scp (see our guide to connecting over SSH):
scp backup.zip root@203.0.113.10:/var/www/example.ir/html/
ssh root@203.0.113.10
cd /var/www/example.ir/html && unzip backup.zip && rm backup.zip
Step 2: Move the database
On the old host, export an SQL file from phpMyAdmin, or, if you have SSH:
mysqldump --single-transaction --default-character-set=utf8mb4 \
-u olduser -p old_database > db-backup.sql
The two extra flags are there for a reason. --single-transaction takes the dump from a consistent snapshot without locking tables, so the old site keeps running. --default-character-set=utf8mb4 keeps characters from turning into question marks. Transfer the file and import it:
mysql -u wpuser -p wpdb < db-backup.sql
Step 3: Align wp-config.php
In the new server's wp-config.php, make these four values match the new database:
define( 'DB_NAME', 'wpdb' );
define( 'DB_USER', 'wpuser' );
define( 'DB_PASSWORD', 'YOUR-STRONG-PASSWORD' );
define( 'DB_HOST', 'localhost' );
Step 4: Test before changing DNS
You can see the site on the new server without touching DNS: add one line to your own computer's hosts file (/etc/hosts on Linux and macOS, C:\Windows\System32\drivers\etc\hosts on Windows):
203.0.113.10 example.ir www.example.ir
Now your browser loads the site from the new server, while the rest of the world still sees the old host. Test the home page, a few posts, the dashboard, search, the shopping cart and an image upload, then remove that line. Almost every error that would give you a scare after the DNS switch can be caught right here.
Step 5: Switch DNS with a low TTL
At least 24 hours before the migration, lower the A record's TTL to 300 seconds, so that when you switch, the change spreads within minutes rather than hours. Then change the IP in the A record, and once everything has settled, put the TTL back. If TTLs are new to you, read our guide to DNS records; if you are changing the domain's nameservers at the same time, the order for creating records before you switch nameservers is in our guide to connecting a domain to a VPS.
Common post-migration traps and how to fix them
1) Every page except the home page returns 404
On shared hosting, the .htaccess file handled permalinks, and Nginx doesn't read it. Your location / block must contain the same try_files line shown above; then run nginx -t && systemctl reload nginx.
2) Errors uploading images or installing plugins
Files you transferred as root belong to root, but PHP runs as the www-data user. Fix ownership and permissions in one go:
chown -R www-data:www-data /var/www/example.ir/html
find /var/www/example.ir/html -type d -exec chmod 755 {} \;
find /var/www/example.ir/html -type f -exec chmod 644 {} \;
chmod 440 /var/www/example.ir/html/wp-config.php
3) Mixed content after enabling SSL
If the site used to run on http, the old addresses stored in the database break the browser's padlock. Do a safe replacement with WP-CLI; its documentation says it handles PHP serialized data intelligently and doesn't touch tables' primary keys, unlike a raw SQL replace:
sudo -u www-data wp search-replace 'http://example.ir' 'https://example.ir' \
--skip-columns=guid --report-changed-only --dry-run
Success: 412 replacements to be made.
sudo -u www-data wp search-replace 'http://example.ir' 'https://example.ir' \
--skip-columns=guid
Two points. First, --skip-columns=guid: WordPress's migration documentation says of this column that "it is critical that you do NOT change the contents of this field", because feed readers use this value to tell which posts they have already shown. Second, WP-CLI deliberately refuses to run as root and prints "Error: YIKES! It looks like you're running this as root."; the right fix is sudo -u www-data, not a way around the warning.
4) Media uploads fail with a 413 error or "file exceeds the maximum upload size"
This trap has two layers, and people usually fix only one of them. The first layer is Nginx: according to the Nginx core module documentation, the default client_max_body_size is only 1m, and a larger request gets an HTTP 413 error; the same documentation warns that browsers cannot display this error correctly. The second layer is PHP: upload_max_filesize defaults to 2M and post_max_size to 8M.
5) The database import stops halfway
With large tables, an import sometimes stops with a dropped connection. The cause is almost always max_allowed_packet: a packet larger than this limit closes the connection. The MySQL documentation describes this variable as existing on both the client and the server side (16 MB on the client, 64 MB on the server) and says both have to be raised:
mysql --max_allowed_packet=256M -u wpuser -p wpdb < db-backup.sql
The table below links each symptom to its cause and the next step:
| Symptom | Likely cause | Next step |
|---|---|---|
| Media upload fails with HTTP 413 | client_max_body_size at the default 1m | Raise it to 64m in the server block and reload |
| "File exceeds the maximum upload size" message | upload_max_filesize and post_max_size are too low | Raise both; the second must always be larger than the first |
| A large menu saves only partly | max_input_vars is still at 1000 | Increase it and reload PHP-FPM |
| Only the home page opens, everything else is 404 | No permalink rule in Nginx | Add try_files to location / |
| Error establishing a database connection | Wrong DB values in wp-config.php | Log in manually with mysql -u wpuser -p wpdb |
| Broken padlock and missing images | http:// addresses left in the database | wp search-replace with --skip-columns=guid |
| The dashboard opens but uploads are rejected | The files are owned by root | chown -R www-data:www-data on the site root |
| Non-Latin text has turned into question marks | The SQL dump wasn't taken as utf8mb4 | Take it again with --default-character-set=utf8mb4 |
| A change to a PHP file doesn't show up | Old bytecode is still in OPcache | systemctl reload php8.3-fpm |
| The site opens for you but not for others | The A record still points to the old IP | Check with dig +short example.ir A |
PHP-FPM and OPcache: where a VPS really pulls ahead
On shared hosting these two layers are out of your hands; on a VPS they are yours. The most important PHP-FPM number is pm.max_children: the maximum number of PHP requests that run at the same time. Measure the average RAM per process and divide the RAM you can allocate by it:
ps -ylC php-fpm8.3 --no-headers | awk '{sum+=$8; n++} END {print sum/n/1024 " MB avg"}'
58.4 MB avg
If, on a server with 2 GB of RAM, you set aside about 1.2 GB for PHP and each process uses 60 MB, a pm.max_children of around 20 makes sense. A bigger number pushes the server into swap, and a smaller one leaves requests waiting in the queue. The message "server reached pm.max_children setting" in the log means the number is too low.
The second layer is OPcache, which keeps compiled bytecode in memory. Its defaults are reasonable, opcache.memory_consumption at 128 MB and opcache.max_accelerated_files at 10000, but WordPress with one theme and twenty plugins goes past ten thousand files, and the cache starts evicting them. PHP rounds this setting up to the next number in a fixed set of primes (7963, 16229 and 32531 are three members of that set), so writing 12000 effectively means 16229.
opcache.validate_timestamps is on and opcache.revalidate_freq is 2 seconds. But if you have turned validate_timestamps off for extra performance, visitors keep seeing the old version until the service is reloaded, while command-line PHP reads the fresh file and you assume the change has taken effect.Why doesn't WordPress email arrive from a VPS?
The cause is a simple misunderstanding: a bare VPS has no email service at all. On shared hosting, the wp_mail() function was wired to a local sending service that the host had installed; a VPS has nothing of the kind, so the contact form and "password reset" emails fail silently. And even if you install a sending service yourself, many networks block outbound port 25, and mail sent from a fresh IP has a good chance of landing in spam.
The right way is to send through the SMTP server of a real email service: install an SMTP plugin and give it your domain mailbox's details, usually port 587 with STARTTLS or port 465 with direct TLS. Your domain's MX record stays untouched, because MX is only about receiving mail. The last step, landing in the inbox instead of the spam folder, depends on the domain's TXT records; we explain the order for creating SPF, DKIM and DMARC in our guide to fixing domain email that goes to spam.
Backups: on a VPS, nobody takes them but you
This is the main difference between a VPS and shared hosting. A full backup has two pieces: the site folder (where wp-content/uploads is the largest part) and the database. With WP-CLI, both take just a few lines:
cd /var/www/example.ir/html
sudo -u www-data wp db export --default-character-set=utf8mb4
Success: Exported to 'wpdb-2026-09-08-3f9a1c2.sql'.
tar -czf /var/backups/wp-$(date +%F).tar.gz wp-content wp-config.php wpdb-*.sql
If you don't give it a file name, wp db export builds one from the pattern "database name + date + random hash", so consecutive backups don't overwrite each other. Three rules: keep backups off the server, schedule them, and rehearse a restore at least once; an untested backup is just a big file. Rotation schemes are covered in our Linux backup guide.
Post-migration checklist
Once WordPress is up on the new VPS, tick off these items one by one before you declare the job done:
- Open the home page, a few posts, the contact page and search;
- Test logging in to the dashboard, uploading media and saving a draft;
- Submit a form and make sure its email arrives through the SMTP route;
- Re-enable your caching plugin and clear the old cache;
- Check automatic SSL renewal with
certbot renew --dry-run; since version 4.0.0, Certbot renews when less than a third of the certificate's lifetime remains, which for a 90-day certificate means about 30 days before expiry; - Set up automatic backups and do a test restore;
- Lock down the server's basic security following our Linux server security guide: firewall, Fail2ban and disabling password logins;
- Watch
/var/log/nginx/error.logandwp-content/debug.logfor a few days, then turnWP_DEBUGoff.
WordPress on a Mehran Host cloud server
A cloud server here is a KVM machine with NVMe storage in a datacenter inside Iran, and its cost is deducted from your wallet by the hour. Two points about money: first, powering the server off doesn't stop billing. A powered-off server that still exists is charged a percentage of the hourly rate because its RAM and disk remain reserved, and the only thing that brings the cost to zero is deleting the server. Second, if your wallet balance reaches zero, active servers are suspended automatically: not when the balance goes negative, but the moment the credit runs out.
On the network side, two things make a migration shorter. DNS management in the same panel covers eight record types (A, AAAA, CNAME, MX, TXT, NS, SRV and CAA), and the lowest TTL you can choose is 60 seconds. And next to every A record there is a CDN switch: turn it on and the public answer changes to the nearest edge node, in Iran or in Europe, while the origin IP stays hidden, with 1 TB of free traffic for a domain whose DNS is managed there.
And if you conclude that your site doesn't need a VPS yet, that is a sound decision too: WordPress hosting on cPanel and DirectAdmin keeps the same site running without any of the work in this article.
Next step
The whole job comes down to three sentences: bring up a current stack (PHP 8.3 and MariaDB 10.11 or later), move the files and the database with the right tools, and test everything through the hosts file before you switch DNS. Create a cloud server and rehearse the migration with hourly billing.
Frequently asked questions
What do I need to install WordPress on a VPS?
You need PHP 8.3 or higher, MariaDB 10.11 or higher or MySQL 8.0 or higher, and HTTPS on every install, which is the baseline the official WordPress requirements page recommends today; the suggested web server is Apache or Nginx. The official Ubuntu 24.04 repositories meet both software requirements without any third-party repository, while Ubuntu 22.04 ships older versions that fall below this baseline.
How much downtime does moving WordPress to a new server cause?
Practically none, if you follow the right order. The trick is to bring the site fully up on the new server before you touch DNS, and to test it through one line in your own computer's hosts file; that way, only you see the new server. Also lower the A record's TTL to 300 seconds at least 24 hours in advance, so the final switch spreads within minutes.
Why does every page except the home page return 404 after moving WordPress?
Because the permalink rule didn't come along. On shared hosting, that rule lived in the .htaccess file, which Apache reads, but Nginx doesn't read such files. Put the line try_files $uri $uri/ /index.php?$args; in the location / section of your server block, then reload the service, and your post and page URLs will resolve again.
Do I need a control panel like cPanel to manage WordPress on a VPS?
No, you don't. WordPress has its own complete dashboard, and server-level tasks, such as creating the database, configuring Nginx, getting an SSL certificate and taking backups, take only a few command-line commands. A control panel is just a graphical layer over those same tasks, and it uses RAM and CPU itself. If you manage several client sites it may save you time; otherwise it is an extra dependency.
Why doesn't my WordPress contact form send email on a VPS?
Because a bare VPS has no mail-sending service, so the wp_mail() function fails silently. The fix is to install an SMTP plugin and give it the details of a real mailbox, usually port 587 with STARTTLS or port 465 with TLS. Don't change your MX record; MX only decides where incoming mail is delivered.