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
- 2.0 GIT
- 3.0 Setup LAMP/LNMP stack
- 3.1 MariaDB (MySQL)
- 3.2 PHP
- 3.4 Nginx (preferred)
- 3.4.1 Create the default site
- 3.4.2 Create basic username/password to lock the default site down with
- 3.4.3 Replace contents of
/etc/nginx/sites-available/default.conf
with the following: - 3.4.4 Enable the default site
- 3.4.5 (Optional) Ensure only files with
.php
extension are ever processed byphp-fpm
- 3.5 Apache
- 3.5.1 Fix the error Apache throws about not having a server name
- 3.5.2 Make sure user is part of the www-data group
- 3.5.3 Modify the default site conf
- 3.5.4 Make sure
index.php
is the first default file - 3.5.5 Add the index file to the default site
- 3.5.6 Make sure apache is aware of the changes
- 3.5.7 Now lock down that file with a .htpasswd
- 3.5.8 Enable a few things
- 3.6 Test installation
- 4. Other Useful Things
- 5. Handy Tips
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.
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
dpkg --add-architecture i386 && apt-get update
apt-get install libc6:i386 libncurses5:i386 libstdc++6:i386
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
set rebinddelete
/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
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 "[email protected]" -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
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
ufw allow 30194/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw show added
ufw enable
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 [email protected]
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
Install the latest version of git
add-apt-repository ppa:git-core/ppa
apt update
apt install git
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.*
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
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";
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 [email protected]
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 "[email protected]"
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
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
apt-get remove --purge '^php*' -yq
add-apt-repository ppa:ondrej/php
apt-get update -yq
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
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
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;
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
sh -c "echo -n 'root:' >> /etc/nginx/.htpasswd"
echo "uvd6>WE4{7VQHg.A8kDFM" | openssl passwd -apr1 -stdin >> /etc/nginx/.htpasswd
cat /etc/nginx/.htpasswd
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;
}
cd /etc/nginx/sites-enabled
ln -sf ../sites-available/default.conf .
nginx -t
service nginx restart
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
add-apt-repository ppa:ondrej/apache2
apt-get update
apt-get install apache2 ssl-cert libapache2-mod-php
Edit /etc/apache2/conf-available/servername.conf
and add (change myserver
to whatever you want)
ServerName "myserver"
Then enable it with
a2enconf servername.conf
usermod -a -G www-data akearney
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>
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>
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
service apache2 restart
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
a2enmod authz_groupfile ssl rewrite
service apache2 restart
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
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
certbot --apache
certbot --nginx
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
curl -sL https://deb.nodesource.com/setup_16.x -o nodesource_setup.sh
bash nodesource_setup.sh
apt-get install -y nodejs
node --version
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
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
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
cd "/usr/local/Saxonica/Saxon-HEC1.1.2/Saxon.C.API/"
/usr/bin/phpize
./configure --enable-saxon
make
make install
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"
Enable the module on the commandline
phpenmod -v 7.4 -s cli saxon
phpenmod -v 7.4 -s fpm saxon
service php7.4-fpm restart
service nginx restart
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
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
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
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
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;
rm -rf /etc/mysql /var/lib/mysql
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
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
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
https://itsfoss.com/how-to-remove-or-delete-ppas-quick-tip/
sudo add-apt-repository --remove ppa:PPA_Name/ppa
nano /etc/php/8.0/fpm/pool.d/www.conf
Then uncomment access.log
and access.format
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
killall -9 -u <USER> && deluser --remove-home -f <USER>
- 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;"