<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Andre Kho</title>
    <description>The latest articles on DEV Community by Andre Kho (@nekoto).</description>
    <link>https://dev.to/nekoto</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F295107%2F641f89d8-1232-4d48-a708-35c1262761e4.png</url>
      <title>DEV Community: Andre Kho</title>
      <link>https://dev.to/nekoto</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nekoto"/>
    <language>en</language>
    <item>
      <title>Enable OAuth2 (XOAUTH2) for Sending Emails Using Gmail Account in OJS 3</title>
      <dc:creator>Andre Kho</dc:creator>
      <pubDate>Fri, 02 May 2025 05:16:46 +0000</pubDate>
      <link>https://dev.to/nekoto/enable-oauth2-xoauth2-for-sending-emails-using-gmail-account-in-ojs-3-597j</link>
      <guid>https://dev.to/nekoto/enable-oauth2-xoauth2-for-sending-emails-using-gmail-account-in-ojs-3-597j</guid>
      <description>&lt;p&gt;&lt;strong&gt;Gmail&lt;/strong&gt; has been a long-time friend (probably) for developers to help in sending automated emails to users freely right from their systems. But of course the scale of the said systems is small to medium one, estimated up to several tens or hundreds of active users yearly. To be able to use Gmail account in the system, developers simply had to use the email address and password in the mail configuration (using a mailing library) in their projects. But this kind of traditional method has long been criticized because of its unsecure nature, where we had to store the password in separate environment variable or hard-coded it in the code just for the baddie to be see it easily.&lt;/p&gt;

&lt;p&gt;In 2024, Google has decided to proceed the transition of authentication method for Gmail accounts from traditional passwords to OAuth2 (&lt;a href="https://support.google.com/a/answer/14114704" rel="noopener noreferrer"&gt;details here&lt;/a&gt;). This policy has forced many users, especially developers, to refactor their existing systems to be able to use &lt;strong&gt;OAuth2 (XOAUTH2)&lt;/strong&gt; for authenticating Gmail accounts that are being used as system account for mailing purposes. Once a feature called "Less secure apps" access for legacy systems was active in Google Account settings for people to be able to transition to newer method, now it has been effectively removed.&lt;/p&gt;

&lt;p&gt;You can read &lt;a href="https://developers.google.com/workspace/gmail/imap/xoauth2-protocol" rel="noopener noreferrer"&gt;this documentation&lt;/a&gt; about how OAuth2 (XOAUTH2) method works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open Journal System (OJS)&lt;/strong&gt; is one of the systems that still use the traditional method for mailing service. Since the change has been implemented throughout the year, people using OJS gradually catch on the issue, which their OJS system can not send automated emails. Have learnt about the Google's change in policy and already mitigate the issue myself before the implementation, I will share what you need to do to update your OJS configuration, so it can be able to send emails with Gmail account once again with OAuth2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;This workaround is for you if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your organization is using Google Workspace. Therefore, the Gmail account which being used for automated mailing purposes is created through Google Workspace.&lt;/li&gt;
&lt;li&gt;You have administrative access to Google Cloud resources of your organization (should be automatically associated with the Google Workspace) and credentials of the Gmail account.&lt;/li&gt;
&lt;li&gt;You hosted the OJS systems either in Google Cloud's organization (much better) or other platforms.&lt;/li&gt;
&lt;li&gt;You are using OJS version 3.&lt;/li&gt;
&lt;li&gt;You have sufficient understanding about how PHP project works.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Go to Google Cloud Console
&lt;/h2&gt;

&lt;p&gt;Visit &lt;a href="https://console.cloud.google.com/" rel="noopener noreferrer"&gt;Google Cloud Console&lt;/a&gt; of your organization. Make sure you are in the &lt;strong&gt;Project&lt;/strong&gt; that contains your hosted OJS if you hosted your OJS with Google Cloud. For you who don't use Google Cloud for hosting, still you can &lt;strong&gt;create a Project&lt;/strong&gt; here first without actually creating VM or any compute solutions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create OAuth client ID
&lt;/h2&gt;

&lt;p&gt;This steps assumed that you have created an &lt;strong&gt;OAuth consent screen&lt;/strong&gt;. If you haven't, you need to go to the &lt;strong&gt;OAuth consent screen&lt;/strong&gt; first, then fill in information as required. No additional configuration needed there.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;APIs &amp;amp; Services&lt;/strong&gt; &amp;gt; Credentials.&lt;/li&gt;
&lt;li&gt;Click on &lt;strong&gt;+ Create credentials&lt;/strong&gt; button, then click on OAuth client ID.&lt;/li&gt;
&lt;li&gt;In the creation form of OAuth client ID page, choose &lt;strong&gt;Web application&lt;/strong&gt; under &lt;strong&gt;Application type&lt;/strong&gt; dropdown. Give name of the client, e.g. "OJS".&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Authorized redirect URIs&lt;/strong&gt; section, click on &lt;strong&gt;+ Add URI&lt;/strong&gt; button, then type in the full URI of OJS' PHPMailer's OAuth handler script, it should be like: &lt;code&gt;https://[YOUR OJS DOMAIN]/lib/pkp/lib/vendor/phpmailer/phpmailer/get_oauth_token.php&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click Create.&lt;/li&gt;
&lt;li&gt;After creation of OAuth client ID, you can always go back to the &lt;strong&gt;Credentials&lt;/strong&gt; page and click on the created OAuth client ID to see Additional information and client secrets, which some of this values will be added to the OJS. Take note of:

&lt;ul&gt;
&lt;li&gt;Client ID&lt;/li&gt;
&lt;li&gt;Client secret&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcdsc8gni246bk75wuhci.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcdsc8gni246bk75wuhci.png" alt="Client ID details" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Modify the &lt;code&gt;get_oauth_token.php&lt;/code&gt; script
&lt;/h2&gt;

&lt;p&gt;You need to get into the directory of previous URI and edit the &lt;code&gt;get_oauth_token.php&lt;/code&gt; directly. Please compare the existing script with the updated version below, before make any changes to yours. Create a backup first!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="cd"&gt;/**
 * PHPMailer - PHP email creation and transport class.
 * PHP Version 5.5
 * @package PHPMailer
 * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
 * @author Marcus Bointon (Synchro/coolbru) &amp;lt;phpmailer@synchromedia.co.uk&amp;gt;
 * @author Jim Jagielski (jimjag) &amp;lt;jimjag@gmail.com&amp;gt;
 * @author Andy Prevost (codeworxtech) &amp;lt;codeworxtech@users.sourceforge.net&amp;gt;
 * @author Brent R. Matzelle (original founder)
 * @copyright 2012 - 2020 Marcus Bointon
 * @copyright 2010 - 2012 Jim Jagielski
 * @copyright 2004 - 2009 Andy Prevost
 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
 * @note This program is distributed in the hope that it will be useful - WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.
 */&lt;/span&gt;

&lt;span class="cd"&gt;/**
 * Get an OAuth2 token from an OAuth2 provider.
 * * Install this script on your server so that it's accessible
 * as [https/http]://&amp;lt;yourdomain&amp;gt;/&amp;lt;folder&amp;gt;/get_oauth_token.php
 * e.g.: http://localhost/phpmailer/get_oauth_token.php
 * * Ensure dependencies are installed with 'composer install'
 * * Set up an app in your Google/Yahoo/Microsoft account
 * * Set the script address as the app's redirect URL
 * If no refresh token is obtained when running this file,
 * revoke access to your app and run the script again.
 */&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;PHPMailer\PHPMailer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cd"&gt;/**
 * Aliases for League Provider Classes
 * Make sure you have added these to your composer.json and run `composer install`
 * Plenty to choose from here:
 * @see http://oauth2-client.thephpleague.com/providers/thirdparty/
 */&lt;/span&gt;
&lt;span class="c1"&gt;// @see https://github.com/thephpleague/oauth2-google&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;League\OAuth2\Client\Provider\Google&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// @see https://packagist.org/packages/hayageek/oauth2-yahoo&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Hayageek\OAuth2\Client\Provider\Yahoo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// @see https://github.com/stevenmaguire/oauth2-microsoft&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Stevenmaguire\OAuth2\Client\Provider\Microsoft&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'code'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'provider'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;Select Provider:&lt;span class="nt"&gt;&amp;lt;br/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;'?provider=Google'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Google&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;'?provider=Yahoo'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Yahoo&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;'?provider=Microsoft'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Microsoft/Outlook/Hotmail/Live/Office365&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// require 'vendor/autoload.php';&lt;/span&gt;
&lt;span class="c1"&gt;// require($_SERVER['DOCUMENT_ROOT'] . '/lib/pkp/lib/vendor/autoload.php');&lt;/span&gt;
&lt;span class="c1"&gt;// Below is the new location of autoload&lt;/span&gt;
&lt;span class="k"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/var/www/lib/vendor/autoload.php'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nb"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$providerName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;array_key_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'provider'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$providerName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'provider'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'provider'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$providerName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;elseif&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;array_key_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'provider'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$providerName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'provider'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$providerName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Google'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Yahoo'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Only Google, Microsoft and Yahoo OAuth2 providers are currently supported in this script.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;//These details are obtained by setting up an app in the Google developer console,&lt;/span&gt;
&lt;span class="c1"&gt;//or whichever provider you're using.&lt;/span&gt;
&lt;span class="nv"&gt;$clientId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'[YOUR CLIENT ID HERE]'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$clientSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'[YOUR CLIENT SECRET HERE]'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;//If this automatic URL doesn't work, set it yourself manually to the URL of this script&lt;/span&gt;
&lt;span class="c1"&gt;//$redirectUri = (isset($_SERVER['HTTPS']) ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'];&lt;/span&gt;
&lt;span class="nv"&gt;$redirectUri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://[YOUR OJS DOMAIN]/lib/pkp/lib/vendor/phpmailer/phpmailer/get_oauth_token.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'clientId'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'clientSecret'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$clientSecret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'redirectUri'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$redirectUri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'accessType'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'offline'&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nv"&gt;$options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="nv"&gt;$provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$providerName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s1"&gt;'Google'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="nv"&gt;$provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Google&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'scope'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'https://mail.google.com/'&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s1"&gt;'Yahoo'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="nv"&gt;$provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Yahoo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="nv"&gt;$provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Microsoft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'scope'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'wl.imap'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'wl.offline_access'&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Provider missing'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'code'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// If we don't have an authorization code then get one&lt;/span&gt;
    &lt;span class="nv"&gt;$authUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$provider&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getAuthorizationUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'oauth2state'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$provider&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Location: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$authUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Check given state against previously stored one to mitigate CSRF attack&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;elseif&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'state'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'state'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'oauth2state'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'oauth2state'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="k"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'provider'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Invalid state'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'provider'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="c1"&gt;// Try to get an access token (using the authorization code grant)&lt;/span&gt;
    &lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$provider&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getAccessToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'authorization_code'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'code'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'code'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Use this to interact with an API on the users behalf&lt;/span&gt;
    &lt;span class="c1"&gt;// Use this to get a new access token if the old one expires&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'Refresh Token: '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRefreshToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Replace &lt;code&gt;vendor&lt;/code&gt; packages manually
&lt;/h2&gt;

&lt;p&gt;Due to the obsolete packages being used by this particular script, you actually need to update the dependencies of this PHPMailer package. However, it may be complicated since it's also a dependency for OJS. For this workaround, you can use my &lt;code&gt;vendor&lt;/code&gt; packages from &lt;a href="https://github.com/nekoto-kun/ojs-vendor-update/blob/main/vendor.zip" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Extract this archive into a separate directory aside of your OJS in the server, e.g. &lt;code&gt;/var/www/lib&lt;/code&gt; (as I pointed out in the PHP script above).&lt;/p&gt;

&lt;p&gt;If you want to get yourself the &lt;em&gt;new&lt;/em&gt; &lt;code&gt;vendor&lt;/code&gt; folder, you may set up a local PHP project (just create an empty folder anywhere you want). Inside this folder, run below command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require league/oauth2-google
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After adding the dependencies, you can archive the &lt;code&gt;vendor&lt;/code&gt; directory in the local project and upload it to your OJS server. Then, extract the archive into separate folder, accessible by the OJS like above mentioned, e.g. &lt;code&gt;/var/www/lib&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Update &lt;code&gt;Mail.inc.php&lt;/code&gt; dependency
&lt;/h2&gt;

&lt;p&gt;Put this &lt;code&gt;require&lt;/code&gt; line at the first lines of the &lt;code&gt;Mail.inc.php&lt;/code&gt; script located in &lt;code&gt;lib/pkp/classes/mail&lt;/code&gt;, right before the actual code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/var/www/lib/vendor/autoload.php'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This line help to make sure the &lt;code&gt;Mail&lt;/code&gt; class can load dependencies right to the PHPMailer package.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Retrieve a Refresh Token
&lt;/h2&gt;

&lt;p&gt;After setting things up, you need to visit the URI of &lt;code&gt;get_oauth_token.php&lt;/code&gt;, so it should be: &lt;code&gt;https://[YOUR OJS DOMAIN]/lib/pkp/lib/vendor/phpmailer/phpmailer/get_oauth_token.php&lt;/code&gt;. In this page, click &lt;strong&gt;Google&lt;/strong&gt; link and you may be prompted with Google Login page. Login with your purposed Gmail account.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpudxzax2ntnliukgzsvt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpudxzax2ntnliukgzsvt.png" alt="get_oauth_token.php page" width="686" height="165"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see a line of refresh token after a successful login. Copy this value and paste somewhere else. We need this refresh token for new configuration of OJS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Add XOAUTH2 configuration to &lt;code&gt;config.inc.php&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Edit the &lt;code&gt;config.inc.php&lt;/code&gt; in the root directory of your OJS, focus on &lt;strong&gt;Email settings&lt;/strong&gt; section here. Adjust the configuration like this example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;Use&lt;/span&gt; &lt;span class="no"&gt;SMTP&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sending&lt;/span&gt; &lt;span class="n"&gt;mail&lt;/span&gt; &lt;span class="n"&gt;instead&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;smtp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;On&lt;/span&gt;

&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="no"&gt;SMTP&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;
&lt;span class="n"&gt;smtp_server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;smtp&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gmail&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;
&lt;span class="n"&gt;smtp_port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;587&lt;/span&gt;

&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;Enable&lt;/span&gt; &lt;span class="no"&gt;SMTP&lt;/span&gt; &lt;span class="n"&gt;authentication&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;Supported&lt;/span&gt; &lt;span class="n"&gt;mechanisms&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tls&lt;/span&gt;
&lt;span class="n"&gt;smtp_auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tls&lt;/span&gt;
&lt;span class="n"&gt;smtp_username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;YOUR&lt;/span&gt; &lt;span class="no"&gt;GMAIL&lt;/span&gt; &lt;span class="no"&gt;ADDRESS&lt;/span&gt; &lt;span class="no"&gt;HERE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;smtp_password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;YOUR&lt;/span&gt; &lt;span class="no"&gt;GMAIL&lt;/span&gt; &lt;span class="no"&gt;PASSWORD&lt;/span&gt; &lt;span class="no"&gt;HERE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;smtp_authtype&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;XOAUTH2&lt;/span&gt;
&lt;span class="n"&gt;smtp_oauth_provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Google&lt;/span&gt;
&lt;span class="n"&gt;smtp_oauth_email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;YOUR&lt;/span&gt; &lt;span class="no"&gt;GMAIL&lt;/span&gt; &lt;span class="no"&gt;ADDRESS&lt;/span&gt; &lt;span class="no"&gt;HERE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;smtp_oauth_clientid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;YOUR&lt;/span&gt; &lt;span class="no"&gt;OAUTH2&lt;/span&gt; &lt;span class="no"&gt;CLIENT&lt;/span&gt; &lt;span class="no"&gt;ID&lt;/span&gt; &lt;span class="no"&gt;HERE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;smtp_oauth_clientsecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;YOUR&lt;/span&gt; &lt;span class="no"&gt;OAUTH2&lt;/span&gt; &lt;span class="no"&gt;CLIENT&lt;/span&gt; &lt;span class="no"&gt;SECRET&lt;/span&gt; &lt;span class="no"&gt;HERE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;smtp_oauth_refreshtoken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'[YOUR OAUTH2 REFRESH TOKEN HERE]'&lt;/span&gt;

&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;Allow&lt;/span&gt; &lt;span class="n"&gt;envelope&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="n"&gt;specified&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;may&lt;/span&gt; &lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="n"&gt;possible&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="n"&gt;configurations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;allow_envelope_sender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;On&lt;/span&gt;

&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;Default&lt;/span&gt; &lt;span class="n"&gt;envelope&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;none&lt;/span&gt; &lt;span class="n"&gt;is&lt;/span&gt; &lt;span class="n"&gt;specified&lt;/span&gt; &lt;span class="n"&gt;elsewhere&lt;/span&gt;
&lt;span class="nf"&gt;default_envelope_sender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;YOUR&lt;/span&gt; &lt;span class="no"&gt;GMAIL&lt;/span&gt; &lt;span class="no"&gt;ADDRESS&lt;/span&gt; &lt;span class="no"&gt;HERE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;Force&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;envelope&lt;/span&gt; &lt;span class="nf"&gt;sender&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;present&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;This&lt;/span&gt; &lt;span class="n"&gt;is&lt;/span&gt; &lt;span class="n"&gt;useful&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;setting&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;wide&lt;/span&gt; &lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;The&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="n"&gt;will&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;force_default_envelope_sender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;On&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 8: Test the mailing feature
&lt;/h2&gt;

&lt;p&gt;Finally, you can test the changes you have made so far by creating a dummy submission in your OJS website, or try anything else that can trigger the outgoing mail action from the site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;OJS has been used by many institutions for years to help managing scienfitic articles and publications. The dynamics from Google especially in the rise of a more secure method to authenticate email addresses using OAuth2 make legacy systems like OJS has to change, where email notifications are one of the most important feature for users may be disrupted if they are using Gmail. While there's maybe difficulties faced by institutions to update their OJS systems following the official documentation, when they are using Gmail accounts to send emails, we can still use some workaround here. Not the best one, but I hope it can help you get there.&lt;/p&gt;

</description>
      <category>ojs</category>
      <category>gmail</category>
      <category>tips</category>
      <category>oauth2</category>
    </item>
    <item>
      <title>Easy Uptime Monitoring Tool with Uptime Kuma in Google Cloud Platform</title>
      <dc:creator>Andre Kho</dc:creator>
      <pubDate>Wed, 08 Dec 2021 09:05:38 +0000</pubDate>
      <link>https://dev.to/nekoto/easy-uptime-monitoring-tool-with-uptime-kuma-in-google-cloud-platform-5d3</link>
      <guid>https://dev.to/nekoto/easy-uptime-monitoring-tool-with-uptime-kuma-in-google-cloud-platform-5d3</guid>
      <description>&lt;p&gt;Monitoring our things like websites and APIs eventually become one of major task right after we have successfully deployed them. We want to make sure that our services can be online and healthy all the time for users. To get this done, we usually don't want to waste time to develop our own monitoring tool just for ourselves.&lt;/p&gt;

&lt;p&gt;Instead, we can use already established online service like UptimeRobot. We can simply register to them, put our URLs that need to be monitored, then we can see how our services performed through provided charts. But just like any third party services, there are always limitations for every free tiers. If we need to have more advanced features, then we pay for paid tier which is kinda waste of money depending of what we actually need. For example, we simply want the uptime checking cycle reduced to some minutes less than five minutes of free tier.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjfr58bohzsiq228rpbly.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjfr58bohzsiq228rpbly.png" alt="Kuma repo" width="800" height="578"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hence, there is &lt;strong&gt;Uptime Kuma&lt;/strong&gt;. This is the best open-source monitoring tool yet I've ever found. It's free to use, inspired by UptimeRobot (much better and simpler), no tiered restrictions and best of all, it's so easy to deploy for ourselves! You can go check their official Github repository &lt;a href="https://github.com/louislam/uptime-kuma" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this post, anyway, I'm about to guide you to how we can easily deploy &lt;strong&gt;Uptime Kuma&lt;/strong&gt; to Google Cloud Platform. It's pretty straightforward to what I'm going to demonstrate, if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We just need to deploy the tool without using any domain/subdomain&lt;/li&gt;
&lt;li&gt;We want this tool accessible through internet&lt;/li&gt;
&lt;li&gt;We want cheap deployment (yes, I'm not gonna put 'cheapest' since there will always the free way)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now let's get started!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Notes: I'm going to use Compute Engine, instead of App Engine Flexible, Cloud Run or any other services that can host a Docker container. For what I have experienced so far, this is the easiest way to go.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a Compute Engine instance with Container Deployment
&lt;/h2&gt;

&lt;p&gt;Uptime Kuma is &lt;strong&gt;stateful&lt;/strong&gt;. It stores some persistent data in itself using local SQLite (based on the repo). Hence, they give ways for us to deploy the tool, which one is by deploying Docker container. The official Docker image is ready to deploy, no need for us to install and configure the environments.&lt;br&gt;
However, one step we still need to do before deploying the container is to create a persistent volume. Of course, this is well supported by Compute Engine, but there are something we need to configure.&lt;/p&gt;

&lt;p&gt;Now let's create our instance through this button in Compute Engine page.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ti8eidpp3fjqei4o1ho.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ti8eidpp3fjqei4o1ho.png" alt="Create instance button" width="439" height="210"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, we configure our instance like these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Give name to our instance and choose location that covered by &lt;a href="https://cloud.google.com/free/docs/gcp-free-tier#free-tier-usage-limits" rel="noopener noreferrer"&gt;GCP's Free Tier&lt;/a&gt;.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft3mv9m8tb3shhtohez68.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft3mv9m8tb3shhtohez68.png" alt="Name and location" width="549" height="237"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Choose machine type that covered by &lt;a href="https://cloud.google.com/free/docs/gcp-free-tier#free-tier-usage-limits" rel="noopener noreferrer"&gt;GCP's Free Tier&lt;/a&gt; as well.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo774fp1hchnec0tptgfz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo774fp1hchnec0tptgfz.png" alt="Choose machine type" width="543" height="348"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Next, we configure our boot disk type. Click on &lt;strong&gt;CHANGE&lt;/strong&gt; button. On the form, choose &lt;strong&gt;Standard persistent disk&lt;/strong&gt;, then click &lt;strong&gt;SELECT&lt;/strong&gt; button. (This disk type is covered by &lt;a href="https://cloud.google.com/free/docs/gcp-free-tier#free-tier-usage-limits" rel="noopener noreferrer"&gt;GCP's Free Tier&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpwzo1zsfz9o9rke4umwx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpwzo1zsfz9o9rke4umwx.png" alt="Boot disk change" width="540" height="185"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fphzfty0o2cfr5nl2crri.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fphzfty0o2cfr5nl2crri.png" alt="Standard boot disk" width="541" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Scroll down a little bit, you will find this section. Click &lt;strong&gt;DEPLOY CONTAINER&lt;/strong&gt; button.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F56d7ocq40q4xay74ohch.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F56d7ocq40q4xay74ohch.png" alt="Deploy container button" width="271" height="115"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In the shown form, first fill in these fields. We type in the URL of Uptime Kuma's latest image from Docker Hub: &lt;code&gt;registry.hub.docker.com/louislam/uptime-kuma:1&lt;/code&gt;. Then if you need it, you can check the options here.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3dye2mzdgj7tcxwwl4em.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3dye2mzdgj7tcxwwl4em.png" alt="Describe Docker image" width="542" height="276"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scroll down, you'll find this section. Click on &lt;strong&gt;ADD VOLUME&lt;/strong&gt;.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5uhn42hkdfs0azd3fqxv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5uhn42hkdfs0azd3fqxv.png" alt="Volume section" width="535" height="129"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now before we continue, you can choose one of two options below to create a Volume for our container. Option #1 is using &lt;strong&gt;TmpFS&lt;/strong&gt; volume type, Option #2 is using &lt;strong&gt;Disk&lt;/strong&gt; volume type.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option #1, using TmpFS volume type
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Choose &lt;strong&gt;TmpFS&lt;/strong&gt; volume type. Even it is a temporary file storage, based on &lt;a href="https://cloud.google.com/compute/docs/containers/configuring-options-to-run-containers#mounting_tmpfs_file_system_as_a_data_volume" rel="noopener noreferrer"&gt;Compute Engine documentation here&lt;/a&gt;, it states that: &lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;with tmpfs on a Compute Engine container, the volume and its data persist across container restarts and are deleted only on VM restart.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz88f6m73fms9a51q5zjw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz88f6m73fms9a51q5zjw.png" alt="Volume TmpFS mount" width="533" height="334"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click &lt;strong&gt;DONE&lt;/strong&gt; and finally &lt;strong&gt;SELECT&lt;/strong&gt; button. Skip Option #2 steps below.
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fipwesvle4188finxsn2u.png" alt="Done volume configuration" width="536" height="475"&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Option #2, using Disk as volume type
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Close the Container deployment form for now. Then, scroll down until you find this advanced section. Click on it and scroll to &lt;strong&gt;Disks&lt;/strong&gt; section.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8z5j0lubanf151jrbytw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8z5j0lubanf151jrbytw.png" alt="Advanced section" width="433" height="37"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click on &lt;strong&gt;ADD NEW DISK&lt;/strong&gt; to open disk creation form. Fill in configuration like below. I simply choose &lt;strong&gt;zonal, blank, standard persistent disk with 10 GB in capacity&lt;/strong&gt;. Click &lt;strong&gt;SAVE&lt;/strong&gt; button when finished.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2p4zt5x47ot55h6l8v4u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2p4zt5x47ot55h6l8v4u.png" alt="New disk button" width="527" height="120"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbvlxtihjnqtu2us2mt3x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbvlxtihjnqtu2us2mt3x.png" alt="New disk form" width="573" height="664"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Now back to steps in Container Deployment form, in Volume mount section, choose &lt;strong&gt;Disk&lt;/strong&gt; as volume type. Then pick your just created Disk and fill in other fields like below. Click &lt;strong&gt;DONE&lt;/strong&gt; and finally &lt;strong&gt;SELECT&lt;/strong&gt; button. Now we can move on to next steps.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz5idefcpcni6t4xkjrlc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz5idefcpcni6t4xkjrlc.png" alt="Volume disk mounts" width="534" height="454"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Further, we have to give our instance a network tag, for example: &lt;code&gt;allow-kuma&lt;/code&gt;. Soon after we create the instance, we need to configure firewall to this instance. You may also want to configure a static external IP address with &lt;strong&gt;Standard network tier&lt;/strong&gt; in this section.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0xttt9eocg9lo8y883fl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0xttt9eocg9lo8y883fl.png" alt="Network tag" width="543" height="529"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Finally, click on &lt;strong&gt;CREATE&lt;/strong&gt; button at the bottom of the screen.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyjerhgl6fbwvx2mlwmw4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyjerhgl6fbwvx2mlwmw4.png" alt="Create button" width="454" height="80"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Configure Firewall rule
&lt;/h2&gt;

&lt;p&gt;Now we already have our instance up and running, but we can't access it through its external IP address yet. We need to open port number &lt;code&gt;3001&lt;/code&gt; to this instance, hence the given network tag.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Go to Firewall page.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgim6r4jau1orkutsndod.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgim6r4jau1orkutsndod.png" alt="Firewall menu" width="441" height="381"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Now set these settings. Click &lt;strong&gt;CREATE&lt;/strong&gt; when you done.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: &lt;code&gt;allow-kuma&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Network: &lt;em&gt;choose the VPC where the Compute Engine instance exists&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Target tags: &lt;code&gt;allow-kuma&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Source IPv4 ranges: &lt;code&gt;0.0.0.0/0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Protocols and ports: Check on &lt;strong&gt;Specified protocols and ports&lt;/strong&gt;, then check &lt;strong&gt;tcp&lt;/strong&gt; and type in number &lt;code&gt;3001&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;p&gt;Just like that, our Uptime Kuma already up and ready to use. Open it through the VM's external IP address and concate with port &lt;code&gt;3001&lt;/code&gt; like so: &lt;code&gt;http://123.123.13.12:3001&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3fvr13tqvjezkhft64zi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3fvr13tqvjezkhft64zi.png" alt="Uptime is running!" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjm7q82m7b5adbm4g23e0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjm7q82m7b5adbm4g23e0.png" alt="Uptime is a go!" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, let's see our cost estimation for those configuration. We assume these variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We are using &lt;strong&gt;Standard networking tier&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;We are using Compute Engine instance that covered by &lt;a href="https://cloud.google.com/free/docs/gcp-free-tier#free-tier-usage-limits" rel="noopener noreferrer"&gt;GCP's Free Tier&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Monthly network egress comes from our instance based on per minute basis pings for 7 monitors is roughly around 0.5 GiB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can refer to this &lt;a href="https://cloud.google.com/products/calculator#id=ab713e64-ab89-4258-8fac-c7566e1c93a4" rel="noopener noreferrer"&gt;Google Cloud pricing calculator estimate link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fidxburfaim5heowfbdkb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fidxburfaim5heowfbdkb.png" alt="Cost estimate" width="444" height="994"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We are only incurred cost around &lt;strong&gt;USD 0.44&lt;/strong&gt; per month for a rich features monitoring tool! Now that's cheap 💰💰&lt;/p&gt;




&lt;p&gt;That's it for this post. Further you can explore Uptime Kuma's features and integrate it with social app like Discord. Also, it provides a customizable status page. They are pretty neat and helpful. Save and share this post if you like!&lt;/p&gt;

</description>
      <category>uptime</category>
      <category>deploy</category>
      <category>googlecloud</category>
      <category>docker</category>
    </item>
    <item>
      <title>Deploy Laravel 8 to Compute Engine instance (LEMP stack)</title>
      <dc:creator>Andre Kho</dc:creator>
      <pubDate>Fri, 03 Dec 2021 05:33:25 +0000</pubDate>
      <link>https://dev.to/nekoto/deploy-laravel-8-to-compute-engine-instance-lemp-stack-280g</link>
      <guid>https://dev.to/nekoto/deploy-laravel-8-to-compute-engine-instance-lemp-stack-280g</guid>
      <description>&lt;p&gt;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:&lt;/p&gt;

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

&lt;p&gt;Now, let's get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create a Compute Engine instance
&lt;/h2&gt;

&lt;p&gt;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 &lt;strong&gt;CREATE INSTANCE&lt;/strong&gt; button as shown below.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3kmdmfxivteba0q1jqt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3kmdmfxivteba0q1jqt.png" alt="Compute Engine main page" width="543" height="197"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Then in create instance form, fill and set properties for our instance. My configuration goes like these:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4xc8ybt5kn1spw3m3k6s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4xc8ybt5kn1spw3m3k6s.png" alt="Compute Engine instance creation configuration part 1/6" width="436" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choose your instance name wisely as it can't be changed later after creation.&lt;/li&gt;
&lt;li&gt;Pick your nearest region and zone for lower latency.&lt;/li&gt;
&lt;li&gt;Choose lowest possible machine class and type to avoid huge charge to your bill, even if it's just for trial use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcrzh8yzjb6hs6alzxh0s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcrzh8yzjb6hs6alzxh0s.png" alt="Compute Engine instance creation configuration part 2/6" width="463" height="308"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On boot disk configuration, choose your most familiar operating system. If it's required, you may choose latest LTS version.&lt;/li&gt;
&lt;li&gt;Choose &lt;code&gt;SSD persistent disk&lt;/code&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6c56auratycd00987pl9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6c56auratycd00987pl9.png" alt="Compute Engine instance creation configuration part 3/6" width="430" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enable all HTTP and HTTPS access, just in case you need to configure SSL certificate for your instance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyf2o468pi82kkmc31vc9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyf2o468pi82kkmc31vc9.png" alt="Compute Engine instance creation configuration part 4/6" width="429" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On advanced configuration, the Network section, choose your existing VPC network. (I already have a custom one called &lt;code&gt;local-vpc&lt;/code&gt;. You may already have the default one.)&lt;/li&gt;
&lt;li&gt;Set a static external IP address as shown below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7kugo5i7fjvdpksnrles.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7kugo5i7fjvdpksnrles.png" alt="Compute Engine instance creation configuration part 5/6" width="420" height="274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg62hw7rzwqkrqo1v1049.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg62hw7rzwqkrqo1v1049.png" alt="Compute Engine instance creation configuration part 6/6" width="427" height="341"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On advanced configuration, the Security section, enable Shielded VM for increased security to your instance.&lt;/li&gt;
&lt;li&gt;You may want to add a &lt;strong&gt;Public SSH key&lt;/strong&gt; in this section, so you can access your instance securely from your workstation or PC.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click on &lt;strong&gt;CREATE&lt;/strong&gt; button to create the instance.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 2: Install and configure server environments
&lt;/h2&gt;

&lt;p&gt;Now your instance already up and running. We need to install our necessary environments for our app.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F91js5z1ep9ws4cidexmx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F91js5z1ep9ws4cidexmx.png" alt="Created VM" width="800" height="18"&gt;&lt;/a&gt;&lt;br&gt;
Click on SSH button (or run &lt;code&gt;ssh&lt;/code&gt; command from your PC that access this VM's external IP address)&lt;/p&gt;

&lt;p&gt;Now first in your SSH terminal, run these commands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set your timezone
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;timedatectl set-timezone Asia/Jakarta
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Check for any updates
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Do upgrade according to updates
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;(Optional) Do reboot the instance
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;At this point, access our instance once again and we will install our environments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install NGINX
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Install MariaDB server. At this point, follow the instruction to make our MariaDB server secure. You'll do the following:

&lt;ul&gt;
&lt;li&gt;Authenticate as root user (just press enter to move forward)&lt;/li&gt;
&lt;li&gt;Set new root password (remember the password you entered here!)&lt;/li&gt;
&lt;li&gt;Accept everything (don't forget to read the instruction, though)
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;mariadb-server
&lt;span class="nb"&gt;sudo &lt;/span&gt;mysql_secure_installation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Create a new database and database user for our app
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mysql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql&amp;gt; CREATE DATABASE &lt;span class="s1"&gt;'mydb'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
mysql&amp;gt; CREATE USER &lt;span class="s1"&gt;'mymy'&lt;/span&gt;@&lt;span class="s1"&gt;'localhost'&lt;/span&gt; IDENTIFIED BY &lt;span class="s1"&gt;'myPassw0rd'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
mysql&amp;gt; GRANT ALL ON &lt;span class="s1"&gt;'mydb'&lt;/span&gt;.&lt;span class="k"&gt;*&lt;/span&gt; TO &lt;span class="s1"&gt;'mymy'&lt;/span&gt;@&lt;span class="s1"&gt;'localhost'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
mysql&amp;gt; FLUSH PRIVILEGES&lt;span class="p"&gt;;&lt;/span&gt;
mysql&amp;gt; EXIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Install &lt;code&gt;unzip&lt;/code&gt; so our Composer can do its work when installing dependencies
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;unzip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Add PPA repository for NGINX and PHP
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;add-apt-repository ppa:ondrej/php &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;add-apt-repository ppa:ondrej/nginx &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Install PHP-FPM and some 'necessary' PHP modules (refer to Laravel official docs anyway)
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; 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
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Check PHP CLI version with &lt;code&gt;php -v&lt;/code&gt; 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.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;update-alternatives &lt;span class="nt"&gt;--config&lt;/span&gt; php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Create an NGINX config file under &lt;code&gt;/etc/nginx/sites-available/&lt;/code&gt; directory. Name the config file either with a domain name you have or just give it &lt;code&gt;main&lt;/code&gt; or anything you want. I'll go with example.com.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/nginx/sites-available/example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This will open a NANO text editor ready to create the file. Copy and paste the code below to the editor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;# Use your domain/subdomain name here&lt;/span&gt;
  &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;# We will clone our "repo-name" repository which&lt;/span&gt;
  &lt;span class="c1"&gt;# contains our Laravel project to /var/www/ directory later.&lt;/span&gt;
  &lt;span class="c1"&gt;# Change this path according to your repository name&lt;/span&gt;
  &lt;span class="c1"&gt;# and end with /public.&lt;/span&gt;
  &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/var/www/repo-name/public&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kn"&gt;index&lt;/span&gt; &lt;span class="s"&gt;index.php&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Frame-Options&lt;/span&gt; &lt;span class="s"&gt;"SAMEORIGIN"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span class="s"&gt;"nosniff"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kn"&gt;charset&lt;/span&gt; &lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="n"&gt;/index.php?&lt;/span&gt;&lt;span class="nv"&gt;$query_string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/favicon.ico&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;log_not_found&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/robots.txt&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;log_not_found&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kn"&gt;error_page&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt; &lt;span class="n"&gt;/index.php&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;\.php$&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;fastcgi_pass&lt;/span&gt; &lt;span class="s"&gt;unix:/var/run/php/php7.4-fpm.sock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;fastcgi_param&lt;/span&gt; &lt;span class="s"&gt;SCRIPT_FILENAME&lt;/span&gt; &lt;span class="nv"&gt;$realpath_root$fastcgi_script_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="s"&gt;fastcgi_params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;/\.(?!well-known).*&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# The access log file name is up to you.&lt;/span&gt;
  &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="n"&gt;/var/log/nginx/access_laravel.log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;# And this error log file name as well is up to you.&lt;/span&gt;
  &lt;span class="kn"&gt;error_log&lt;/span&gt; &lt;span class="n"&gt;/var/log/nginx/error_laravel.log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a symbolic link to the config file in &lt;code&gt;/etc/nginx/sites-enabled/&lt;/code&gt; directory, so NGINX can read our file from there.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Unlink existing default configuration
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo unlink&lt;/span&gt; /etc/nginx/sites-enabled/default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Test our NGINX configuration
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;If no errors come up, restart the NGINX service to load our new configuration
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;service nginx reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we will install additional environments for our Laravel app.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install Node Version Manager (NVM) to run npm installation in the project later.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-o-&lt;/span&gt; https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
&lt;span class="nb"&gt;source&lt;/span&gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Install NodeJS latest LTS version. Use &lt;code&gt;nvm list-remote&lt;/code&gt; to list Node versions (current latest LTS version is Gallium)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nvm &lt;span class="nb"&gt;install &lt;/span&gt;lts/gallium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;You may want to check the installed version of Node and NPM by running these commands.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;-v&lt;/span&gt;
npm &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Install Composer &lt;strong&gt;(Required)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; https://getcomposer.org/installer &lt;span class="nt"&gt;-o&lt;/span&gt; composer-setup.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;HASH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; https://composer.github.io/installer.sig&lt;span class="sb"&gt;`&lt;/span&gt;
php &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"if (hash_file('SHA384', 'composer-setup.php') === '&lt;/span&gt;&lt;span class="nv"&gt;$HASH&lt;/span&gt;&lt;span class="s2"&gt;') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;php composer-setup.php &lt;span class="nt"&gt;--install-dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/local/bin &lt;span class="nt"&gt;--filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;composer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3: Pull your app from a remote repository
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;First, we need to generate a new SSH key pair. Please refer to &lt;a href="https://www.ssh.com/academy/ssh/keygen" rel="noopener noreferrer"&gt;this site&lt;/a&gt; to learn about how to use SSH.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; rsa &lt;span class="nt"&gt;-b&lt;/span&gt; 4096 &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;your-Github-email&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure change "your-Github-email" to &lt;strong&gt;your actual email address used in Github&lt;/strong&gt; (it's not mandatory, to be honest. You can fill anything, but email is recommended as it's easier to identify).&lt;br&gt;
You'll be asked file name for the key. You may just press &lt;strong&gt;Enter&lt;/strong&gt; to use default file name.&lt;br&gt;
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 &lt;strong&gt;Enter&lt;/strong&gt; twice so you won't use any passphrase.&lt;br&gt;
Finally, the SSH key pair is generated and stored within &lt;strong&gt;.ssh folder in current user directory&lt;/strong&gt; (&lt;code&gt;~/.ssh/&lt;/code&gt;). There will be two files: one with &lt;code&gt;.pub&lt;/code&gt; extension is called &lt;strong&gt;public key&lt;/strong&gt;, the other one is called &lt;strong&gt;private key&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Next step, we will put our &lt;strong&gt;public key&lt;/strong&gt; 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).&lt;br&gt;
Copy our &lt;strong&gt;public key&lt;/strong&gt; content which usually begins with &lt;code&gt;ssh-rsa xxxx&lt;/code&gt;.&lt;br&gt;
Then, go to your &lt;a href="https://github.com/settings/keys" rel="noopener noreferrer"&gt;Github account settings page &amp;gt; SSH and GPG keys&lt;/a&gt;. Click on &lt;strong&gt;New SSH key&lt;/strong&gt; button. Type a name for the key (anything but noticeable) and paste the public key in provided text area. Click on &lt;strong&gt;Add new key&lt;/strong&gt; button.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvh5utvxqy48uf1ejij2e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvh5utvxqy48uf1ejij2e.png" alt="Add SSH key to Github" width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, we can start cloning our repo from Github securely. Here are the steps.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;(Required)&lt;/strong&gt; Secure our SSH private key by changing file permission to &lt;strong&gt;read-only by its owner (current user who creates the key pair&lt;/strong&gt;.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;400 .ssh/id_rsa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Activate &lt;code&gt;ssh-agent&lt;/code&gt; so we can start use SSH.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;ssh-agent &lt;span class="nt"&gt;-s&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Add our &lt;strong&gt;private key&lt;/strong&gt; to the SSH agent. Remember your key file name.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-add id_rsa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Test whether we can connect to Github via SSH. Type &lt;code&gt;yes&lt;/code&gt; and press Enter to confirm connection when being asked.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-T&lt;/span&gt; git@github.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;If it returns a message like: "&lt;code&gt;Hi, &amp;lt;user&amp;gt;! You've successfully authenticated, but GitHub does not provide shell access.&lt;/code&gt;", then it's successful connection! We can continue to clone our repo to &lt;code&gt;/var/www&lt;/code&gt; directory by running this command (change to your username and repository name accordingly).
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /var/www/
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nv"&gt;$USER&lt;/span&gt;:&lt;span class="nv"&gt;$USER&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
git clone git@github.com:&amp;lt;your-Github-username&amp;gt;/&amp;lt;your-repo-name&amp;gt;
&lt;span class="nb"&gt;sudo chown &lt;/span&gt;root:root &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;What I'm doing upthere is changing ownership of &lt;code&gt;/var/www&lt;/code&gt; folder to current user, so we can run &lt;code&gt;git clone&lt;/code&gt; command as current user. Thus, our cloned repo will be owned by it. Then, we return the ownership of &lt;code&gt;/var/www&lt;/code&gt; directory back to &lt;code&gt;root&lt;/code&gt; user (not everything inside it). Pay attention to &lt;code&gt;cd&lt;/code&gt; command.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 4: Install Laravel dependencies and configure project environment variables
&lt;/h2&gt;

&lt;p&gt;At this point, we already have our cloned project inside &lt;code&gt;/var/www&lt;/code&gt; directory. Make sure you are inside the project directory by using &lt;code&gt;cd&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;First thing is to create a &lt;code&gt;.env&lt;/code&gt; file by copying from &lt;code&gt;.env.example&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, open and edit the copied &lt;code&gt;.env&lt;/code&gt; file with NANO editor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change respective environment variable values to suit your requirements. For my case, I'll go with updating these values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APP_ENV=production
APP_DEBUG=false
DB_DATABASE=my-db
DB_USERNAME=mymy
DB_PASSWORD=myPassw0rd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember your database user, its password, and the database whom its database user can access (refer to Step 2). Press &lt;strong&gt;Ctrl + X&lt;/strong&gt;, then &lt;strong&gt;Y&lt;/strong&gt;, and finally press &lt;strong&gt;Enter&lt;/strong&gt; to save.&lt;/p&gt;

&lt;p&gt;We will install all Laravel modules and its dependencies by running these commands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--optimize-autoloader&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt;
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Moving forward, we need to generate application key which is mandatory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan key:generate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next we will create a symbolic link for our &lt;code&gt;/storage&lt;/code&gt; folder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan storage:link
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One more step, we can migrate our database. You may skip this if you are actually not using database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan migrate:refresh &lt;span class="nt"&gt;--seed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we need to change ownership for &lt;code&gt;www-data&lt;/code&gt; user (web server user) so it can access and serve our app. Change &lt;code&gt;repo-name&lt;/code&gt; to your actual repository or folder name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; www-data:www-data /var/www/repo-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are many alternatives to this. One way, you can add &lt;code&gt;www-data&lt;/code&gt; user to current user group and vice versa.&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi2foowghl9h0i6xlkigt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi2foowghl9h0i6xlkigt.png" alt="Final view" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;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).&lt;br&gt;
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.&lt;/p&gt;




&lt;p&gt;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 🤗&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>nginx</category>
      <category>laravel</category>
      <category>deploy</category>
    </item>
    <item>
      <title>Guide to Setup a Moodle website in Google Cloud Platform (GCP) with HTTPS</title>
      <dc:creator>Andre Kho</dc:creator>
      <pubDate>Fri, 25 Jun 2021 18:14:14 +0000</pubDate>
      <link>https://dev.to/nekoto/guide-to-setup-a-moodle-website-in-google-cloud-platform-gcp-with-https-2f63</link>
      <guid>https://dev.to/nekoto/guide-to-setup-a-moodle-website-in-google-cloud-platform-gcp-with-https-2f63</guid>
      <description>&lt;p&gt;&lt;strong&gt;Moodle&lt;/strong&gt; has been great for supporting many schools, campuses, and even companies in digitalized education system. Moodle is quite stable to be deployed by IT teams or individual for their organizations, simple to customized. It can be deployed offline or online depending on needs and situation. There are &lt;em&gt;many third-party software installers&lt;/em&gt; that can ship and deploy Moodle almost instantly, most of them are &lt;em&gt;managed by another third-party hosting services&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;While it's pretty quick and simple to start, but at some point it's becoming difficult to maintain and keep up-to-date. We may &lt;strong&gt;not have a full control of the server&lt;/strong&gt; that hosts our Moodle, hence we can't setup some new requirements needed or do advanced tinkering. We may &lt;strong&gt;overprovision resources&lt;/strong&gt; that host the Moodle application, when most of the time it's always in &lt;strong&gt;underutilization&lt;/strong&gt;. We also may &lt;strong&gt;have corrupted Moodle installation&lt;/strong&gt; from those third-party tools, that we don't notice at first but then it's showing everywhere. We also may forget at some point to check &lt;strong&gt;whether our website is using a secure protocol (HTTPS) or not&lt;/strong&gt;. Yeah, lot of risks, but it's not the end of the world.&lt;/p&gt;

&lt;p&gt;In this post, I will guide you &lt;strong&gt;how to setup a Moodle LMS website hosted in a Compute Engine (CE) instance in Google Cloud Platform (GCP)&lt;/strong&gt;. The application will be secured using free and open source SSL certificate service so we don't have to pay if we don't want to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Post Updates
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;30 June 2021: Modify cron steps as we only setup using PHP 7.4 and add opinion about scaling Moodle&lt;/li&gt;
&lt;li&gt;29 June 2021: Add more reference and set server time zone&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Assumptions 🤔
&lt;/h2&gt;

&lt;p&gt;Below is the list of environment and app version used in this guide (at the time this post out):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Moodle 3.11+&lt;/li&gt;
&lt;li&gt;PHP 7.4.20 and 8.0&lt;/li&gt;
&lt;li&gt;MySQL 8&lt;/li&gt;
&lt;li&gt;Ubuntu 20.04 LTS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While it's based on my experience, please note that you may adjust some steps to suit your needs. Another disclaimer is that the steps I'm going to show can be subjective, so I make some assumptions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You already have an active Billing Account set for your organization (or your personal Google Account).&lt;/li&gt;
&lt;li&gt;You are already prepared to pay for using GCP resources and you aware that pay-per-use model needs extensive monitoring of resource usage to avoid overpricing.&lt;/li&gt;
&lt;li&gt;You already have a DNS Records management set elsewhere outside GCP (eg. cPanel user account).&lt;/li&gt;
&lt;li&gt;You already have control of a school/campus domain name.&lt;/li&gt;
&lt;li&gt;You are trying to setup your Moodle LMS to be accessible on a subdomain or domain.&lt;/li&gt;
&lt;li&gt;Your user size won't be too big to handle, hopefully (approx. 10-100 concurrent users).&lt;/li&gt;
&lt;li&gt;You already have familiar knowledge of GCP, their policies, and techniques.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Overview 📑
&lt;/h2&gt;

&lt;p&gt;There are 5 main steps to setup everything, listed as below:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Provision a Compute Engine instance&lt;/li&gt;
&lt;li&gt;Install server environments&lt;/li&gt;
&lt;li&gt;Make our subdomain/domain secure&lt;/li&gt;
&lt;li&gt;Get Moodle through Git&lt;/li&gt;
&lt;li&gt;Setup Moodle specific configurations&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 1: Provision a Compute Engine instance &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;First things first, you have to &lt;strong&gt;create a CE instance in GCP&lt;/strong&gt;. It's pretty easy and straightforward. You may want to use &lt;code&gt;gcloud shell&lt;/code&gt; commands, but I prefer to use the Cloud Console web interface.&lt;/p&gt;

&lt;p&gt;As for initial setup, you can start with as low as below specifications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Region/Zone: choose the &lt;strong&gt;nearest possible one&lt;/strong&gt; from your place (the price may be higher, but it provides lower latency). Our instance is &lt;strong&gt;zonal resource&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Machine configuration:

&lt;ul&gt;
&lt;li&gt;Machine family: &lt;strong&gt;General Purpose&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Series: &lt;strong&gt;E2&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Machine type: &lt;strong&gt;e2-micro&lt;/strong&gt; (1 shared-core up to 2 vCPUs and 1 GB memory which only costs around &lt;strong&gt;US$11.14 per month on full run&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Boot disk:

&lt;ul&gt;
&lt;li&gt;Operating system: &lt;strong&gt;Ubuntu&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Version: &lt;strong&gt;latest LTS possible&lt;/strong&gt; (choose 20.04 LTS as of the time I wrote this post)&lt;/li&gt;
&lt;li&gt;Boot disk type: &lt;strong&gt;SSD persistent disk&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Size: &lt;strong&gt;10 GB&lt;/strong&gt; (only costs around &lt;strong&gt;US$2.21 per month&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Firewall: &lt;strong&gt;Check both Allow HTTP traffic and Allow HTTPS traffic&lt;/strong&gt;
&lt;/li&gt;

&lt;li&gt;Under &lt;strong&gt;Networking&lt;/strong&gt; tab in advance configuration, click on one of the &lt;strong&gt;Network interfaces&lt;/strong&gt; (should be &lt;strong&gt;default&lt;/strong&gt; at first). You may want to use &lt;strong&gt;Static External IP instead of Ephemeral&lt;/strong&gt;.&lt;/li&gt;

&lt;li&gt;(Optional) SSH keys: Provide your public SSH key(s) generated from your PC. Make sure your private SSH key(s) is secured in your user directory. Although it's optional, but I use this to provide access to the VM directly from my Terminal without bother opening Google Cloud Console in browser.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Noted that this specification is the lowest possible one and hopefully cost-effective. You may choose higher resources, but &lt;strong&gt;it's a good thing to start small when you are working in the cloud&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Install server environments &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Please make note of your &lt;strong&gt;instance's external IP address&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you choose to provide SSH key(s) to the instance, then you can open your terminal and try to connect through it with this command (I'm using Windows Terminal btw):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &amp;lt;your_username&amp;gt;@&amp;lt;external_ip&amp;gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &amp;lt;full/path/to/your/private/keys&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Otherwise, you can click on SSH button inline with instance name in &lt;strong&gt;Compute Engine &amp;gt; VM Instances table&lt;/strong&gt;. This will open a new browser window that showing terminal access to the instance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set server time zone
&lt;/h3&gt;

&lt;p&gt;Before we continue further, we need to make sure that our server time zone matches with ours. We can check via below command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;timedatectl status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it shows different time zone than yours, then you need to change it. We can check available time zones with below command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;timedatectl list-timezones
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find your time zone (mine is Asia/Jakarta, which is UTC+7), and then execute command below to set your time zone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;timedatectl set-timezone &amp;lt;your &lt;span class="nb"&gt;time &lt;/span&gt;zone&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, I reboot the machine, although the server's time zone already changed. Just to make sure, of course.&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment preparations
&lt;/h3&gt;

&lt;p&gt;Now we are going to install environments needed, based on the official guide for installing Moodle in Ubuntu (&lt;a href="https://docs.moodle.org/311/en/Step-by-step_Installation_Guide_for_Ubuntu" rel="noopener noreferrer"&gt;check it here&lt;/a&gt;). I will show you here anyway.&lt;/p&gt;

&lt;p&gt;Update Ubuntu components&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add apt repository to track updates for PHP&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;add-apt-repository ppa:ondrej/php
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install Apache and MySQL&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;apache2 mysql-client mysql-server php7.4 libapache2-mod-php7.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run MySQL secure installation. You'll be asked for a new ROOT password using options of validation (choose 1 = MEDIUM). Then you'll just confirm following questions with Y (yes). This has to be done to make your MySQL server as secure as possible by disabling remote root access and provide strict validation when creating passwords.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mysql_secure_installation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install additional software or dependencies. Please be aware that we are going to install components for PHP 7.4.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;graphviz aspell ghostscript php7.4-pspell php7.4-curl php7.4-gd php7.4-intl php7.4-mysql php7.4-xml php7.4-xmlrpc php7.4-ldap php7.4-zip php7.4-soap php7.4-mbstring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Apache server to make sure all components are loaded successfully.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;service apache2 restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install Git (for Step 4).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check listening ports
&lt;/h3&gt;

&lt;p&gt;We need to check whether port 80 (not secure) and 443 (secure) is listening from your machine by running this command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ss &lt;span class="nt"&gt;-tulwn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If port 443 is not listed, we need to install ssl module for Apache.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;a2enmod ssl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Apache once again&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;service apache2 restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now check again all listening ports. Port 443 should be listed as LISTEN.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ss &lt;span class="nt"&gt;-tulwn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Make our subdomain/domain secure &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Now we need to make sure that our subdomain (or main domain) uses HTTPS. That means, we need an SSL certificate. We will use the free way to obtain that.&lt;/p&gt;

&lt;p&gt;First, in your DNS records manager, &lt;strong&gt;add or modify two A records&lt;/strong&gt; just like below. By doing this, we are going to make sure that our subdomain/domain will pinpoint to your machine external IP address. In order (Name, TTL, Type, Record):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;example.com, 14400, A, (your external IP)&lt;/li&gt;
&lt;li&gt;
&lt;a href="http://www.example.com" rel="noopener noreferrer"&gt;www.example.com&lt;/a&gt;., 14400, A, (your external IP)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then, we are going to install &lt;strong&gt;Certbot&lt;/strong&gt;, &lt;strong&gt;a free&lt;/strong&gt;, open source software tool for automatically using &lt;strong&gt;Let’s Encrypt&lt;/strong&gt; certificates on manually-administrated websites to enable HTTPS (&lt;a href="https://certbot.eff.org/" rel="noopener noreferrer"&gt;check it here&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;You can follow the specific instructions through &lt;a href="https://certbot.eff.org/lets-encrypt/ubuntufocal-apache" rel="noopener noreferrer"&gt;this link&lt;/a&gt; for our case (Ubuntu 20.04 and Apache).&lt;/p&gt;

&lt;p&gt;If you follow all the Certbot instructions and get a successful result, then you should be able to access your subdomain/domain using HTTPS. Now that's easy and under your control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Get Moodle through Git &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Git is what is called a "version control system". By using git, it will much easier down the road to update the Moodle core application. We will use /opt directory for this installation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Download the Moodle code and index.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;git clone git://git.moodle.org/moodle.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠ &lt;strong&gt;Warning&lt;/strong&gt;: Because we are using &lt;strong&gt;e2-micro&lt;/strong&gt; (as I was), this command will somehow make our machine disk write and read to be at peak for several minutes (if not hours) and your SSH access might be interrupted because of timeout. Take your time, sip a cup of tea ☕. It will be finished anytime soon.&lt;/p&gt;

&lt;p&gt;Then, change directory into the downloaded Moodle folder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;moodle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Retrieve a list of each branch available. Press Q to quit seeing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;git branch &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Corresponding with Moodle convention on branch naming, we will use the latest Moodle version (as the time of writing, it's Moodle 3.11). Tell git which branch to track or use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;git branch &lt;span class="nt"&gt;--track&lt;/span&gt; MOODLE_311_STABLE origin/MOODLE_311_STABLE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check out the Moodle version specified.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;git checkout MOODLE_311_STABLE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Setup Moodle specific configurations &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Copy installation to web root
&lt;/h3&gt;

&lt;p&gt;Now we have our Moodle installation, we need to copy that to the webroot (it's usually under &lt;code&gt;/var/www/html&lt;/code&gt;). Then we need to create &lt;code&gt;moodledata&lt;/code&gt; directory to store Moodle-generated files. We should change both directories' permissions accordingly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; /opt/moodle /var/www/html/
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; /var/moodledata
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; www-data /var/moodledata
&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; 777 /var/moodledata
&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; 0755 /var/www/html/moodle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Change web root entrypoint
&lt;/h3&gt;

&lt;p&gt;At this point, we should have PHP 7.4 and MySQL 8 running. And also, our Moodle application is now accessible through &lt;code&gt;https://(your domain)/moodle&lt;/code&gt;. This is quite annoying. We need to change the web root config so that our user can access through &lt;code&gt;https://(your domain)&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/apache2/sites-available/000-default.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now change in the line of &lt;code&gt;DocumentRoot /var/www/html&lt;/code&gt; to &lt;code&gt;DocumentRoot /var/www/html/moodle&lt;/code&gt;. Save the file by presing Ctrl+X &amp;gt; confirm with Y &amp;gt; Enter to save with default file name (we are using Nano text editor).&lt;/p&gt;

&lt;p&gt;Restart the Apache server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;service apache2 restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create new database and user
&lt;/h3&gt;

&lt;p&gt;We need to create a user for our Moodle database. Open MySQL as root, use your MySQL root password from Step 2.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we are in MySQL shell.&lt;br&gt;
Create database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql&amp;gt; CREATE DATABASE moodle DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create user. Use your username and password of your choice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql&amp;gt; CREATE USER &lt;span class="s1"&gt;'moodledude'&lt;/span&gt;@&lt;span class="s1"&gt;'localhost'&lt;/span&gt; IDENTIFIED BY &lt;span class="s1"&gt;'passwordformoodledude'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Grant some privileges to the user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql&amp;gt; GRANT SELECT,INSERT,UPDATE,DELETE,CREATE,CREATE TEMPORARY TABLES,DROP,INDEX,ALTER ON moodle.&lt;span class="k"&gt;*&lt;/span&gt; TO &lt;span class="s1"&gt;'moodledude'&lt;/span&gt;@&lt;span class="s1"&gt;'localhost'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quit the shell.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql&amp;gt; quit&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Update opcache and some PHP configuration
&lt;/h3&gt;

&lt;p&gt;Before we continue, we should update some opcache and PHP configuration according to Moodle docs. Please review the required settings for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.moodle.org/311/en/OPcache#Configuration" rel="noopener noreferrer"&gt;opcache configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.moodle.org/311/en/PHP" rel="noopener noreferrer"&gt;PHP configuration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For &lt;strong&gt;opcache config file&lt;/strong&gt;, you can open from this location (use &lt;code&gt;ls&lt;/code&gt; to check opcache.ini name file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /etc/php/7.4/apache2/conf.d/
&lt;span class="nb"&gt;ls
sudo &lt;/span&gt;nano 10-opcache.ini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;strong&gt;PHP config file&lt;/strong&gt;, you can open from this location:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /etc/php/7.4/apache2/
&lt;span class="nb"&gt;ls
sudo &lt;/span&gt;nano php.ini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please update all required settings before we continue. Note that from my case, I don't need to uncomment extension sections in &lt;code&gt;php.ini&lt;/code&gt;. They are already enabled since component installation at Step 2.&lt;/p&gt;

&lt;p&gt;After you finished, restart the Apache server to load the new settings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;service apache2 restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Continue installation from web interface
&lt;/h3&gt;

&lt;p&gt;Now we can continue our Moodle installation from its web interface. Before that, change the permissions on &lt;code&gt;/var/www/html/moodle&lt;/code&gt; directory so the installation can succeed smoothly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; 777 /var/www/html/moodle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open your browser and access &lt;code&gt;https://(your domain)&lt;/code&gt; and follow the prompts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Change path of moodledata to: &lt;strong&gt;/var/moodledata&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Change database type to: &lt;strong&gt;mysqli&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Change database settings:

&lt;ul&gt;
&lt;li&gt;Host server: &lt;strong&gt;localhost&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Database: &lt;strong&gt;moodle&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;User: &lt;strong&gt;moodledude&lt;/strong&gt; (the user you created when setting up the database)&lt;/li&gt;
&lt;li&gt;Password: &lt;strong&gt;passwordformoodledude&lt;/strong&gt; (the password for the user you created)&lt;/li&gt;
&lt;li&gt;Tables Prefix: &lt;strong&gt;mdl_&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Environment checks. At this point every thing should be in green &lt;strong&gt;OK&lt;/strong&gt;. If there are any warnings, let me know.&lt;/li&gt;

&lt;li&gt;Next and next and confirm installations.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;At this point, installation should finished successfully (just wait). Follow next prompts for creating Admin user and website configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Change system path
&lt;/h3&gt;

&lt;p&gt;Now Moodle is already up and running. We need to update some little configuration a little more.&lt;br&gt;
Navigate to &lt;strong&gt;Site Administration &amp;gt; Server &amp;gt; System Paths&lt;/strong&gt;. Input path settings like the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Path to du: &lt;strong&gt;/usr/bin/du&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Path to aspell: &lt;strong&gt;/usr/bin/aspell&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Path to dot: &lt;strong&gt;/usr/bin/dot&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save your changes.&lt;/p&gt;
&lt;h3&gt;
  
  
  Setup cron job for Moodle
&lt;/h3&gt;

&lt;p&gt;As required by Moodle, we need to setup a cron job (as mentioned in &lt;a href="https://docs.moodle.org/311/en/Cron" rel="noopener noreferrer"&gt;this documentation&lt;/a&gt;). We can check the PHP CLI configuration using below command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php &lt;span class="nt"&gt;-i&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;php.ini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure that the PHP version used by CLI is the same with our initial installation, which is 7.4.&lt;/p&gt;

&lt;p&gt;Now we add our cron job by using crontab.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;crontab &lt;span class="nt"&gt;-u&lt;/span&gt; www-data &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first, you'll be asked which editor to use. Choose nano. Then, add the following at the end of the opened file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* * * * * /usr/bin/php  /var/www/html/moodle/admin/cli/cron.php &amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save your changes. Now our Moodle has its cron job running every 1 minute.&lt;/p&gt;

&lt;h3&gt;
  
  
  Don't forget to revert Moodle directory permissions
&lt;/h3&gt;

&lt;p&gt;Run this command to revert &lt;code&gt;/var/www/html/moodle&lt;/code&gt; permissions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; 0755 /var/www/html/moodle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Monitor the usage
&lt;/h3&gt;

&lt;p&gt;Don't forget to monitor the instance usage and cost. &lt;strong&gt;Simple but important&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Known Issues 🤯
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If you try to access VM directories via Visual Studio Code, you may encounter lag and sometimes timeout. In my case, the disk IOPS and I/O usage is full for several minutes and hours when the issue happens. Do you have any ideas on this?
&lt;strong&gt;Solution&lt;/strong&gt;: We have to scale up our machine type.&lt;/li&gt;
&lt;li&gt;Certbot may fail to give you SSL certificate if you don't properly setup DNS records and open port 443.&lt;/li&gt;
&lt;li&gt;Certbot may fail to renew SSL certificate on first check, even we already have a proper setup. Try executing the certbot command again.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next steps ⏩
&lt;/h2&gt;

&lt;p&gt;Below is the list of what you can do further:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start installing plugins that suit your needs.&lt;/li&gt;
&lt;li&gt;Scale up your instance by turning it off first then change its machine type and disk size. Restart it whenever you are ready.&lt;/li&gt;
&lt;li&gt;Setup specific Firewall policies for your subnetworks.&lt;/li&gt;
&lt;li&gt;Avoid directory ownership mismanagement. You don't want to accidentally allow unintended access and authorization that can alter your Moodle source.&lt;/li&gt;
&lt;li&gt;Use external database authentication and enrolment plugins to speed up administration routines. I will cover this in the upcoming post.&lt;/li&gt;
&lt;li&gt;If you follow Moodle installation steps from official docs, pay attention on &lt;code&gt;clamav&lt;/code&gt; component. This antivirus will cause hourly huge spikes in CPU utilization to process virus database. You may want to scale up your instance if you want to make use of it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Opinion 💭
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;There is "official" Docker image of Moodle, but it's solely used for development purposes, not production.&lt;/li&gt;
&lt;li&gt;You may think of using Kubernetes architecture to get autohealing and autoscaling features, but that may bring a lot of technical difficulties to setup. Always start simple.&lt;/li&gt;
&lt;li&gt;As stated by Moodle docs, Moodle LMS itself can be clustered (or at least scaled out). However, it's not necessary and will require complex architecture work to do so. Instead, Moodle LMS can be scaled up by just increasing memory, CPU, and disk. In this way, of course, Moodle is at higher risk of downtime since our single VM only exists in a single zone in a region.&lt;/li&gt;
&lt;li&gt;Actually, there is a way to separate Moodle source app, moodledata, and the database so it can be scaled out. But that will incur higher cost than we expected. For example, GCP itself allows use of Cloud Storage to be mounted for VMs using FUSE. The idea is to use it as a place for moodledata. But we will be charged with Cloud Storage pricing plus its performance may not significantly sufficient for Moodle to store and process its data (lower performance than SSD and limited concurency capability).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion 🏁
&lt;/h2&gt;

&lt;p&gt;That is a hell lot of steps to install Moodle in GCP. But, we already established our very own Moodle installation. &lt;strong&gt;We have full control over resources and that is the main point and a good thing!&lt;/strong&gt; We only pay for what we use (or provision to be exact), we can make use of cloud flexibility to scale up or scale down, and we can integrate with other features GCP offers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading 📚
&lt;/h2&gt;

&lt;p&gt;Please review these articles to enrich your knowledge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.moodle.org" rel="noopener noreferrer"&gt;Moodle Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.moodle.org/311/en/Performance_recommendations" rel="noopener noreferrer"&gt;Moodle Performance Recommendations as of 3.11&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/docs" rel="noopener noreferrer"&gt;Google Cloud Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/products/calculator" rel="noopener noreferrer"&gt;Google Cloud Pricing Calculator&lt;/a&gt; (yeah, it's not a reading, but it's also important tool to estimate your cloud cost in GCP)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📣 Please leave a comment, suggestions, or any feedback on this matter. If you found this post useful, please share to others and make people happy.&lt;br&gt;
😉 If you need further assistance, I'm happy to help you. Contact me anytime.&lt;br&gt;
😎 If you need a hand for your related projects, I'm also happy to collaborate. Contact me then. Quick!&lt;/p&gt;

&lt;p&gt;Thank you for reading this lengthy post. I appreciate that a lot 😊&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>moodle</category>
      <category>cloud</category>
      <category>setup</category>
    </item>
    <item>
      <title>My Moodle Tips #3</title>
      <dc:creator>Andre Kho</dc:creator>
      <pubDate>Wed, 12 May 2021 05:37:36 +0000</pubDate>
      <link>https://dev.to/nekoto/my-moodle-tips-3-4n44</link>
      <guid>https://dev.to/nekoto/my-moodle-tips-3-4n44</guid>
      <description>&lt;p&gt;Do full cleanup for every completed courses by your users (regularly or on demand). They usually don't aware of your limited resources.&lt;/p&gt;

&lt;p&gt;From my perspective, you may need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to a &lt;strong&gt;Course settings&lt;/strong&gt; &amp;gt; &lt;strong&gt;Reset course&lt;/strong&gt; &amp;gt; &lt;strong&gt;Check all&lt;/strong&gt; course module categories and options &lt;em&gt;that considerably consume too many resources&lt;/em&gt; &amp;gt; &lt;strong&gt;Reset course&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Unenroll inactive users. If you have external database enrolments and/or authenticated users, you may want to delete all associated user's data that not currently active from those tables. Make sure the related Moodle CRON jobs are active for this case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Be aware of these course resources! (your case may be different)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Assignments&lt;/li&gt;
&lt;li&gt;Files/Folders&lt;/li&gt;
&lt;li&gt;Quizzes&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>moodle</category>
      <category>lms</category>
      <category>administration</category>
      <category>php</category>
    </item>
    <item>
      <title>My Moodle Tips #2</title>
      <dc:creator>Andre Kho</dc:creator>
      <pubDate>Thu, 25 Feb 2021 14:08:31 +0000</pubDate>
      <link>https://dev.to/nekoto/my-moodle-tips-2-3jaf</link>
      <guid>https://dev.to/nekoto/my-moodle-tips-2-3jaf</guid>
      <description>&lt;p&gt;If your Moodle starts consuming too much inodes available on a server, be sure to &lt;strong&gt;purge caches&lt;/strong&gt; periodically.&lt;/p&gt;

&lt;p&gt;Go to &lt;strong&gt;Site administration&lt;/strong&gt; &amp;gt; &lt;strong&gt;Development&lt;/strong&gt; &amp;gt; &lt;strong&gt;Purge caches&lt;/strong&gt;, then click on &lt;strong&gt;Purge all caches&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It will relieve some spaces for you, especially if your resources are limited.&lt;/p&gt;

</description>
      <category>moodle</category>
      <category>cache</category>
      <category>php</category>
      <category>lms</category>
    </item>
    <item>
      <title>My Moodle Tips #1</title>
      <dc:creator>Andre Kho</dc:creator>
      <pubDate>Fri, 15 Jan 2021 05:09:40 +0000</pubDate>
      <link>https://dev.to/nekoto/dealing-with-500-or-503-errors-from-moodle-lms-p75</link>
      <guid>https://dev.to/nekoto/dealing-with-500-or-503-errors-from-moodle-lms-p75</guid>
      <description>&lt;p&gt;If your website going down and showing HTTP error code 500 and/or 503 or sometimes your website's template getting error in some places, you might want to check your &lt;strong&gt;opcache&lt;/strong&gt; PHP extension.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Opcache&lt;/strong&gt; has to be enabled. It will lower your resource usage significantly because of its opcode caching capability.&lt;/p&gt;

&lt;p&gt;However, if it has always been enabled but causing errors like I mentioned above, you can disable &lt;strong&gt;opcache&lt;/strong&gt; extension temporarily (like 3-5 seconds) then enable it again. Your PHP engine should be refreshed and your Moodle site will be up again (if your server doesn't refresh, restart it manually).&lt;/p&gt;

</description>
      <category>moodle</category>
      <category>php</category>
      <category>opcache</category>
      <category>lms</category>
    </item>
    <item>
      <title>After 8 months 4 weeks and 1 day</title>
      <dc:creator>Andre Kho</dc:creator>
      <pubDate>Fri, 18 Sep 2020 03:25:40 +0000</pubDate>
      <link>https://dev.to/nekoto/after-8-months-4-weeks-and-1-day-4g1d</link>
      <guid>https://dev.to/nekoto/after-8-months-4-weeks-and-1-day-4g1d</guid>
      <description>&lt;p&gt;It has been a while I didn't post something for 8 months 4 weeks and 1 day 😂. What a shame.&lt;/p&gt;

&lt;p&gt;Anyway, I just want to share my current state for now. There are a lot of things happened before and after the pandemic. My primary laptop broken (data were safe, hopefully), my only phone was also broken. However, I was able to work for developments and thesis since I still have to get to the campus and able to borrowed my sister's netbook (Thank God).&lt;/p&gt;

&lt;p&gt;One of the great things was that me with my team can deliver new app using Flutter framework and deployed it successfully without too much drama (perhaps). The app was made for a campus which I work for and thankfully, it is the first project to be adequately documented. That is one achievement for our team by the way (a lot of do-not-tell-others stories here). Here is &lt;a href="https://play.google.com/store/apps/details?id=id.ac.likmi.likminetmobile&amp;amp;pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1" rel="noopener noreferrer"&gt;the app&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We still have to work and support on other things like e-learning system that our campus using. In other side, we still have to improve the way we work, managing projects and smaller tasks, and so on.&lt;/p&gt;

&lt;p&gt;That's it for the post. Not a insightful post 😅. But anyway, sharing stories is good, no?&lt;/p&gt;

</description>
      <category>personal</category>
      <category>blog</category>
    </item>
    <item>
      <title>Introduction</title>
      <dc:creator>Andre Kho</dc:creator>
      <pubDate>Fri, 20 Dec 2019 03:15:02 +0000</pubDate>
      <link>https://dev.to/nekoto/introduction-160d</link>
      <guid>https://dev.to/nekoto/introduction-160d</guid>
      <description>&lt;p&gt;Helow, this is my first post and I'm gonna share my life story here from now on perhaps.&lt;/p&gt;

&lt;p&gt;I'm &lt;strong&gt;Andre&lt;/strong&gt;, from Indonesia, currently aged 22, Bachelor graduate in Informatics, Master student in Information System,  working as IT support in an 'IT College'. Sounds great, never enjoy it 😄.&lt;/p&gt;

&lt;p&gt;Why do I put that line? Stay tuned, because it's gonna be a long thread I will ever write in social platform like DEV. Life is tough, but I'm grateful with that 😤.&lt;/p&gt;

&lt;p&gt;Good day! 😁&lt;/p&gt;

</description>
      <category>intro</category>
      <category>devlive</category>
      <category>career</category>
      <category>future</category>
    </item>
  </channel>
</rss>
