WordPress on Ubuntu 20 with Tor (nginx, MariaDB, PHP, Redis)

Last Revised: October 2, 2021

This tutorial has been created on a Clouding.io VPS. You can create your own VPS from 3€/month.

In addition, you have the possibility to create your VPS with the WordPress image in one click.


Versions to install

Operating System: Ubuntu 20
Control Panel: None
Web server: nginx
Database: MariaDB 10.5
Processor: PHP 8.0
Cache: Redis

Remember that if you are going to mount the system, both locally and externally, you must leave port 9050 available (and if you want external control, 9051) so that the necessary sockets of the Tor network can be opened. In addition, port 80 for web connections.

Configuring the Operating System

Once the operating system is installed, the first thing we will configure will be the server time. In this case we will configure the universal time zone UTC.

timedatectl set-timezone 'UTC'
timedatectl set-ntp on

The next thing we will do is check the version of the operating system and, subsequently, make a complete update of it.

lsb_release -a
apt -y update && apt -y upgrade && apt -y dist-upgrade && apt -y autoremove

Once everything is updated, we install some tools and base software that can be useful to have on the system.

apt -y install software-properties-common curl vim zip unzip apt-transport-https

Installing MariaDB

The next step will be the installation of the database. In this case we are going to use MariaDB 10.5. The first thing we will do is configure the download, and then its installation.

curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | sudo bash -s -- --mariadb-server-version="mariadb-10.5"
apt -y update && apt -y upgrade && apt -y dist-upgrade && apt -y autoremove
apt -y install mariadb-server mariadb-client

Now that it is installed, we will proceed to the initial configuration. For this we will use the secure installation system, which will ask us some questions.


To the question of whether we want to change the password, depending on whether or not we have put in the installation, we will change it. In case you have not put any, it is highly recommended to put a strong password.

Set root password? [Y/n]: Y

To the rest of the questions, we will answer the following:

Remove anonymous users? [Y/n]: Y
Disallow root login remotely? [Y/n]: Y
Remove test database and access to it? [Y/n]: Y
Reload privilege tables now? [Y/n]: Y

At this time we will have the database configured. Now we will make it run on the system restarts and start it.

systemctl stop mysql.service
systemctl enable mysql.service
systemctl start mysql.service
systemctl status mysql.service

Installing nginx

From here we have the database configured and we will proceed to the installation of the web server. In this case we are going to use nginx. To be up to date, we will not use the version that comes with the operating system, but a more updated and maintained one.

add-apt-repository ppa:ondrej/nginx
apt -y update && apt -y upgrade && apt -y dist-upgrade && apt -y autoremove
apt -y install nginx nginx-extras

Now that we have nginx installed, we are going to configure it to start at the system restarts automatically.

systemctl stop nginx.service
systemctl enable nginx.service
systemctl start nginx.service
systemctl status nginx.service

Installing PHP

At this time we already have the web server, so we are going to install and configure PHP to work properly with the database and the web server. In this case we are going to install the PHP 8.0 version. First we will do the installation of the most updated packages (which are not those that come with the operating system) and that in case of needing it, in addition, they would allow us to have several versions of PHP in parallel.

add-apt-repository ppa:ondrej/php
apt -y update && apt -y upgrade && apt -y dist-upgrade && apt -y autoremove
apt -y install php8.0 php8.0-fpm php8.0-common php8.0-dev php8.0-cli php8.0-bcmath php8.0-curl php8.0-gd php8.0-imap php8.0-mbstring php8.0-mysql php8.0-opcache php8.0-soap php8.0-xml php8.0-zip php8.0-xdebug libgeoip-dev php-pear pkg-config imagemagick libmagickwand-dev php8.0-imagick

Now that we have PHP correctly installed, let’s activate it so that when the system restarts it runs automatically.

systemctl stop php8.0-fpm.service
systemctl enable php8.0-fpm.service
systemctl start php8.0-fpm.service
systemctl status php8.0-fpm.service
php -v

We will also take the opportunity to update the PECL.

pecl channel-update pecl.php.net

Installing Redis

To work with improvements in the performance of the object cache, we are going to leave Redis ready as a storage system.

apt -y update && apt -y upgrade && apt -y dist-upgrade && apt -y autoremove
apt -y install redis-server php8.0-redis

Later, and in the same way as the rest of the elements, we are going to configure it to start automatically if the server is restarted.

systemctl stop redis-server.service
systemctl enable redis-server.service
systemctl start redis-server.service
systemctl status redis-server.service

Cleaning the default sites

By default, the system responds to the IP address and other elements. To prevent the default sites from being displayed, we will delete the default pages.

We delete all files by default.

rm /var/www/html/index.*

And we will create the default page.

vim /var/www/html/index.html

Where we will add the content of the default page.

<!DOCTYPE html>
<p>Hello World!</p>

And we will create a robot file.txt so that these pages are not indexed by default.

vim /var/www/html/robots.txt

Where we will add the content of the file that prevents indexing.

User-Agent: *
Disallow: /

Installing WP-CLI

For the creation of our WordPress sites, we will use the installation using WP-CLI.

cd /root/
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
php wp-cli.phar --info
chown www-data: wp-cli.phar
chmod +x wp-cli.phar
mv wp-cli.phar /usr/local/bin/wp

And we validate that it is installed and updated.

wp --info
wp cli update

Configuring the Tor network

From Ubuntu it is very simple to configure the Tor network since we only have to install it (it does not require any extra configuration).

apt -y install tor
apt -y update && apt -y upgrade && apt -y dist-upgrade && apt -y autoremove

Once the installation has been done, we can replace the default Tor configuration file with the one indicated below. Keep in mind that by default Tor comes “without configuration”, so mainly it is to activate and adapt what does not come by default.

SocksPort 9050
RunAsDaemon 1
DataDirectory /var/lib/tor

HiddenServiceDir /var/lib/tor/site_1/
HiddenServicePort 80

#HiddenServiceDir /var/lib/tor/site_2/
#HiddenServicePort 80

Now that we have set everything up, we will create the folders where the data will be stored and give you the corresponding permissions.

mkdir /var/lib/tor/site_1/
chmod 700 /var/lib/tor/site_1/
chown -R debian-tor:debian-tor /var/lib/tor/site_1/

We already have the Tor configuration, so we are going to restart the service to validate

service tor@default stop
service tor@default start
service tor@default status

With this we will see that Tor is already active and working on the server. Now it remains to configure the web so that it is visible to the users of the network.

Before continuing, we can already know the domain name (.onion) that has been assigned to us.

cat /var/lib/tor/site_1/hostname

This will return a domain/site of the style to:


Configuring services

Configuring nginx

We will make backup copies of the configuration files of sites to later put ours.

cd /etc/nginx/sites-enabled/
rm default
cd /etc/nginx/sites-available/
rm default

We will configure the global configuration of nginx.

cd /etc/nginx/
vim nginx.conf

With the following content:

user www-data;
pid /run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 65535;
include /etc/nginx/modules-enabled/*.conf;
events {
  multi_accept on;
  worker_connections 65535;
  use epoll;
http {
  charset utf-8;
  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  server_tokens off;
  more_clear_headers Server;
  log_not_found off;
  types_hash_max_size 2048;
  client_max_body_size 64m;
  keepalive_timeout 10;
  server_names_hash_bucket_size 128;
  server_names_hash_max_size 1024;
  include /etc/nginx/mime.types;
  default_type application/octet-stream;
  # logging
  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log;
  # TLS
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_prefer_server_ciphers on;
  # gzip
  gzip on;
  gzip_vary on;
  gzip_proxied any;
  gzip_comp_level 9;
  gzip_disable "msie6";
  gzip_buffers 16 8k;
  gzip_min_length 1100;
  gzip_types application/atom+xml application/javascript application/json application/x-javascript application/xml application/xml+rss image/svg+xml text/css text/javascript text/plain text/xml;
  # more
  include /etc/nginx/conf.d/*.conf;
  include /etc/nginx/sites-enabled/*;

And we configured the PHP 8 options for WordPress.

cd /etc/nginx/
vim wordpress_fastcgi_8_0.conf

With the following content:

fastcgi_pass unix:/var/run/php/php8.0-fpm.sock;
fastcgi_index index.php;
fastcgi_buffers 256 16k;
fastcgi_buffer_size 128k;
fastcgi_busy_buffers_size 256k;
fastcgi_temp_file_write_size 256k;
fastcgi_hide_header X-Powered-By;
fastcgi_hide_header X-Pingback;
fastcgi_hide_header Link;
fastcgi_intercept_errors off;
fastcgi_split_path_info ^(.+.php)(/.+)$;
try_files $fastcgi_script_name =404;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PHP_ADMIN_VALUE open_basedir=$document_root/:/usr/lib/php/:/tmp/;
fastcgi_param PATH_INFO $path_info;
set $path_info $fastcgi_path_info;
include fastcgi.conf;

We tested and restarted nginx.

nginx -t
nginx -s reload

Configuring PHP 8

We back up php settings to create our own.

cd /etc/php/8.0/fpm/
cp php.ini php.ini.original
vim /etc/php/8.0/fpm/php.ini

With the following content:

engine = On
short_open_tag = Off
precision = 14
output_buffering = 4096
zlib.output_compression = Off
implicit_flush = Off
unserialize_callback_func =
serialize_precision = -1
disable_functions = pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
disable_classes =
zend.enable_gc = On
zend.exception_ignore_args = On
zend.exception_string_param_max_len = 0
expose_php = Off
max_execution_time = 60
max_input_time = 60
memory_limit = 256M
error_reporting = E_ALL
display_errors = Off
display_startup_errors = Off
log_errors = On
log_errors_max_len = 1024
ignore_repeated_errors = Off
ignore_repeated_source = Off
report_memleaks = On
html_errors = On
variables_order = "GPCS"
request_order = "GP"
register_argc_argv = Off
auto_globals_jit = On
post_max_size = 32M
auto_prepend_file =
auto_append_file =
default_mimetype = "text/html"
default_charset = "UTF-8"
doc_root =
user_dir =
enable_dl = Off
file_uploads = On
upload_max_filesize = 32M
max_file_uploads = 20
allow_url_fopen = On
allow_url_include = Off
default_socket_timeout = 60
[CLI Server]
cli_server.color = On
date.timezone = 'UTC'
pdo_mysql.cache_size = 2000
[mail function]
SMTP = localhost
smtp_port = 25
mail.add_x_header = Off
odbc.allow_persistent = On
odbc.check_persistent = On
odbc.max_persistent = -1
odbc.max_links = -1
odbc.defaultlrl = 4096
odbc.defaultbinmode = 1
mysqli.max_persistent = -1
mysqli.allow_persistent = On
mysqli.max_links = -1
mysqli.cache_size = 2000
mysqli.default_port = 3306
mysqli.default_socket =
mysqli.default_host =
mysqli.default_user =
mysqli.default_pw =
mysqli.reconnect = Off
mysqlnd.collect_statistics = On
mysqlnd.collect_memory_statistics = Off
bcmath.scale = 0
session.save_handler = files
session.use_strict_mode = 0
session.use_cookies = 1
session.use_only_cookies = 1
session.name = PHPSESSID
session.auto_start = 0
session.cookie_lifetime = 0
session.cookie_path = /
session.cookie_domain =
session.cookie_httponly =
session.serialize_handler = php
session.gc_probability = 0
session.gc_divisor = 1000
session.gc_maxlifetime = 1440
session.referer_check =
session.cache_limiter = nocache
session.cache_expire = 180
session.use_trans_sid = 0
session.sid_length = 26
session.trans_sid_tags = "a=href,area=href,frame=src,form="
session.sid_bits_per_character = 5
zend.assertions = -1
tidy.clean_output = Off
soap.wsdl_cache_limit = 5
ldap.max_links = -1

And we restart PHP.

systemctl restart php8.0-fpm.service
systemctl status php8.0-fpm.service

Configuring Redis

We make a copy of the configuration files.

cd /etc/redis/
cp redis.conf redis.conf.original
vim /etc/redis/redis.conf

With the following content:

# include /path/to/local.conf
# include /path/to/other.conf
# loadmodule /path/to/my_module.so
# loadmodule /path/to/other_module.so
# bind
# bind ::1
bind ::1
protected-mode yes
port 6379
tcp-backlog 511
# unixsocket /var/run/redis/redis-server.sock
# unixsocketperm 700
timeout 0
tcp-keepalive 300
daemonize yes
supervised no
pidfile /var/run/redis/redis-server.pid
loglevel notice
logfile /var/log/redis/redis-server.log
# syslog-enabled no
# syslog-ident redis
# syslog-facility local0
databases 4
always-show-logo yes
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /var/lib/redis
# masterauth 
slave-serve-stale-data yes
slave-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
# repl-ping-slave-period 10
# repl-timeout 60
repl-disable-tcp-nodelay no
# repl-backlog-size 1mb
# repl-backlog-ttl 3600
slave-priority 100
# min-slaves-to-write 3
# min-slaves-max-lag 10
# min-slaves-max-lag is set to 10.
# slave-announce-ip
# slave-announce-port 1234
# requirepass foobared
# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52
# rename-command CONFIG ""
# maxclients 10000
maxmemory 256mb
# maxmemory-policy noeviction
# maxmemory-samples 5
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
slave-lazy-flush no
appendonly no
appendfilename "appendonly.aof"
# appendfsync always
appendfsync everysec
# appendfsync no
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble no
lua-time-limit 5000
# cluster-enabled yes
# cluster-config-file nodes-6379.conf
# cluster-node-timeout 15000
# cluster-slave-validity-factor 10
# cluster-migration-barrier 1
# cluster-require-full-coverage yes
# cluster-slave-no-failover no
# cluster-announce-ip
# cluster-announce-port 6379
# cluster-announce-bus-port 6380
slowlog-log-slower-than 10000
slowlog-max-len 128
latency-monitor-threshold 0
#  notify-keyspace-events Elg
#  notify-keyspace-events Ex
notify-keyspace-events ""
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
stream-node-max-bytes 4096
stream-node-max-entries 10
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
# client-query-buffer-limit 1gb
# proto-max-bulk-len 512mb
hz 10
aof-rewrite-incremental-fsync yes
rdb-save-incremental-fsync yes
# lfu-log-factor 10
# lfu-decay-time 1
# activedefrag yes
# active-defrag-ignore-bytes 100mb
# active-defrag-threshold-lower 10
# active-defrag-threshold-upper 100
# active-defrag-cycle-min 25
# active-defrag-cycle-max 75

We restart Redis and PHP to load your configuration.

systemctl restart redis-server.service
systemctl status redis-server.service
systemctl restart php8.0-fpm.service
systemctl status php8.0-fpm.service

Creating a website

For these examples we will use the root folder “/webs/” and the domain “example.com“.

We create the folder where we host our websites.

mkdir /webs/
cd /webs/

And here we will create as many sites as we want. In this case our example site.

mkdir /webs/example.onion/

Site configuration in nginx

We access and create the configuration file.

cd /etc/nginx/sites-available/
vim example.onion.conf

With the following content:

server {
  server_tokens off;
  root /webs/example.onion;
  index index.php index.html;
  location = /favicon.ico {
    log_not_found off;
    access_log off;
  location = /robots.txt {
    allow all;
    log_not_found off;
    access_log off;
  location ~ /.well-known {
    allow all;
  location ~ /.ht {
    deny all;
  location / {
    try_files $uri $uri/ /index.php;
  location ~ .php$ {
    include wordpress_fastcgi_8_0.conf;

We create the symbolic link and restart nginx to activate this setting.

ln -s /etc/nginx/sites-available/example.onion.conf /etc/nginx/sites-enabled/
nginx -t
nginx -s reload

We restart the nginx to load the new configuration.

nginx -t
nginx -s reload

Database Configuration

We will create the database for the website we are going to use. First we access the database.

mysql -u root -p

And we will create the database, the username and password and activate the privileges.

CREATE DATABASE example CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci;
GRANT ALL ON example.* TO 'example'@'localhost' IDENTIFIED BY 'la_contraseña_aqui';

Creation of the website

Now we have all the necessary elements for the creation of our site.

cd /webs/example.onion/
wp core download --allow-root
wp config create --dbprefix=wpprefix_ --locale=en_US --dbname=example --dbuser=example --dbpass='la_contraseña_aqui' --dbcharset=utf8mb4 --dbcollate=utf8mb4_general_ci --allow-root

And we will do the installation of the site:

wp core install --url="example.onion" --title="Mi WordPress" --admin_user="AdminDeWordPress" --admin_password="PasswordDeWordPress" --admin_email="example@example.com" --allow-root

And we will update the files and their permissions.

cd /webs/example.onion/
chown -R www-data:www-data ./
find /webs/example.onion/ -type d -exec chmod 750 {} ;
find /webs/example.onion/ -type f -exec chmod 640 {} ;

Keep in mind that in this case WordPress will work behind a proxy, so in the configuration fichewro we should make some changes.

define('WP_HOME', 'http://example.onion';
define('WP_SITEURL', 'http://example.onion');
define('WP_HTTP_BLOCK_EXTERNAL', true);
define('WP_ACCESSIBLE_HOSTS', '*.wordpress.org');
define('WP_PROXY_HOST', '');
define('WP_PROXY_PORT', '9050');

With this we can connect to the Tor network and launch our new website…

Securing the .onion

remember that Tor servers may disappear, or if you have to migrate a site between servers, you will need to store the configuration keys.

In these cases, remember to copy and save the contents of the folder of each of the sites you have on the server.


About this document

This document is regulated by the EUPL v1.2 license, published in WP SysAdmin and created by Javier Casares. Please, if you use this content in your website, your presentation or any material you distribute, remember to mention this site or its author, and having to put the material you create under EUPL license.