Setup Ubuntu Server (2020)

Bash

If at all possible, use latest Ubuntu version that is on a LTS release, and setup SSH key access, instead of a master password.

Once created & can login, open ~/.bashrc and put in the following:

alias upgrade='sudo apt-get update && sudo apt-get upgrade && sudo apt-get dist-upgrade && sudo apt-get autoremove && sudo apt-get autoclean'
COMPOSER_PATH=~/.composer/vendor/bin

Then source it with source ~/.bashrc and run apt-get update. Now we can run upgrade.

Note: When updating the kernel, a message may prompt that is pink/purple in the background, and gray with about seven options and the below text. Make sure keep the local version currently installed is chosen as the option, which is selected by default.

A new version of /boot/grub/menu.lst is available, but the version installed currently has been locally modified.

Sync Time

dpkg-reconfigure tzdata

Apt Packages

Next, we'll install the PHP packages we need:

apt-get install php7.2 php7.2-bcmath php7.2-bz2 php7.2-cli php7.2-common php7.2-curl php-curl curl php7.2-dev php7.2-fpm php7.2-gd php7.2-intl php7.2-json php7.2-mbstring php7.2-mysql php7.2-sqlite3 php7.2-xml php7.2-zip php-pear php7.2-cgi php-mbstring php-cli php-intl

For PHP 7.4, if your server has it, then use this:

apt-get install php7.4 php7.4-bcmath php7.4-bz2 php7.4-cli php7.4-common php7.4-curl php-curl curl php7.4-dev php7.4-fpm php7.4-gd php7.4-intl php7.4-json php7.4-mbstring php7.4-mysql php7.4-sqlite3 php7.4-xml php7.4-zip php-pear php7.4-cgi php-mbstring php-cli php-intl

Now we setup the basic services we'll need for a running laravel application:

  • Beanstalkd (queue)

  • Redis (cache, queue)

  • MySQL (client/server for DB)

  • Supervisor (running queue/keeping it up)

  • Nginx (http server)

  • Lets Encrypt (ssl certificates)

  • ImageMagick

  • Misc Server Stuff (such as git, zip

apt-get install beanstalkd redis-server mysql-server mysql-client supervisor nginx certbot letsencrypt python-certbot-nginx php-imagick imagemagick zip unzip git php-redis

PHP Setup

PHP FPM should be running already, but let's double check:

service php7.2-fpm status

We'll first update the FPM php.ini file, located at - /etc/php/7.2/fpm/php.ini

memory_limit = 512M
upload_max_filesize = 256M
post_max_size = 256M

Let's do the same changes for the CLI configuration, except skip memory limit. File is at - /etc/php/7.2/cli/php.ini

After that's done, restart FPM with service php7.2-fpm restart and double check that it's running fine with service php7.2-fpm status

Composer Setup

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

After setup, test that composer will run under root, and non-root. From that point on, it is much better to NOT run composer as root.

Next, to speed up composer going forward, with global, and non-global packages, install this:

composer global require hirak/prestissimo

Nginx Setup

Default vHost

Nginx should be running already, but let's double check:

service nginx status

Let's open the default vhost in /etc/nginx/sites-available/default and replace it with the following:

server {
listen 80 default_server;
listen [::]:80 ipv6only=on default_server;
root /var/www/vhosts;
index index.html index.php;
server_name _;
location / {
try_files $uri $uri/ =404;
}
}

Run nginx -t to make sure nothing got messed up, and set the root/sock path to match what you setup. By default it's /var/www/html , and it depends for the sock file. To find out for sure, and to do further configuration (FPM) before setting up a staging vhost, open the following file:

/etc/php/7.2/fpm/pool.d/www.conf

Look for listen = and you should see it being referenced now. Take that path and put it in the default vhost file.

Staging vHost

For this we will need to utilize Letsencrypt, enabling IPv6 on our machines, HTTP2, being able too see a hello world example, and running on PHP. First off, here's the vHost file with SSL being commented out. This will also redirect HTTP traffic to HTTPS.

server {
listen 80;
server_name sub.domain.com;
rewrite ^ https://sub.domain.com$request_uri? permanent;
}
server {
listen 443 ssl http2;
server_name sub.domain.com;
set $root_path '/var/www/vhosts/sub.domain.com/public';
root $root_path;
ssl_certificate /etc/letsencrypt/live/sub.domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/sub.domain.com/privkey.pem;
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
index index.php;
location / {
try_files $uri $uri/ /index.php?$args;
}
location @rewrite {
rewrite ^/(.*)$ /index.php?_url=/$1;
}
location ~ \.php$ {
try_files $uri /index.php =404;
fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}

The above can go in /etc/nginx/sites-available/sub.domain.com.vhost , and create a symbolic link in sites-enabled, by running:

cd /etc/nginx/sites-enabled/ && ln -s ../sites-available/sub.domain.com.vhost

Let's Encrypt Setup

With Let's Encrypt installed, and the Nginx plugin for it, and vHost setup, let's create a certificate! When running these commands, be sure to choose the Nginx option specifically for it. Nginx should have a non-ssl vhost to read, just comment out the SSH lines before running this (and restart nginx).

letsencrypt certonly --agree-tos -m [email protected] -d sub.domain.com

If you need multiple domains, keep passing in -d param. Here's an example with multiple sub-domains, and doing a dry run with the flag:

letsencrypt certonly --agree-tos -m [email protected] -d sub.domain.com -d www.domain.com -d domain.com --dry-run

If everything checks out okay, then you can remove the comments by the SSL keys info, above for Nginx.

MySQL Setup

MySQL should be running already, but let's double check:

service mysql status
mysql_secure_installation (recommended)

After installation you can secure a password & some recommended security improvement by running mysql_secure_installation via SSH. Skip the first one if you want, but create a new password, disable remote login, anonymous users, test databases, and definitely reload privileges.

Non-Root User Setup

So root is not so depended on, why don't we create a non-root user that can be used when logging into MySQL, for the Laravel database type, and in general is a great idea.

Remember that root password that was set previously? We'll need it below. After logging in, copy/paste the commands after the first one, one by one.

mysql -u root -p
CREATE USER 'usernamehere'@'localhost' IDENTIFIED BY 'passwordhere';
GRANT ALL PRIVILEGES ON * . * TO 'usernamehere'@'localhost';
FLUSH PRIVILEGES;

Redis

service redis-server status
service redis-server restart
redis-cli
nano /etc/redis/redis.conf
(uncomment this line in the ^ file) bind 127.0.0.1 ::1

Require Password

To improve security, we should have a password set in the aforementioned .conf file, and then we can easily set it in the laravel app. Due to the speediness of redis, we need a long, unrememberable, password. Run the following:

(look for this line & add `requirepass yourpwd` after) # requirepass foobared
(to generate LONG pwd) openssl rand 60 | openssl base64 -A
service redis-server restart

Beanstalkd

service beanstalkd status
service beanstalkd restart

Supervisor Setup

Create the file /etc/supervisor/conf.d/siteorenvname.conf and put the following in, with personalization:

[program:staging]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/vhosts/staging/artisan queue:work --tries=3
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/vhosts/staging/storage/logs/worker.log

After saving the file, run these commands:

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start staging:*

Laravel Necessities

composer require pda/pheanstalk
composer require aws/aws-sdk-php
composer require predis/predis
composer require league/flysystem-aws-s3-v3
composer require league/flysystem-cached-adapter
php artisan storage:link (if local filesystem driver)

Non-Root User Setup

sudo adduser usernameishere
sudo usermod -aG groupnamehere usernameishere (add existing user to existing group)
sudo rsync --archive --chown=usernameishere:usernameishere ~/.ssh /home/usernameishere
sudo groupadd devs
sudo usermod -g devs usernameishere (add new user to existing group)
sudo groups usernameishere (list groups for user)
sudo mkdir /var/www/vhosts (if needed)
(as root) cd ~/ && ln -s /var/www/vhosts Sites
(NOT as root) cd ~/ && ln -s /var/www/vhosts Sites
sudo chown -R www-data:devs /var/www/vhosts
sudo chown www-data:devs /usr/local/bin/composer
sudo chmod -vR g+w /var/www/vhosts
sudo usermod -a -G devs www-data

Laravel Permissions Setup

After creating project so files/folders are populated on disk, a git pull of a laravel project, whatever it is, below is what you want to run after composer. Right before development starts, make sure the permissions are right, locally, or on staging/production.

Please click on "laravel_permissions.sh" on the bottom left to see full instructions, and any feedback from the community.

File Storage: Digital Ocean

Instead of using a local driver for storage, or Amazon S3, let's use the same stack of Digital Ocean, but this time use their S3 alternative, which uses the same exact driver.

Open up config/filesystems.php and add in a new disk:

'digitalocean' => [
'driver' => 's3',
'key' => env('DO_SPACES_KEY'),
'secret' => env('DO_SPACES_SECRET'),
'endpoint' => env('DO_SPACES_ENDPOINT'),
'region' => env('DO_SPACES_REGION'),
'bucket' => env('DO_SPACES_BUCKET'),
],

Create an API key, and Spaces bucket before proceeding any further. At the time of writing this, Spaces costs $5/month for unlimited buckets, besides further bandwidth costs. With a domain connected, it would be recommended to enable the CDN, and keep the other settings as is.

After updating the config file, lets set the values in our .env file:

# Digital Ocean (file storage)
DO_SPACES_KEY="YOUR-API-KEY-HERE"
DO_SPACES_SECRET="YOUR-API-SECRET-HERE"
DO_SPACES_ENDPOINT="nyc3.digitaloceanspaces.com"
DO_SPACES_REGION="nyc3"
DO_SPACES_BUCKET="thenameofyourspacehere"
FILESYSTEM_DRIVER="digitalocean"

nyc3 is the name of the region for your space, which you can find by going to Dashboard -> Spaces -> Look at name of space in the list. Region names will look like sfo2, nyc3, and so on. As for FILESYSTEM_DRIVER, this line makes Digital Ocean Spaces the default for any file storage usage, which is a good thing!

With a New York example above, here's another example with San Fransisco:

https://thenameofyourspacehere.sfo2.digitaloceanspaces.com

Run php artisan config:clear, clear cache, and restart the queue just in case.

Example Laravel .env File

With the above packages being installed, and configured, below is a sample .env file that can be loaded in. Adjust to your vHost location on your local/remote box, username changes - that sort of thing. In terms of drivers, testing that these things work - as of January 18th, 2020 - these all work with the newest Digital Ocean Ubuntu Server version.

Away we go:

apt-get install gcc g++ make nodejs && node -v && npm -v

Install NPM/Yarn with Correct Permissions

First, install NodeJS. After it completes running, two separate version numbers should popup. An example is below the install line:

apt-get install gcc g++ make nodejs && node -v && npm -v
(EXAMPLE RESULTS WHEN RUNNING CLI LINE ABOVE)
Reading package lists... Done
Building dependency tree
Reading state information... Done
make is already the newest version (4.1-9.1ubuntu1).
make set to manually installed.
g++ is already the newest version (4:7.4.0-1ubuntu2.3).
g++ set to manually installed.
gcc is already the newest version (4:7.4.0-1ubuntu2.3).
gcc set to manually installed.
The following NEW packages will be installed:
nodejs
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
Need to get 24.0 MB of archives.
After this operation, 115 MB of additional disk space will be used.
Do you want to continue? [Y/n] Y
Get:1 https://deb.nodesource.com/node_13.x bionic/main amd64 nodejs amd64 13.6.0-1nodesource1 [24.0 MB]
Fetched 24.0 MB in 2s (12.6 MB/s)
Selecting previously unselected package nodejs.
(Reading database ... 107907 files and directories currently installed.)
Preparing to unpack .../nodejs_13.6.0-1nodesource1_amd64.deb ...
Unpacking nodejs (13.6.0-1nodesource1) ...
Setting up nodejs (13.6.0-1nodesource1) ...
Processing triggers for man-db (2.8.3-2ubuntu0.1) ...
v13.6.0
6.13.4

With NPM, and NodeJS installed, we should install yarn next. This is simply a preference thing. A single version number, such as 1.21.1, should popup at the end if all went well.

curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - && echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list && sudo apt-get update && sudo apt-get install yarn && yarn -v

In an ideal world, we would be done at this point. Unfortunately, permissions are back as a necessary step to resolve. Run the below, in order, and change out devs, if you created a different group name:

mkdir /home/devs
chown -R root:devs /home/devs
mkdir /home/devs/.npm-global
npm config set prefix "/home/devs/.npm-global"

Next, update your ~/.bashrc file to include the following block - this will set the paths for NPM, Composer, Local Path & be ready to go!

# custom paths
export MAIN_PATH=/usr/local/bin
export COMPOSER_PATH=~/.composer/vendor/bin
export NPM_BIN_PATH=/home/devs/.npm-global/bin
export PATH=${PATH}:${MAIN_PATH}:${COMPOSER_PATH}:${NPM_BIN_PATH}

Simply run source ~/.bashrc after saving the file. Running npm -v, composer, and yarn -v should all work properly. Since you'll want to end up using it anyways, run yarn global add @vue/cli to be 100% sure the front-end stuff is working alright.

PusherJS/Laravel Echo/Sockets

Run these commands after signing up for a free Pusher account:

composer require pusher/pusher-php-server
yarn add laravel-echo && yarn add pusher.js
yarn add sass sass-loader lodash (just to make things easier)

Update your .env file to use the new broadcast driver, and update the values for pusher. We should also pass the channel name so VueJS has access to everything it needs for broadcasting events.

BROADCAST_DRIVER="pusher"
# Pusher Channel
PUSHER_APP_ID="YOURAPPID"
PUSHER_APP_KEY="YOURAPPKEY"
PUSHER_APP_SECRET="YOURAPPSECRET"
PUSHER_APP_CLUSTER="mt1"
PUSHER_APP_CHANNEL="YOURCHANNELNAMEHERE"
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MIX_PUSHER_APP_CHANNEL="${PUSHER_APP_CHANNEL}"

Misc Commands

apt-get install emacs25-nox php-elisp
composer global require laravel/installer
passwd usernameishere (set a new password for user)
groupadd groupnameishere (create new group)
usermod -g groupname username (assign existing user to group)
adduser username groupname (after group created, create new user to be assigned)
openssl rand 60 | openssl base64 -A (create long, complicated, basically impossible to crack, password)

Resources