Gitpodifying a new Laravel Application
Tags: tools, docker, containerization, devops
Published: 2020-05-26 16:41:39

<!-- wp:heading -->
<h2>Introduction</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p><a rel="noreferrer noopener" aria-label="Gitpod (opens in a new tab)" href="https://GitPod.io" target="_blank">Gitpod</a> is a SaaS platform that offers Ready-To-Code dev environments. What this means is that GitPod allows you describe your Development Environment as code and easily provision or share it.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>This is great for letting people quickly try out a new library or framework and even for day to day development.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><a rel="noreferrer noopener" aria-label="This article from the GitPod blog (opens in a new tab)" href="https://www.gitpod.io/blog/gitpodify/" target="_blank">This article from the GitPod blog</a> is a great explanation for the problem GitPod solves as well as a fantastic general purpose guide.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Here, we'll extend on that specifically for the Laravel framework and see how to create a Ready-To-Code environment for our projects.</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2>Getting Started</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>The first step is to navigate to the GitHub repository you'd like to set up.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>For this example, I'll use a Laravel project we're building at WTXtra.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>The GitHub repository is publicly available at <a href="https://github.com/WeTalkSound/trivyeah-core/">https://github.com/WeTalkSound/trivyeah-core/</a>. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Next, we'll just edit this URL slightly to launch a Gitpod workspace for this repo.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>We'll add <strong>"gitpod.io/#"</strong> in front of the repo URL and hit enter. For the example above, that'll be <a href="https://gitpod.io/#https://github.com/WeTalkSound/trivyeah-core/">https://gitpod.io/#https://github.com/WeTalkSound/trivyeah-core/</a></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>For first time Gitpod users, you'll be asked to sign in with GitHub and authorize the application.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>When we go to this URL, Gitpod begins building our workspace and you'll see the splash screen.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>To determine how to set up the environment, Gitpod follows the instructions in a <strong>.gitpod.yml</strong> file. If this file does not exist, then it approximates an environment based on your repo.</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2>Set up .gitpod.yml</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>The gitpod.yml file specifies the commands that the environment should run on build as well as the Dockerfile to use to build the container. Adding a custom Dockerfile is optional, but potentially offers us more control over the environment we end up with.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>So, we'll create a simple .gitpod.yml file with the following:</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted"><code class="language-yaml">image:
  file: .gitpod.Dockerfile</code>
</pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>And right next to it, create a <strong>.gitpod.Dockerfile</strong> like so:</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted"><code class="language-docker">FROM gitpod/workspace-mysql               

USER gitpod</code></pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>This uses the gitpod/workspace-mysql as the base image. This image installs and configures MySQL with a root user with no password.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>This works to get you an environment with (almost) everything you need, but there is still some basic housekeeping most Laravel apps will need to take care of.</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":3} -->
<h3>Setting up a Database</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>On the first run of our environment, we'll need to create the database our app will use.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>If you're familiar with docker, you can easily do this from within your custom Dockerfile. However, I prefer to do it from the .gitpod.yml file.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Within the Gitpod configuration file, we can specify any number of tasks to be run. We can specify tasks to be run only the first time a container is being provisioned or we can specify that it be run even time the container starts.</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted"><code class="language-docker">image:
  file: .gitpod.Dockerfile
<strong>tasks:
  - init: mysql -u root -e "create database trivyeah"</strong></code></pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>We'll modify our .gitpod.yml file and add the tasks section to it. It specifies that on init, we'll run the command <code class="language-bash">mysql -u root -e "create database trivyeah"</code> which will log into MySQL as root (with no password) and execute the command specified by the <code>e</code> flag, in this case, creating a database called <strong>trivyeah</strong>.</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":3} -->
<h3>Creating a .env file</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>We'll use the .<strong>env.example</strong> file provided by default with new Laravel installations. In here, we'll specify all the keys that need to be used by the application, including the database name we created above.</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted"><code class="language-none">...
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
<strong>DB_DATABASE=trivyeah</strong>
DB_USERNAME=root
DB_PASSWORD=
...</code></pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>Any other default information we'll need to provision our app can be added to the env.example file.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Next, we will copy the .env.example file to the .env file</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted"><code class="language-yaml">image:
  file: .gitpod.Dockerfile
tasks:
  - init: mysql -u root -e "create database trivyeah" <strong>&amp;&amp; cp .env.example .env</strong></code></pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>We'll add this to the init command, since we only want it to copy for the first run of the container.</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":3} -->
<h3>Installing Dependencies</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>This is another action we will need to perform on initialization of the environment. We'll run the composer and npm (or yarn, if you prefer) commands in the init action as well.</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted"><code class="language-yaml">image:
  file: .gitpod.Dockerfile
tasks:
  - init: mysql -u root -e "create database trivyeah" &amp;&amp; cp .env.example .env &amp;&amp; <strong>composer install &amp;&amp; npm install</strong></code></pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>At this point, we can begin using <code>artisan</code> commands for any other application specific setup we need to.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>For example, we'll need to generate a unique application key for our .env</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":3} -->
<h3>Generate Application Key</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>In a new Laravel application, we'll need to run the <code>php artisan key:generate</code> command to fill the APP_KEY field in the .env file.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>However, it's a command we'll only need to run the first time. So, we'll add it to the init field of our configuration again.</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted"><code class="language-yaml">image:
  file: .gitpod.Dockerfile
tasks:
  - init: mysql -u root -e "create database trivyeah" &amp;&amp; cp .env.example .env &amp;&amp; composer install &amp;&amp; npm install &amp;&amp; <strong>php artisan key:generate</strong></code></pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>At this point, all the set up should be done. If you have any other setup specific to your Laravel app, they can be added here as well. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>The init entry is getting unwieldy and so we'll refactor it as soon as we're done with everything else. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>The next thing to do is run migrations before we serve the app.</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":3} -->
<h3>Run Migrations</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>For migrations, I prefer to run them each time I use the container, in case there has been a change. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>I also run the seeders at this point to populate the database with any required data or test data.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Since I want the command to always run, I'll add it to a <strong>command</strong> entry on the .gitpod.yml file.</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted"><code class="language-yaml">image:
  file: .gitpod.Dockerfile
tasks:
  - init: mysql -u root -e "create database trivyeah" &amp;&amp; cp .env.example .env &amp;&amp; composer install &amp;&amp; npm install &amp;&amp; php artisan key:generate
    <strong>command: php artisan migrate --seed</strong></code></pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>Apart from running each time we run our environment, <strong>command</strong> task entries have another key differences from <strong>init</strong> tasks. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>An init task expects to end. Long running processes such as running a development server or continuously compiling assets are not suited for init tasks. Command tasks on the other hand can run for as long as the environment is running and are suited to such tasks above.</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":3} -->
<h3>Run Development Server</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>We can run the development server on Gitpod as well. We'll want to run it after migrations are done.</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted"><code class="language-yaml">image:
  file: .gitpod.Dockerfile
tasks:
  - init: mysql -u root -e "create database trivyeah" &amp;&amp; cp .env.example .env &amp;&amp; composer install &amp;&amp; npm install &amp;&amp; php artisan key:generate
    command: php artisan migrate --seed <strong>&amp;&amp; php artisan serve</strong></code></pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>Running and exposing a service on Gitpod creates a publicly accessible URL, similar to ngrok, of the form <strong>&lt;PORT&gt;-&lt;GITPOD-CONTAINER-ID&gt;. gitpod.io</strong></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>There's a small problem with our current setup though. Laravel uses the value of <code>$_SERVER['HTTP']</code> to generate URLs and Routes. This value will be set to localhost:8000 and not to the correct Gitpod URL.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>To work around this, we'll set our APP_URL in .env to our Gitpod URL and then instruct Laravel to use that value to generate URLs and Routes.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>We'll update our <strong>.env.example</strong> to the following:</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted"><code class="language-none">APP_NAME="Your App Name"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
<strong>APP_URL=</strong>
...</code></pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>Gitpod loads some environment variables. You can run <code>env</code> on the terminal to see all environment variables. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>One of them is <strong>GITPOD_WORKSPACE_URL</strong> which is the same as the one in your browser when you launch a workspace. We'll need to write the variable into our <strong>.env</strong> file.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>We'll use the bash utility <code>sed</code> for this. Essentially, we'll do two replacements. One to put in the workspace URL and another to add the port number in front.<br></p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted"><code class="language-bash"> sed -i "s|APP_URL=|APP_URL=${GITPOD_WORKSPACE_URL}|g" .env &amp;&amp; sed -i "s|APP_URL=https://|APP_URL=https://8000-|g" .env</code></pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>The first pass replaces <strong>"APP_URL="</strong> with <strong>"APP_URL=https://&lt;your-workspace-id&gt;.gitpod.io" </strong>and the second pass replaces <strong>"APP_URL=https://"</strong> with <strong>"APP_URL=https://8000-"</strong><br><br>We'll now add this to our init entry in <strong>.gitpod.yml</strong> so that it happens right after our <strong>.env</strong> file is created.</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted"><code class="language-yaml">image:
  file: .gitpod.Dockerfile
tasks:
  - init: mysql -u root -e "create database trivyeah" &amp;&amp; cp .env.example .env &amp;&amp; <strong>sed -i "s|APP_URL=|APP_URL=${GITPOD_WORKSPACE_URL}|g" .env &amp;&amp; sed -i "s|APP_URL=https://|APP_URL=https://8000-|g" .env &amp;&amp;</strong> composer install &amp;&amp; npm install &amp;&amp; php artisan key:generate
    command: php artisan migrate --seed &amp;&amp; php artisan serve</code></pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>That init hook has gotten quite unwieldy and we'll address it shortly. In the interim, we'll need to add some code in our <strong>AppServiceProvider</strong> to force Laravel to use our APP_URL to generate URLs and Routes.</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted"><code class="language-php"> public function boot()
    {
        \URL::forceRootUrl(\Config::get('app.url'));
        if (\Str::contains(\Config::get('app.url'), 'https://')) {
            \URL::forceScheme('https');
        }
    }</code></pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>The code we've added forces Laravel to use the <strong>app.url</strong> config entry (which is gotten from .env's APP_URL) as the root URL.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Warning: If you use an older version of Laravel </strong>the <code class="language-php">\Str::contains()</code> method won't work and you'll have to use the string helper <code class="language-php">str_contains()</code> instead.</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":3} -->
<h3>Cleaning Up .gitpod.yml </h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>The init entry in our Gitpod configuration file is messy. We can move all that to a bash script so that it's more readable. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Let's create a <strong>.gitpod-init.sh</strong> file in the root of our project and copy everything in our init entry there.</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted"><code class="language-php">mysql -u root -e "create database trivyeah"
cp .env.example .env
sed -i "s|APP_URL=|APP_URL=${GITPOD_WORKSPACE_URL}|g" .env
sed -i "s|APP_URL=https://|APP_URL=https://8000-|g" .env
composer install
npm i
php artisan key:generate</code></pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>And finally, we'll update our <strong>.gitpod.yml</strong> to use this command</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted"><code class="language-yaml">image:
  file: .gitpod.Dockerfile
tasks:
  - init: <strong>bash .gitpod-init.sh</strong>
    command: php artisan migrate --seed &amp;&amp; php artisan serve</code></pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>Careful with the filenames. Watch the “<strong>.</strong>” in front.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Everything should work now.</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":3} -->
<h3>Running our environment</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>At this point, we can push to our repository and run the environment. If you already had a workspace for this repo and branch, you can delete it by going to <a href="https://gitpod.io/workspaces">https://gitpod.io/workspaces</a>.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Now we can create and test a new workspace by going to  <strong>"gitpod.io/#https://github.com/&lt;org-name&gt;/&lt;repo-name&gt;"</strong> as we described at the beginning of the article.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>If everything works as it should, we can add a badge to the <strong>Readme.md</strong> that can be clicked to launch the workspace.</p>
<!-- /wp:paragraph -->

<!-- wp:preformatted -->
<pre class="wp-block-preformatted"><code class="language-none">[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/&lt;org-name&gt;/&lt;repo-name&gt;)</code> </pre>
<!-- /wp:preformatted -->

<!-- wp:paragraph -->
<p>Our Laravel project is now Gitpod enabled and can deploy ready-to-code environments in one click.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Many thanks to <a rel="noreferrer noopener" aria-label="Gabriel Nwogu (opens in a new tab)" href="https://twitter.com/gabrielnwogu" target="_blank">Gabriel Nwogu</a> for testing this out and suggesting changes.</p>
<!-- /wp:paragraph -->