Skip to main content

Self-Hosting

This page is not for general users, this page used to keep track what softwares and services that I (DOM Cloud Developer) employ so that I don't forget the steps needed when I create a new server from stratch.

You can also use this page as a reference if you want to know what's running behind the scenes.

OS and Installation

Very basic stuff

  1. Install Rocky Linux latest or Ubuntu LTS, do yum update/apt update.
  2. Set PermitRootLogin yes (or prohibit-password) and PasswordAuthentication yes in /etc/ssh/sshd_config
  3. Install Virtualmin: sh virtualmin-install.sh --minimal --bundle LEMP
  4. Install Passenger
  5. Enable quotas for /
  6. Set port=2443 in /etc/webmin/miniserv.conf
  7. Disable all email related services, uninstall imap and dovecot if possible.
  8. Clone and set up DOM Cloud Bridge with user bridge
/etc/sudoers.d/bridge
bridge ALL = (root) NOPASSWD: /home/bridge/public_html/sudoutil.js
bridge ALL = (root) NOPASSWD: /bin/systemctl restart bridge
/lib/systemd/system/bridge.service
[Unit]
Description=DOM Cloud Bridge
Documentation=https://domcloud.co
After=network.target

[Service]
Type=simple
User=bridge
WorkingDirectory=/home/bridge/public_html
ExecStart=/usr/bin/node /home/bridge/public_html/app.js
Restart=always

[Install]
WantedBy=multi-user.target
echo "passenger-config restart-app ~" > /usr/local/bin/restart && chmod 755 /usr/local/bin/restart
mkdir /etc/skel/public_html
pushd /etc/skel/public_html
mkdir .well-known
touch favicon.ico
wget https://gist.githubusercontent.com/willnode/0efb7e2d70a2a8005c690d887d0cdb8a/raw/499b82245fd4d430d77f4c1febd4419d869d1109/index.html
popd

Package installs (RHEL)

PHP:

info

Uninstall all PHP from appstream first. (yum remove php-*)

dnf install epel-release http://rpms.remirepo.net/enterprise/remi-release-9.rpm 
dnf install php{74,80,81,82,83}-php-{bcmath,cli,common,devel,fpm,gd,imap,intl,mbstring,mysqlnd,opcache,pdo,pecl-mongodb,pecl-redis,pecl-zip,pgsql,process,sodium,soap,xml}
ln -s `which php83` /usr/local/bin/php
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
find /etc/opt/remi/ -maxdepth 1 -name 'php*' -exec sed -i "s/upload_max_filesize = 2M/upload_max_filesize = 512M/g" {}/php.ini \; -exec sed -i "s/post_max_size = 8M/post_max_size = 512M/g" {}/php.ini \;
find /etc/opt/remi/ -type f -name www.conf -print0 | xargs -0 sed -i 's/pm = dynamic/pm = ondemand/g'

Tools, libs, services:

dnf install awscli btop certbot clang cmake gcc-c++ git ncdu htop iftop ipset jq lsof make nano neovim rsync sendmail strace tar time vim wget xz \
{libcurl,libffi,libsqlite3x,libtool-ltdl,nettle,openldap,postgresql-server,passenger,perl,readline,xmlsec1,xmlsec1-openssl}-devel \
libmd libreport-filesystem mesa-libGL perl-{DBD-Pg,macros,Crypt-SSLeay,Text-ASCIITable} podman podman-docker slirp4netns \
earlyoom fail2ban-server iptables-services postfix postgresql-server postgresql-contrib
wget https://rdfind.pauldreik.se/rdfind-1.6.0.tar.gz
tar -xf rdfind-1.6.0.tar.gz ; cd rdfind-1.6.0
./configure --disable-debug ; make install
cd .. ; rm -rf rdfind-1.6.0
pip3 install pipenv
wget https://github.com/sayanarijit/xplr/releases/latest/download/xplr-linux.tar.gz
tar -xzvf xplr-linux.tar.gz
mv xplr /usr/local/lib/xplr
info

Enable PostgreSQL service from webmin after installing it

Package installs (Debian)

apt-get remove firewalld php{-cli,*cgi,-pear,-xml}
apt-get install software-properties-common
add-apt-repository ppa:ondrej/php; apt-get update
apt-get install php{7.4,8.0,8.1,8.2}-{bcmath,cli,common,dev,fpm,gd,imap,igbinary,intl,mbstring,mysql,opcache,mongodb,readline,redis,zip,pgsql,soap,xml}
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
find /etc/php/ -maxdepth 1 -mindepth 1 -exec sed -i "s/upload_max_filesize = 2M/upload_max_filesize = 512M/g" {}/fpm/php.ini \; -exec sed -i "s/post_max_size = 8M/post_max_size = 512M/g" {}/fpm/php.ini \;

Tools, libs, services:

apt-get install certbot cmake git ncdu htop iftop ipset jq lsof make nano rsync sendmail strace tar time vim wget xz-utils
apt-get install {libcurl4-openssl,libbz2,libffi,libgl,libsqlite3,nettle,libmd,libpq,passenger,libperl,libssl}-dev {libdbd-pg,libtemplate}-perl
apt-get install earlyoom rdfind fail2ban postfix postgresql{,-contrib} bind9{,-host} mariadb-{common,server} webmin-virtualmin-nginx{,-ssl}
apt-get autoremove

Configuration Files

System Files

/etc/fstab
UUID=<uuid>       /       xfs     relatime,seclabel,uquota,inode64,gquota,attr2,rw             0       0
UUID=<uuid> /home xfs relatime,seclabel,uquota,inode64,gquota,attr2,rw,nofail 0 0
proc /proc proc defaults,nosuid,nodev,noexec,relatime,hidepid=2,gid=adm 0 0
/swapfile swap swap defaults,nofail 0 0
/etc/security/limits.conf
root             soft    nofile          65535
# note: linux hardlimit is 524288 (ulimit -Hn)
/etc/environment
LC_ALL="en_US.UTF-8"
LC_CTYPE="en_US.UTF-8"
LANGUAGE="en_US.UTF-8"
/etc/gemrc
gem: --no-document
/etc/gitconfig
[pull]
rebase = true
[init]
defaultBranch = main
/etc/resolv.conf
nameserver 127.0.0.1
nameserver 1.1.1.1
nameserver 1.0.0.1

Services

systemctl edit nginx
[Service]
LimitNOFILE=65535
systemctl edit earlyoom
[Service]
SupplementaryGroups=adm
systemctl edit iptables
[Service]
ExecStartPre=ipset -! create whitelist hash:ip
systemctl edit ip6tables
[Service]
ExecStartPre=ipset -! create whitelist-v6 hash:ip family inet6
/etc/default/earlyoom
EARLYOOM_ARGS="-r 0 -m 4 -M 409600 -g --prefer '^(node|python|ruby|java)' --avoid '^(dnf|mariadbd|postmaster|named|nginx|polkitd|do-agent|sshd|php-fpm)$'"
/var/spool/cron/root
0,5,10,15,20,25,30,35,40,45,50,55 * * * * /etc/webmin/status/monitor.pl
0 * * * * find '/var/spool/cron/' -not -name root -type f | xargs sed -i '/^\s*(\*|\d+,)/d'
* * * * * pgrep PassengerAgent || systemctl restart nginx
*/5 * * * * /usr/bin/node /home/bridge/public_html/sudokill.js -i bridge,do-agent,dbus,earlyoom,mysql,named,nobody,postgres,polkitd,rpc
@daily passenger-config reopen-logs
@weekly /usr/bin/node /home/bridge/public_html/sudocleanssl.js
@weekly find /var/spool/clientmqueue /var/webmin/diffs -mindepth 1 -delete
@weekly find /home -maxdepth 1 -type d -ctime +7 -exec rm -rf {}/{.cache,.npm,Downloads,public_html/.yarn/cache,public_html/node_modules/.cache,.composer/cache} \;
@monthly find /home -maxdepth 1 -type d -ctime +7 -exec rdfind -minsize 100000 -makehardlinks true -makeresultsfile false {}/{.vscode-server,.pyenv,.rvm,.cargo,.local,go,.rustup,public_html/node_modules} \;
@monthly find /etc/letsencrypt/{csr,keys} -name *.pem -type f -mtime +180 -exec rm -f {} ';'
@reboot sleep 60 && sudo /usr/bin/bash /home/bridge/public_html/src/whitelist/refresh.sh

MariaDB

/etc/my.cnf.d/mariadb-server.cnf
[mysqld]
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
log-error=/var/log/mariadb/mariadb.log
pid-file=/run/mariadb/mariadb.pid
innodb_file_per_table = 1
innodb_buffer_pool_size = 64M
myisam_sort_buffer_size = 8M
read_rnd_buffer_size = 512K
net_buffer_length = 8K
read_buffer_size = 256K
sort_buffer_size = 512K
table_open_cache = 64
max_allowed_packet = 64M
key_buffer_size = 16M

PostgreSQL

/var/lib/pgsql/data/postgresql.conf
...
listen_addresses = '*'
max_connections = 1000
...
/var/lib/pgsql/data/pg_hba.conf
local   all             all                                     trust
host all all 127.0.0.1/32 trust
host all all ::1/128 trust
local replication all trust
host replication all 127.0.0.1/32 trust
host replication all ::1/128 trust
host all all 0.0.0.0/0 md5
host all all ::/0 md5

SSL & Folder securities

mkdir -p /etc/nginx/conf.d && chmod 0750 $_
mkdir -p /etc/ssl/virtualmin && chmod 0750 $_

Set SSL key path template to /etc/ssl/virtualmin/${ID}/ssl.key and all other as same key directory, so...

grep -r /etc/ssl/virtualmin /etc/webmin
/etc/webmin/virtual-server/config:key_tmpl=/etc/ssl/virtualmin/${ID}/ssl.key
/etc/webmin/virtual-server/templates/1:cert_key_tmpl=/etc/ssl/virtualmin/${ID}/ssl.key

NGINX

/etc/webmin/virtualmin-nginx/config
php_socket=1
nginx_cmd=/usr/sbin/nginx
add_to=/etc/nginx/conf.d/
http2=1
listen_mode=1
child_procs=4
extra_dirs=
rotate_cmd=nginx -s reload
apply_cmd=systemctl reload nginx
stop_cmd=systemctl stop nginx
start_cmd=systemctl start nginx
add_link=
nginx_config=/etc/nginx/nginx.conf
proxy_websockets=0
/etc/nginx/fastcgi.conf
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param HTTPS $https;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_read_timeout 600s;
/etc/nginx/passenger.conf
passenger_root /usr/share/ruby/vendor_ruby/phusion_passenger/locations.ini;
passenger_ruby /usr/bin/ruby;
passenger_python /usr/bin/python3;
passenger_nodejs /usr/bin/node;
passenger_friendly_error_pages on;
passenger_disable_security_update_check on;
passenger_disable_anonymous_telemetry on;
passenger_instance_registry_dir /var/run/passenger-instreg;
passenger_log_file /var/log/nginx/passenger.log;
passenger_min_instances 0;
passenger_max_pool_size 10;
passenger_max_instances_per_app 1;
/etc/nginx/finetuning.conf
server_names_hash_bucket_size 64;
server_names_hash_max_size 131072;
limit_req_zone $binary_remote_addr zone=basic_limit:50m rate=2r/s;
limit_req zone=basic_limit burst=500 nodelay;
gzip_types text/css application/javascript image/svg+xml;
gzip_min_length 1024;
gzip_comp_level 3;
gzip on;
sendfile on;
tcp_nopush on;
keepalive_timeout 60;
keepalive_requests 1000;
directio 16m;
output_buffers 3 512k;
client_max_body_size 512m;
disable_symlinks if_not_owner;
proxy_http_version 1.1;
ssl_protocols TLSv1.2 TLSv1.3;
server_tokens off;
merge_slashes off;
msie_padding off;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 1h;
ssl_session_tickets off;
ssl_early_data on;
ssl_buffer_size 4k;
/etc/nginx/nginx.conf
user nginx;
worker_processes auto;
worker_rlimit_nofile 65535;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 10240;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
include /etc/nginx/mime.types;
include /etc/nginx/finetuning.conf;
include /etc/nginx/fastcgi.conf;
include /etc/nginx/passenger.conf;
default_type application/octet-stream;
map $sent_http_content_type $expires {
default off;
text/html epoch;
text/css max;
application/javascript max;
~image/ max;
~font/ max;
~audio/ max;
~video/ max;
}
expires $expires;
server {
server_name _;
listen <IPv4>;
listen [<IPv6>];
return 301 https://$host$request_uri;
}
include /etc/nginx/conf.d/*.conf;
}
/etc/logrotate.d/nginx
/var/log/nginx/*.log /var/log/virtualmin/*_log {
create 0640 nginx root
daily
rotate 10
missingok
notifempty
compress
delaycompress
sharedscripts
postrotate
/bin/kill -USR1 `cat /run/nginx.pid 2>/dev/null` 2>/dev/null || true
endscript
}

Fail2Ban

/etc/fail2ban/filter.d/wordpress-auth.conf
[Definition]
failregex = ^<HOST> .* "POST /+xmlrpc.php .*" [24]\d\d \d* ".*"$
^<HOST> .* "POST /wp-login.php .*" [24]\d\d \d* ".*"$

ignoreregex =
/etc/fail2ban/jail.local
[DEFAULT]
maxretry = 10
bantime = 86400 ; 24h

[sshd]
enabled = true
port = ssh

[webmin-auth]
port = 2443
enabled = true

[phpmyadmin-syslog]
enabled = true

[mysqld-auth]
enabled = true

[wordpress-auth]
enabled = true
port = http,https
logpath = /var/log/virtualmin/*access_log

Iptables

/etc/sysconfig/iptables
*filter
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
-A INPUT -p icmp -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -p tcp -m multiport --dports 22,80,443,3306,5432 -j ACCEPT
-A INPUT -p tcp -m tcp --sport 53 -j ACCEPT
-A INPUT -p udp -m udp --sport 53 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 53 -j ACCEPT
-A INPUT -p udp -m udp --dport 53 -j ACCEPT
-A INPUT -p tcp --dport 2443:2453 -j ACCEPT
-A INPUT -p tcp --dport 32768:65535 -j ACCEPT
-A INPUT -j REJECT --reject-with icmp-host-prohibited
-A FORWARD -j REJECT --reject-with icmp-host-prohibited
-A OUTPUT -o lo -j ACCEPT
-A OUTPUT -m conntrack --ctstate ESTABLISHED -j ACCEPT
-A OUTPUT -p tcp --dport 22 -j ACCEPT
-A OUTPUT -p tcp -m tcp --dport 53 -j ACCEPT
-A OUTPUT -p udp -m udp --dport 53 -j ACCEPT
-A OUTPUT -p tcp -m tcp --sport 53 -j ACCEPT
-A OUTPUT -p udp -m udp --sport 53 -j ACCEPT
-A OUTPUT -p tcp -m tcp --dport 25 -j REJECT
-A OUTPUT -p tcp -m tcp --sport 25 -j REJECT
-A OUTPUT -m set -j ACCEPT --match-set whitelist dst
COMMIT
/etc/sysconfig/ip6tables
*filter
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
-A INPUT -p ipv6-icmp -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -p tcp -m multiport --dports 22,80,443,3306,5432 -j ACCEPT
-A INPUT -p tcp -m tcp --sport 53 -j ACCEPT
-A INPUT -p udp -m udp --sport 53 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 53 -j ACCEPT
-A INPUT -p udp -m udp --dport 53 -j ACCEPT
-A INPUT -p tcp --dport 2443:2453 -j ACCEPT
-A INPUT -p tcp --dport 32768:65535 -j ACCEPT
-A INPUT -j REJECT --reject-with icmp6-adm-prohibited
-A FORWARD -j REJECT --reject-with icmp6-adm-prohibited
-A OUTPUT -o lo -j ACCEPT
-A OUTPUT -m conntrack --ctstate ESTABLISHED -j ACCEPT
-A OUTPUT -p tcp --dport 22 -j ACCEPT
-A OUTPUT -p tcp -m tcp --dport 53 -j ACCEPT
-A OUTPUT -p udp -m udp --dport 53 -j ACCEPT
-A OUTPUT -p tcp -m tcp --sport 53 -j ACCEPT
-A OUTPUT -p udp -m udp --sport 53 -j ACCEPT
-A OUTPUT -p tcp -m tcp --dport 25 -j REJECT
-A OUTPUT -p tcp -m tcp --sport 25 -j REJECT
-A OUTPUT -m set -j ACCEPT --match-set whitelist-v6 dst
COMMIT