Deploy Laravel 8 to Compute Engine instance (LEMP stack)
Tags: googlecloud, nginx, laravel, deploy
Published: 2021-12-03 05:33:25

In this post I'll walk you through to get a Laravel 8 app running on a fresh Compute Engine instance in NGINX way. We'll be using these assumptions:

- Already have a Laravel 8 project stored in a remote repository (I'm using Github for this post).
- Stateful solution which means the database and application code are placed together in the instance.
- You want to use Google Cloud Platform services to host your application.
- You want to have full control over server environments eg. web server configurations.
- You want your app to just works as is.
- You have familiarity with Linux commands.
- You need optimal configuration for your app.
- You may skip the database steps if you are not actually use it on your project.

Now, let's get started.

## Step 1: Create a Compute Engine instance

First thing to do is to create and configure our VM instance to serve our app. In Cloud Console, refer to Compute Engine menu then click on **CREATE INSTANCE** button as shown below.
![Compute Engine main page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i3kmdmfxivteba0q1jqt.png) 

Then in create instance form, fill and set properties for our instance. My configuration goes like these:

![Compute Engine instance creation configuration part 1/6](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4xc8ybt5kn1spw3m3k6s.png)
- Choose your instance name wisely as it can't be changed later after creation.
- Pick your nearest region and zone for lower latency.
- Choose lowest possible machine class and type to avoid huge charge to your bill, even if it's just for trial use.
 
![Compute Engine instance creation configuration part 2/6](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/crzh8yzjb6hs6alzxh0s.png)
- On boot disk configuration, choose your most familiar operating system. If it's required, you may choose latest LTS version.
- Choose `SSD persistent disk` and smallest size as starting point (10 GB is the lowest). SSD comes with greater performance compared to others except the Extreme one, but costs higher.

![Compute Engine instance creation configuration part 3/6](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6c56auratycd00987pl9.png)
- Enable all HTTP and HTTPS access, just in case you need to configure SSL certificate for your instance.

![Compute Engine instance creation configuration part 4/6](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yf2o468pi82kkmc31vc9.png)
 
- On advanced configuration, the Network section, choose your existing VPC network. (I already have a custom one called `local-vpc`. You may already have the default one.)
- Set a static external IP address as shown below.

![Compute Engine instance creation configuration part 5/6](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7kugo5i7fjvdpksnrles.png)
- Set a static external IP address now so you won't bother to reconfigure DNS A record to this instance everytime its IP address changes.
 
![Compute Engine instance creation configuration part 6/6](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g62hw7rzwqkrqo1v1049.png)
- On advanced configuration, the Security section, enable Shielded VM for increased security to your instance.
- You may want to add a **Public SSH key** in this section, so you can access your instance securely from your workstation or PC.

Click on **CREATE** button to create the instance.

---

## Step 2: Install and configure server environments

Now your instance already up and running. We need to install our necessary environments for our app.
![Created VM](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/91js5z1ep9ws4cidexmx.png)
Click on SSH button (or run `ssh` command from your PC that access this VM's external IP address)

Now first in your SSH terminal, run these commands:

- Set your timezone
  ```shell
sudo timedatectl set-timezone Asia/Jakarta
  ```
- Check for any updates
  ```shell
sudo apt-get update
  ```
- Do upgrade according to updates
  ```shell
sudo apt-get upgrade
  ```
- (Optional) Do reboot the instance
  ```shell
sudo reboot
  ```

At this point, access our instance once again and we will install our environments:
- Install NGINX
  ```shell
sudo apt install nginx
```
- Install MariaDB server. At this point, follow the instruction to make our MariaDB server secure. You'll do the following:
  - Authenticate as root user (just press enter to move forward)
  - Set new root password (remember the password you entered here!)
  - Accept everything (don't forget to read the instruction, though)
  ```shell
sudo apt install mariadb-server
sudo mysql_secure_installation
```

- Create a new database and database user for our app
```shell
sudo mysql
```
```shell
mysql> CREATE DATABASE 'mydb';
mysql> CREATE USER 'mymy'@'localhost' IDENTIFIED BY 'myPassw0rd';
mysql> GRANT ALL ON 'mydb'.* TO 'mymy'@'localhost';
mysql> FLUSH PRIVILEGES;
mysql> EXIT
```

- Install `unzip` so our Composer can do its work when installing dependencies
```shell
sudo apt install unzip
```
  
- Add PPA repository for NGINX and PHP
 ```shell
sudo add-apt-repository ppa:ondrej/php && sudo apt update
sudo add-apt-repository ppa:ondrej/nginx && sudo apt update
sudo apt upgrade
```
- Install PHP-FPM and some 'necessary' PHP modules (refer to Laravel official docs anyway)
```shell
sudo apt install -y php7.4-fpm php7.4-curl php7.4-gd php7.4-json php7.4-mbstring php7.4-mysql php7.4-opcache php7.4-xml php7.4-xmlrpc php7.4-fileinfo php7.4-imagick php-pear
sudo apt upgrade
```
- Check PHP CLI version with `php -v` command. If the PHP CLI version check returns other than version 7.4, run below command to change its version. Type a number option that shows version 7.4 then press Enter.
  ```shell
sudo update-alternatives --config php
```
- Create an NGINX config file under `/etc/nginx/sites-available/` directory. Name the config file either with a domain name you have or just give it `main` or anything you want. I'll go with example.com.
```shell
sudo nano /etc/nginx/sites-available/example.com
```
  This will open a NANO text editor ready to create the file. Copy and paste the code below to the editor.
```nginx
server {
  listen 80;

  # Use your domain/subdomain name here
  server_name example.com;

  # We will clone our "repo-name" repository which
  # contains our Laravel project to /var/www/ directory later.
  # Change this path according to your repository name
  # and end with /public.
  root /var/www/repo-name/public;

  index index.php;

  add_header X-Frame-Options "SAMEORIGIN";
  add_header X-Content-Type-Options "nosniff";

  charset utf-8;

  location / {
    try_files $uri $uri/ /index.php?$query_string;
  }

  location = /favicon.ico { access_log off; log_not_found off; }
  location = /robots.txt  { access_log off; log_not_found off; }

  error_page 404 /index.php;

  location ~ \.php$ {
    fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
    include fastcgi_params;
  }

  location ~ /\.(?!well-known).* {
    deny all;
  }

  # The access log file name is up to you.
  access_log /var/log/nginx/access_laravel.log;

  # And this error log file name as well is up to you.
  error_log /var/log/nginx/error_laravel.log;
}
```
Make sure everything is correct, then press Ctrl + X, press Y, and finally press Enter. This will save the new file and exit the editor.
- Create a symbolic link to the config file in `/etc/nginx/sites-enabled/` directory, so NGINX can read our file from there.
```shell
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
```
- Unlink existing default configuration
```shell
sudo unlink /etc/nginx/sites-enabled/default
```
- Test our NGINX configuration
  ```shell
sudo nginx -t
```
- If no errors come up, restart the NGINX service to load our new configuration
```shell
sudo service nginx reload
```

Now, we will install additional environments for our Laravel app.
- Install Node Version Manager (NVM) to run npm installation in the project later.
  ```shell
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
source ~/.bashrc
```
- Install NodeJS latest LTS version. Use `nvm list-remote` to list Node versions (current latest LTS version is Gallium)
```shell
nvm install lts/gallium
```
- You may want to check the installed version of Node and NPM by running these commands.
```shell
node -v
npm -v
```
- Install Composer **(Required)**
```shell
curl -sS https://getcomposer.org/installer -o composer-setup.php
```
```shell
HASH=`curl -sS https://composer.github.io/installer.sig`
php -r "if (hash_file('SHA384', 'composer-setup.php') === '$HASH') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
```
```shell
sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer
```

---

## Step 3: Pull your app from a remote repository
Now it's time to put our app to this VM. I'll go with more secure way to clone our repository by using SSH keys. You may use your existing SSH keys or generate a new one.

First, we need to generate a new SSH key pair. Please refer to [this site](https://www.ssh.com/academy/ssh/keygen) to learn about how to use SSH.
```shell
ssh-keygen -t rsa -b 4096 -C "<your-Github-email>"
```
Make sure change "your-Github-email" to **your actual email address used in Github** (it's not mandatory, to be honest. You can fill anything, but email is recommended as it's easier to identify).
You'll be asked file name for the key. You may just press **Enter** to use default file name.
Then, you'll be asked to type in a passphrase. It's a best practice to type in the passphrase, but for our case, it's not necessary to do so. Just press **Enter** twice so you won't use any passphrase.
Finally, the SSH key pair is generated and stored within **.ssh folder in current user directory** (`~/.ssh/`). There will be two files: one with `.pub` extension is called **public key**, the other one is called **private key**.

Next step, we will put our **public key** content to our Github account (if you are using other service that Github, refer to their documentation about how to clone a repo using SSH).
Copy our **public key** content which usually begins with `ssh-rsa xxxx`.
Then, go to your [Github account settings page > SSH and GPG keys](https://github.com/settings/keys). Click on **New SSH key** button. Type a name for the key (anything but noticeable) and paste the public key in provided text area. Click on **Add new key** button.
![Add SSH key to Github](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vh5utvxqy48uf1ejij2e.png)

Now, we can start cloning our repo from Github securely. Here are the steps.
- **(Required)** Secure our SSH private key by changing file permission to **read-only by its owner (current user who creates the key pair**.
```shell
chmod 400 .ssh/id_rsa
```
- Activate `ssh-agent` so we can start use SSH.
```shell
eval `ssh-agent -s`
```
- Add our **private key** to the SSH agent. Remember your key file name.
```shell
ssh-add id_rsa
```
- Test whether we can connect to Github via SSH. Type `yes` and press Enter to confirm connection when being asked.
```shell
ssh -T git@github.com
```
- If it returns a message like: "`Hi, <user>! You've successfully authenticated, but GitHub does not provide shell access.`", then it's successful connection! We can continue to clone our repo to `/var/www` directory by running this command (change to your username and repository name accordingly).
```shell
cd /var/www/
sudo chown $USER:$USER .
git clone git@github.com:<your-Github-username>/<your-repo-name>
sudo chown root:root .
```
What I'm doing upthere is changing ownership of `/var/www` folder to current user, so we can run `git clone` command as current user. Thus, our cloned repo will be owned by it. Then, we return the ownership of `/var/www` directory back to `root` user (not everything inside it). Pay attention to `cd` command.

---

## Step 4: Install Laravel dependencies and configure project environment variables
At this point, we already have our cloned project inside `/var/www` directory. Make sure you are inside the project directory by using `cd` command.

First thing is to create a `.env` file by copying from `.env.example` file.
```shell
cp .env.example .env
```

Then, open and edit the copied `.env` file with NANO editor.
```shell
nano .env
```
Change respective environment variable values to suit your requirements. For my case, I'll go with updating these values.
```env
APP_ENV=production
APP_DEBUG=false
DB_DATABASE=my-db
DB_USERNAME=mymy
DB_PASSWORD=myPassw0rd
```
Remember your database user, its password, and the database whom its database user can access (refer to [Step 2](#step-2-install-and-configure-server-environments)). Press **Ctrl + X**, then **Y**, and finally press **Enter** to save.

We will install all Laravel modules and its dependencies by running these commands.
```shell
composer install --optimize-autoloader --no-dev
npm install
npm run dev
```

Moving forward, we need to generate application key which is mandatory.
```shell
php artisan key:generate
```

Next we will create a symbolic link for our `/storage` folder.
```shell
php artisan storage:link
```

One more step, we can migrate our database. You may skip this if you are actually not using database.
```shell
php artisan migrate:refresh --seed
```

Finally, we need to change ownership for `www-data` user (web server user) so it can access and serve our app. Change `repo-name` to your actual repository or folder name.
```shell
sudo chown -R www-data:www-data /var/www/repo-name
```
There are many alternatives to this. One way, you can add `www-data` user to current user group and vice versa.

At last, we can access our website through external IP address of our VM. There are more steps if we want to make use of our own domain instead of server's IP address, but those are for future post.
![Final view](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i2foowghl9h0i6xlkigt.png)
 
 
---

## Conclusion

It's pretty hefty to just deploying one website, especially if there are many requirements behind the scene. As the title said, one way we can deploy our Laravel app is just by leveraging most used server engine like NGINX, then we can put every other Laravel dependencies in place inside the server in a pretty straightforward way (read: running through commands).
One thing to note that we need to pay attention about security of the server itself, which a small part of it is by making use of SSH. We can further improve this by configuring correct firewall rules, install SSL certificates, reconfigure SSH access, optimize NGINX configurations, etc. If you mind though, you can configure high availability to your app even with Compute Engine by making our VMs stateless.

---

That's all for this post. Hope it helps you. Don't hesitate to ask me if you are unsure or you want add something that I might be missing in this post. Thank you for your time reading this 🤗