Skip to content

Instantly share code, notes, and snippets.

@pointybeard
Last active November 12, 2023 20:00
Show Gist options
  • Save pointybeard/bf33095f93ce3bcc8567849166d8bd11 to your computer and use it in GitHub Desktop.
Save pointybeard/bf33095f93ce3bcc8567849166d8bd11 to your computer and use it in GitHub Desktop.
Server Build Guide: Ubuntu (20.04LTS) LNMP/LAMP

Server Build Guide: Ubuntu (20.04LTS) LNMP/LAMP

This is a living document which reflects the current process, accumulated and tuned over many years, that I use to set up a Ubuntu 20.04LTS LNMP/LAMP stack from scratch. It might not suit everyone, and probably isn't without its flaws, but it's a good foundation for any new server setup, one that I use for dev and production servers alike.

If you would like a quick, no-fuss, local LNMP stack, check out my ubuntu_lnmp_docker_vagrant repository.

1. General Setup

1.1 Ensure everything is up to date

apt-get clean && \
apt-get update -yq && \
apt-get upgrade -yq

IMPORTANT: If asked, DO NOT overwrite /boot/grub/menu.lst. This is very important if server is a VPS. keep the installed version to avoid getting locked out.

1.2 Install important packages

apt-get install -yq build-essential \
    module-assistant \
    linux-headers-virtual \
    software-properties-common \
    apt-utils \
    locales \
    man-db \
    keychain \
    acpid \
    dkms \
    tree \
    zip \
    unzip \
    wget \
    ruby \
    curl \
    acl \
    iproute2

1.3 (Optional) Install 32bit libraries

dpkg --add-architecture i386 && apt-get update
apt-get install libc6:i386 libncurses5:i386 libstdc++6:i386

1.4 Set local, date, and time correctly

localedef -i en_AU -c -f UTF-8 -A /usr/share/locale/locale.alias en_AU.UTF-8
dpkg-reconfigure tzdata
timedatectl set-timezone Australia/Brisbane
timedatectl set-ntp no

apt-get install ntp
service ntp start

Check NTP is installed correctly:

timedatectl

1.5 Fix the nano backspace/delete key problem

set rebinddelete

1.6 Lock everything down

1.6.1 Create the "wheel" group

/usr/sbin/groupadd wheel

Next, give SUDOer access to anyone in the wheel group

/usr/sbin/visudo

Add the following lines

## Allows people in group wheel to run all commands
%wheel  ALL=(ALL)    ALL

1.6.2 Create a new super user and give them access

It is a bad idea to allow root to log in, so we're going to create a new super user who can.

/usr/sbin/adduser akearney
/usr/sbin/usermod -a -G wheel akearney

On local machine do the following

mkdir ~/.ssh && cd ~/.ssh
ssh-keygen -t rsa -C "hi@alannahkearney.com" -b 4096
cat ~/.ssh/id_rsa.pub

Copy the public key from above and then, back on the new server

mkdir ~akearney/.ssh
nano ~akearney/.ssh/authorized_keys

Paste the public key and save authorized_keys. Then set permissions correctly

chown -R akearney:akearney ~akearney/.ssh
chmod 700 ~akearney/.ssh
chmod 600 ~akearney/.ssh/authorized_keys

1.6.3 Update SSH config

Lets change the default port, allow access for a our new user, turn off root login and disallow login with plain password.

nano /etc/ssh/sshd_config

Add/modify the following:

Port 30194         ## <--- change to a port of your choosing
Protocol 2
PermitRootLogin no
PasswordAuthentication no
UseDNS no
AllowUsers akearney

1.6.4 Turn on the firewall

ufw allow 30194/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw show added
ufw enable

1.6.5 Reload SSHD

service ssh restart

Note: DO NOT log out yet. Instead, start a new terminal window and attempt to log in with the new user. This prevents you from being locked out of your server if something was mis-configured.

ssh -p 30194 akearney@123.45.67.890

1.7 Update .bashrc

Add the following to ~akearney/.bashrc then use source ~akearney/.bashrc to apply changes (once logged in as that user). Can add this to root user .bashrc, but recommend using different colour scheme (see below).

PS1='\[\033[0;35m\]\u@\h\[\033[0;33m\] \w\[\033[00m\]: '
##PS1='\[\033[01;36m\]\u@\h\[\033[0;33m\] \w\[\033[00m\]: ' ## Root user colour scheme

alias update="sudo apt update"
alias install="sudo apt install"
alias upgrade="sudo apt upgrade"
alias remove="sudo apt remove"
alias free="free -m"
alias myip="ip addr show eth0 | grep inet | awk '{ print $2; }' | sed 's/\/.*$//'"
alias ..="cd .."
alias ...="cd ../.."
alias h='cd ~'
alias c='clear'
alias ls='ls -lah'

keychain -q id_rsa
. ~/.keychain/`uname -n`-sh

Reload bash config with the following

source ~/.bashrc

2.0 GIT

Install the latest version of git

add-apt-repository ppa:git-core/ppa
apt update
apt install git

2.1 Install hub

Hub is a wrapper for Git which adds github specific commands (change hub-linux-amd64-2.14.2.tgz to suit distro):

wget https://github.com/github/hub/releases/download/v2.14.2/hub-linux-amd64-2.14.2.tgz && \
tar -zxf hub-linux-amd64-2.14.2.tgz && \
mv hub-linux-amd64-2.14.2/bin/hub /usr/bin/hub && \
chmod 755 /usr/bin/hub && \
rm -R hub-linux-amd64-2.*

2.2 Install gh

gh is GitHub on the command line. It brings pull requests, issues, and other GitHub concepts to the terminal next to where you are already working with git and your code.

https://github.com/cli/cli/blob/trunk/docs/install_linux.md

apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-key C99B11DEB97541F0
apt-key adv --keyserver keyserver.ubuntu.com --recv-key C99B11DEB97541F0
apt-add-repository https://cli.github.com/packages
apt-get update
apt-get install gh

2.3 Optional Setup

2.3.1 Add some Aliases

Open ~akearney/.bashrc and add

alias git="hub"
alias gs="git status -s";
alias gd="git diff";
alias gl="git log --oneline --decorate --all --graph";

2.3.2 Set up a new Identity

Based on article https://www.micah.soy/posts/setting-up-git-identities/

Clear out anything existing

git config --global --unset user.name
git config --global --unset user.email
git config --global --unset user.signingkey
git config --global user.useConfigOnly true

Generate a new key

gpg --full-gen-key

Choose (1) RSA and RSA (default) key type. Choose key size of 4096 bits. Set the key to not expire (0) unless you want to repeat this step periodically. Finally, set your name and email address. Comment can be left blank.

Output the pubilc key. This will output a sec ID in the format of rsa4096/[serial]:

gpg --list-secret-keys --keyid-format LONG hi@alannahkearney.com

Copy the serial number, then run this command to output the public key:

gpg --armor --export <serial>

Copy the public key block and add it to Github settings.

Now we need to create the identities in git’s global config:

git config --global user.github.name "Alannah Kearney"
git config --global user.github.email "hi@alannahkearney.com"
git config --global user.github.signingkey <key>

Create an alias for setting identity per-repo:

git config --global alias.identity '! git config user.name "$(git config user.$1.name)"; git config user.email "$(git config user.$1.email)"; git config user.signingkey "$(git config user.$1.signingkey)"; :'

For each project, specify the git identity to use:

git identity github

3.0 Setup LAMP/LNMP stack

3.1 MariaDB (MySQL)

This install uses mariadb-server instead of mysql-server (see, https://mariadb.com/kb/en/installing-mariadb-deb-files/).

NOTE: This only works on LTS releases of Ubuntu. Use the MariaDB repo configurator for other releases: https://mariadb.org/download/?t=repo-config

curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup -o mariadb_repo_setup
bash mariadb_repo_setup --mariadb-server-version=10.7
apt-get update -y
apt-get install -y mariadb-server mariadb-client
systemctl start mariadb.service && systemctl enable mariadb.service

Next, secure the installation. Suggest setting a root user password when prompted.

/usr/bin/mysql_secure_installation

Open nano /etc/mysql/mariadb.conf.d/50-server.cnf and comment out bind-address and skip-external-locking. Then change/add the following under mysqld:

wait_timeout = 600
max_allowed_packet = 64M

Connect to MySQL as root (mysql -u root -p) and run the following:

CREATE USER 'root'@'localhost' IDENTIFIED BY 'root';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION;
CREATE USER 'root'@'%' IDENTIFIED BY 'root';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;

Restart MySQL with service mysql restart

3.2 PHP

apt-get remove --purge '^php*' -yq
add-apt-repository ppa:ondrej/php
apt-get update -yq

8.1.x (preferred)

apt-get install -y php php-pear php-curl php-dev php-gd php-mbstring php-zip php-mysql php-xml php-json php-pcov php-xml php-intl

7.4.x

Install the PHP7.4 libraries

apt-get install -y php7.4 php-pear php7.4-curl php7.4-dev php7.4-gd php7.4-mbstring php7.4-zip php7.4-mysql php7.4-xml php7.4-json php7.4-pcov

This will most likely install both PHP 7.4 and 8.x (with 8 being the default). To switch to 7.4, update the symlink in /usr/bin like so:

cd /usr/bin
rm php.default
ln -s php7.4 php.default

Verify the correct version with php --version. Output should look like this:

PHP 7.4.22 (cli) (built: Jul 30 2021 13:08:17) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
    with Zend OPcache v7.4.22, Copyright (c), by Zend Technologies

3.4 Nginx (preferred)

add-apt-repository ppa:ondrej/nginx
apt-get update

Install some libraries. Note, this assumes PHP8.1. Change php-fpm to php7.4-fpm for 7.4.x

apt-get install python3-certbot-nginx nginx php-fpm ssl-cert

Add the following to /etc/nginx/conf.d/ports.conf (uncomment the 443 lines to enable https)

server {
    listen 80;
    listen [::]:80 ipv6only=on;
    #listen 443 ssl;
    #listen [::]:443 ipv6only=on ssl;
}

Open /etc/nginx/nginx.conf and change server_names_hash_bucket_size from 32 to 64. i.e.

server_names_hash_bucket_size 64;

3.4.1 Create the default site

rm -R /var/www/html && mkdir -p /var/www/default/{logs,public,private,cgi-bin,ssl}
echo "<?php phpinfo();" > /var/www/default/public/index.php
fix-www-permissions

3.4.2 Create basic username/password to lock the default site down with

sh -c "echo -n 'root:' >> /etc/nginx/.htpasswd"
echo "uvd6>WE4{7VQHg.A8kDFM" | openssl passwd -apr1 -stdin >> /etc/nginx/.htpasswd
cat /etc/nginx/.htpasswd

3.4.3 Replace contents of /etc/nginx/sites-available/default.conf with the following:

server {

    listen 80 default_server;
    listen [::]:80 default_server;
    #listen 443 default_server;
    #listen [::]:443 default_server;

    root /var/www/default/public;

    index index.php;

    server_name _;

    charset utf-8;
    autoindex off;

    access_log /var/www/default/logs/access.nginx.log;
    error_log /var/www/default/logs/error.nginx.log;

    # Make sure requests for favicon.ico and robots.txt don't end up in the logs
    location = /favicon.ico { log_not_found off; access_log off; }
    location = /robots.txt  { log_not_found off; access_log off; }

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        include     snippets/fastcgi-php.conf;
        include     fastcgi_params;
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
    }

    # lock it down with username and password
    auth_basic "Restricted Content";
    auth_basic_user_file /etc/nginx/.htpasswd;
}

3.4.4 Enable the default site

cd /etc/nginx/sites-enabled
ln -sf ../sites-available/default.conf .
nginx -t
service nginx restart

3.4.5 (Optional) Ensure only files with .php extension are ever processed by php-fpm

Uncomment `security.limit_extensions` in `/etc/php/8.0/fpm/pool.d/www.conf`

Finally, reload nginx

service php8.1-fpm restart
service nginx reload

3.5 Apache

add-apt-repository ppa:ondrej/apache2

apt-get update
apt-get install apache2 ssl-cert libapache2-mod-php

3.5.1 Fix the error Apache throws about not having a server name

Edit /etc/apache2/conf-available/servername.conf and add (change myserver to whatever you want)

ServerName "myserver"

Then enable it with

a2enconf servername.conf

3.5.2 Make sure user is part of the www-data group

 usermod -a -G www-data akearney

3.5.3 Modify the default site conf

Edit /etc/apache2/sites-enabled/000-default.conf and change to the following:

<VirtualHost *:80>

    ServerAdmin webmaster@localhost

    DocumentRoot /var/www/default/public

    <Directory />
            Options FollowSymLinks
            AllowOverride None
    </Directory>
    
    <Directory /var/www/default/public>
            Options Indexes FollowSymLinks MultiViews
            AllowOverride all
            Order allow,deny
            allow from all
            Require all granted
    </Directory>

    ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
    <Directory "/usr/lib/cgi-bin">
            AllowOverride None
            Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
            Order allow,deny
            Allow from all
    </Directory>

    ErrorLog /var/www/default/logs/error.log

    # Possible values include: debug, info, notice, warn, error, crit,
    # alert, emerg.
    LogLevel warn

    CustomLog /var/www/default/logs/access.log combined

    Alias /doc/ "/usr/share/doc/"
    <Directory "/usr/share/doc/">
        Options Indexes MultiViews FollowSymLinks
        AllowOverride None
        Order deny,allow
        Deny from all
        Allow from 127.0.0.0/255.0.0.0 ::1/128
    </Directory>

</VirtualHost>

3.5.4 Make sure index.php is the first default file

Edit /etc/apache2/mods-enabled/dir.conf and make sure contents is as follows (notice index.php is first):

<IfModule mod_dir.c>
    DirectoryIndex index.php index.html index.cgi index.pl index.xhtml index.htm
</IfModule>

3.5.5 Add the index file to the default site

rm -R /var/www/html && mkdir -p /var/www/default/{logs,public,private,cgi-bin,ssl}
echo "<?php phpinfo();" > /var/www/default/public/index.php
fix-www-permissions

3.5.6 Make sure apache is aware of the changes

service apache2 restart

3.5.7 Now lock down that file with a .htpasswd

Add the following to /var/www/default/public/.htaccess

AuthName "Restricted Area"
AuthType Basic
AuthUserFile /var/www/default/public/.htpasswd
AuthGroupFile /dev/null
require valid-user

Then add the following to /var/www/default/public/.htpasswd (password is uvd6>WE4{7VQHg.A8kDFM)

root:$apr1$2l2UT30t$FtTHrCLyimoeEtG4M/nIH1

3.5.8 Enable a few things

a2enmod authz_groupfile ssl rewrite
service apache2 restart

3.6 Test installation

Go to http://your.ip.add/ (remember can use myip command get get IP address). Have a look at the error log if something has gone wrong tail -f /var/www/default/logs/error.log

4. Other Useful Things

4.1 Certbot

Remove any existing stuff (just in case):

apt-get remove --purge "^certbot*"

For Ubuntu 20.04LTS Focal use snapd:

apt-get install snapd -yq
snap install --classic certbot
ln -s /snap/bin/certbot /usr/bin/certbot

For Ubuntu versions lower than 20.04LTS use the folllowing (see, https://certbot.eff.org/lets-encrypt/ubuntufocal-nginx):

add-apt-repository ppa:certbot/certbot
apt-get update
apt-get install software-properties-common python-certbot-apache python-certbot-nginx

Check that certbox will automatically try to renew certificates.

systemctl status certbot.timer

The command to renew certbot is installed in one of the following locations:

/etc/crontab/
/etc/cron.*/*
systemctl list-timers

If necessary, add the following to the crontab (crontab -e) to trigger a renew action on a regular basis

14 0 * * * certbot renew --quiet --no-self-upgrade
14 11 * * * certbot renew --quiet --no-self-upgrade

Run and enable certificates for selected hosts

4.1.1 Apache

certbot --apache

4.1.2 Nginx

certbot --nginx

4.2 Composer

curl -sS https://getcomposer.org/installer -o composer-setup.php
php composer-setup.php --install-dir=/usr/local/bin --filename=composer
ln -sf /usr/local/bin/composer /usr/bin/
rm composer-setup.php

4.3 NodeJS

curl -sL https://deb.nodesource.com/setup_16.x -o nodesource_setup.sh
bash nodesource_setup.sh
apt-get install -y nodejs
node --version

4.4 Saxon/C PHP Extension

Installs Saxon/C v1.1.2, which includes XSLT3 support, for PHP7.4.

Based on article https://serverpilot.io/docs/how-to-install-the-php-saxon-c-extension

apt-get -y install gcc g++ make autoconf libc6-dev pkg-config

Note that gcj-jdk was discontinued in 2017 and is no longer available in Ubuntu after verson 16.04. Instead, need to install OpenJDK (see, https://www.digitalocean.com/community/tutorials/how-to-install-java-with-apt-on-ubuntu-20-04).

apt-get -y install default-jre default-jdk

Check it has installed correctly

java -version
javac -version

4.4.1 Get and install the saxon-c binary

cd /usr/local
wget --no-check-certificate "http://www.saxonica.com/saxon-c/libsaxon-HEC-setup64-v1.1.2.zip"
unzip libsaxon-HEC-setup64-v1.1.2.zip
./libsaxon-HEC-setup64-v1.1.2 -batch

4.4.2 Set some config

ln -sf "/usr/local/Saxonica/Saxon-HEC1.1.2/libsaxonhec.so" /usr/lib/libsaxonhec.so
ln -sf "/usr/local/Saxonica/Saxon-HEC1.1.2/rt" /usr/lib/rt

bash -c "echo '# JetVM env path (required for Saxon)' > /etc/ld.so.conf.d/jetvm.conf"
bash -c "echo /usr/lib/rt/lib/amd64 >> /etc/ld.so.conf.d/jetvm.conf"
bash -c "echo /usr/lib/rt/lib/amd64/jetvm >> /etc/ld.so.conf.d/jetvm.conf"
ldconfig

4.4.3 Build the extension

cd "/usr/local/Saxonica/Saxon-HEC1.1.2/Saxon.C.API/"
/usr/bin/phpize
./configure --enable-saxon
make
make install

4.4.4 A bit more configuration

bash -c "echo env[LD_LIBRARY_PATH] = /usr/lib/rt/lib/amd64:/usr/lib/rt/lib/amd64/jetvm > /etc/php/7.4/apache2/conf.d/saxon_ld_library_path.conf"
bash -c "echo extension=saxon.so > /etc/php/7.4/mods-available/saxon.ini"

4.4.6 Enable the extension & verify

Enable the module on the commandline

phpenmod -v 7.4 -s cli saxon

4.4.6.1 Nginx

phpenmod -v 7.4 -s fpm saxon
service php7.4-fpm restart
service nginx restart

4.4.6.2 Apache

Add the following to the end of /etc/apache2/envvars

export LD_LIBRARY_PATH=/usr/lib/rt/lib/amd64:$LD_LIBRARY_PATH

Enable the module

phpenmod -v 7.4 -s apache2 saxon
service apache2 restart

4.4.7 Test it works via command line

php -i | grep Saxon && \
php -r '$proc = new Saxon\SaxonProcessor(); var_dump($proc); echo $proc->version() . PHP_EOL;'

Output should look something like this:

Saxon/C
Saxon/C => enabled
Saxon/C EXT version => 1.1.0
Saxon => 9.8.0.4
object(Saxon\SaxonProcessor)#1 (0) {
}
Saxon/C 1.1.2 running with Saxon-HE 9.8.0.15J from Saxonica

4.5 PECL/Pickle

Download the pickle.phar from https://github.com/friendsofphp/pickle:

wget https://github.com/FriendsOfPHP/pickle/releases/latest/download/pickle.phar

Move into /usr/local/bin:

mv pickle.phar /usr/local/bin/pickle
chmod +x /usr/local/bin/pickle

4.6 Install openssl and curl cacert.pem

Save the cURL ca certificate to /etc/ssl/private/

cd /etc/ssl/private/
wget https://curl.haxx.se/ca/cacert.pem
chmod 0600 cacert.pem
chown root:www-data cacert.pem

Edit php.ini (e.g. /etc/php/8.1/fpm/php.ini) and set curl.cainfo and openssl.cafile to /etc/ssl/private/cacert.pem

service php8.1-fpm restart

5. Handy Tips

5.1 MySQL

5.1.1 Add a new Database, User, and grant permissions

CREATE DATABASE `<database_name>` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci;
CREATE USER 'newuser'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON `<database_name>`.* TO 'username'@'localhost';
FLUSH PRIVILEGES;

5.1.2 Remove left over MySQL bits

rm -rf /etc/mysql /var/lib/mysql

5.1.3 Upgrading MariaDB version

Run the same basic commands as installation:

curl -LsS -O https://downloads.mariadb.com/MariaDB/mariadb_repo_setup
bash mariadb_repo_setup --mariadb-server-version=10.7
apt-get update
apt-get install mariadb-client mariadb-server

Upgrade all tables

mysql_upgrade -u root -p

5.2 General Server Admin

5.2.1 Create a script for setting permissions

adduser www-data akearney
mkdir ~akearney/bin
echo "chgrp -R www-data /var/www && chmod -R g+w /var/www && find /var/www -type d -exec chmod 2775 {} \; && find /var/www -type f -exec chmod ug+rw {} \;" > ~/bin/fix-www-permissions
chmod +x ~akearney/bin/fix-www-permissions
ln -s ~akearney/bin/fix-www-permissions /usr/local/sbin/fix-www-permissions
fix-www-permissions

5.2.2 To remove packages generally

apt-get remove --purge '^package_name*'
apt autoremove && apt autoclean
apt-get -yq update && apt-get -yq upgrade

If there are errors, try using

dpkg --configure -a

5.2.3 Remove a PPA

https://itsfoss.com/how-to-remove-or-delete-ppas-quick-tip/

sudo add-apt-repository --remove ppa:PPA_Name/ppa

5.2.4 Enable php-fpm access log

nano /etc/php/8.0/fpm/pool.d/www.conf

Then uncomment access.log and access.format

5.2.5 Kill all processes that are using port 443 and 80

This is helpful if getting errors like bind() to 0.0.0.0:443 failed (98: Address already in use) when trying to start nginx (tail /var/log/nginx/error.log)

fuser -k 443/tcp
fuser -k 80/tcp
service nginx restart

If this still doesn't work, check to see if Apache is running in the background

service apache2 stop
apt remove apache2

5.2.6 Remove a user and their home directory

killall -9 -u <USER> && deluser --remove-home -f <USER>

5.2.7 Change MySQL datadir location

  • Stop the mysql service with sudo service mysql stop
  • Make the new directory with mkdir /vagrant/mysql && chown -R mysql:mysql /vagrant/mysql
  • Copy contents of existing directory with cp -R -p /var/lib/mysql/* /vagrant/mysql
  • Open /etc/mysql/mariadb.conf.d/50-server.cnf and change "datadir" to /vagrant/mysql
  • Restart MySQL service with sudo service mysql restart
  • Verify the change with mysql -u root -p -e "SELECT @@datadir;"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment