Another Heroku tutorial ??
Most of the instructions for deploying Laravel on Heroku use Apache as a web server. By default Laravel comes with a .htaccess
file used by Apache to rewrite all dynamic urls to the public/index.php
file to handle routing with Laravel. Apache on Heroku also supports override config with .htaccess
file, so setup with Apache is very easy.
https://devcenter.heroku.com/articles/custom-php-settings#apache-defaults
Apache uses a Virtual Host that responds to all hostnames. The document root is set up as a <Directory> reachable without access limitations and
AllowOverride All
set to enable the use of.htaccess
files. Any request to a URL ending on.php
will be rewritten to PHP-FPM using a proxy endpoint namedfcgi://heroku-fcgi
viamod_proxy_fcgi
. The DirectoryIndex directive is set toindex.php index.html index.html
.
But what if you want to use Nginx? And a Laravel application not only runs PHP but also often has queue, schedule, database, redis, socketio … so how to handle it?
In this article I will go over some concepts related to Heroku and practice deploying Laravel, Socket.IO and Nginx.
To practice you need a Heroku Free account and install heroku-cli
locally.
Deploy Laravel App
Create a Laravel project with composer
:
1 2 3 4 5 | composer create-project --prefer-dist laravel/laravel:6 heroku-laravel-nginx-socketio <span class="token function">git</span> init <span class="token function">git</span> add <span class="token keyword">.</span> <span class="token function">git</span> commit -m <span class="token string">'Init Laravel 6'</span> |
Create heroku app:
1 2 3 | cd heroku-laravel-nginx-socketio heroku apps:create heroku-laravel-nginx-socketio |
When creating the app, heroku cli will automatically add 1 git remote repository, you can check it with the command:
1 2 3 4 | $ <span class="token function">git</span> remote -v heroku https://git.heroku.com/heroku-laravel-nginx-socketio.git <span class="token punctuation">(</span> fetch <span class="token punctuation">)</span> heroku https://git.heroku.com/heroku-laravel-nginx-socketio.git <span class="token punctuation">(</span> push <span class="token punctuation">)</span> |
If you don’t have heroku
remote, add it yourself with the command git remote add heroku <heroku git url>
Ok, try to push on heroku repo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | $ <span class="token function">git</span> push heroku master Counting objects: 113, done. Delta compression using up to 4 threads. Compressing objects: 100% <span class="token punctuation">(</span> 95/95 <span class="token punctuation">)</span> , done. Writing objects: 100% <span class="token punctuation">(</span> 113/113 <span class="token punctuation">)</span> , 56.69 KiB <span class="token operator">|</span> 3.54 MiB/s, done. Total 113 <span class="token punctuation">(</span> delta 10 <span class="token punctuation">)</span> , reused 0 <span class="token punctuation">(</span> delta 0 <span class="token punctuation">)</span> remote: Compressing <span class="token function">source</span> files <span class="token punctuation">..</span> . done. remote: Building source: remote: remote: <span class="token operator">!</span> Warning: Multiple default buildpacks reported the ability to handle this app. The first buildpack <span class="token keyword">in</span> the list below will be used. remote: Detected buildpacks: PHP,Node.js remote: See https://devcenter.heroku.com/articles/buildpacks <span class="token comment">#buildpack-detect-order</span> remote: ----- <span class="token operator">></span> PHP app detected <span class="token punctuation">..</span> . |
Unlike normal git push when pushing on Github, for example, after pushing on heroku, Heroku will build and deploy.
Buildpacks
Because when the buildpacks have not been added to the heroku app, Heroku will automatically detect the appropriate buildpack to build (perhaps Heroku relies on composer.json
and package.json
files to detect buidpack?).
What is buildpack ? => In a nutshell, buildpacks is a collection of scripts commonly used to build, compile applications depending on the programming language of each application. For example, here, our application is PHP Laravel, there will be steps:
composer install
to install packages,npm install
to install JS packages,npm run dev
to compile and generate assets such as Sass, CSS, Javascript …The main buildpacks supported by Heroku are: Ruby, Node.js, Clojure, Python, Java, Gradle, JVM, Grails 3.x, Scala, Play 2.x, PHP, Go
Here we need 2 buildpacks, heroku-buildpack-php, to run composer
and heroku-buildpack-nodejs to run npm
. Add to Heroku App with the following command:
1 2 3 | heroku buildpacks:add heroku/php heroku buildpacks:add heroku/nodejs |
Heroku will run buildpacks in turn in the order they are added.
Push back and see the result:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | $ <span class="token function">git</span> commit --amend --no-edit $ <span class="token function">git</span> push heroku master -f <span class="token punctuation">..</span> . remote: Building source: remote: remote: ----- <span class="token operator">></span> Node.js app detected remote: remote: ----- <span class="token operator">></span> Creating runtime environment remote: remote: NPM_CONFIG_LOGLEVEL <span class="token operator">=</span> error remote: NODE_ENV <span class="token operator">=</span> production remote: NODE_MODULES_CACHE <span class="token operator">=</span> true remote: NODE_VERBOSE <span class="token operator">=</span> false remote: remote: ----- <span class="token operator">></span> Installing binaries remote: engines.node <span class="token punctuation">(</span> package.json <span class="token punctuation">)</span> : unspecified remote: engines.npm <span class="token punctuation">(</span> package.json <span class="token punctuation">)</span> : unspecified <span class="token punctuation">(</span> use default <span class="token punctuation">)</span> remote: remote: Resolving node version 10.x <span class="token punctuation">..</span> . remote: Downloading and installing node 10.16.3 <span class="token punctuation">..</span> . remote: Using default <span class="token function">npm</span> version: 6.9.0 remote: remote: ----- <span class="token operator">></span> Installing dependencies remote: Installing node modules <span class="token punctuation">(</span> package.json <span class="token punctuation">)</span> remote: added 1005 packages from 481 contributors <span class="token keyword">in</span> 31.347s remote: remote: ----- <span class="token operator">></span> Build <span class="token punctuation">..</span> . |
Now the npm install
command has been run by Nodejs Buildpack with production NODE_ENV
. But we still lack the running npm run dev
or the npm run prod
, so how to run it?
According to the document https://devcenter.heroku.com/articles/nodejs-support#customizing-the-build-process , we need to define 1 more npm scripts to guide Heroku run build, declared in package.json
file either build
or heroku-postbuild
, where heroku-postbuild
takes precedence.
1 2 3 4 | remote: -----> Build remote: Detected both "build" and "heroku-postbuild" scripts remote: Running heroku-postbuild |
So the package.json file will look like this:
1 2 3 4 5 6 | <span class="token property">"scripts"</span> <span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"prod"</span> <span class="token operator">:</span> <span class="token string">"npm run production"</span> <span class="token punctuation">,</span> ... <span class="token property">"heroku-postbuild"</span> <span class="token operator">:</span> <span class="token string">"npm run prod"</span> <span class="token punctuation">}</span> |
Procfile
Open the app with the command: heroku open
, a new browser tab is open: https://heroku-laravel-nginx-socketio.herokuapp.com/ and you will see 403 Forbidden
error ??
In the log when pushing on heroku, there is a paragraph:
1 2 3 4 5 6 | remote: ----- <span class="token operator">></span> Preparing runtime environment <span class="token punctuation">..</span> . remote: NOTICE: No Procfile, using <span class="token string">'web: heroku-php-apache2'</span> <span class="token keyword">.</span> remote: ----- <span class="token operator">></span> Checking <span class="token keyword">for</span> additional extensions to install <span class="token punctuation">..</span> . remote: ----- <span class="token operator">></span> Discovering process types remote: Procfile declares types - <span class="token operator">></span> web |
=> NOTICE: No Procfile, using 'web: heroku-php-apache2'
Procfile? => Each app will have a file named
Procfile
to declare the commands that are run when the app starts, for example:
- Run the web server
- Run queue worker
- …
Syntax of Procfile
file:
1 2 | <process type>: <command> |
Inside:
<process type>
is the name of the command, also known as Process Type , for example,web
,worker
…<command>
is the command that is run when app start, for exampleheroku-php-apache2
,php artisan queue:work
…
There are two special process types, of which the web
is the only process type that can handle HTTP requests. If your app needs a web server then you need to declare the command to run the web server in this type of process.
Each line will be a process type, each process type is run on a completely independent dyno. One of the advantages of Heroku is that it can be easily scaled by increasing the number of dynos per process type. But this will be discussed after we have money and need to run an app production on Heroku because with Free plan Heroku only allows us to create 1 web process and 1 worker process, maximum 1 dyno per process.
Back to our Laravel app, if we use Apache we will have the following Procfile file:
1 2 | web: vendor/bin/heroku-php-apache2 public/ |
By default, heroku-php-apache2
uses the current folder as the root document, but Laravel needs to set the document root to public
to navigate to the public/index.php
file.
Want to use Nginx instead of Apache we will use command vendor/bin/heroku-php-nginx
instead of vendor/bin/heroku-php-apache2
and need to custom Nginx config, because Nginx does not support .htaccess
files like Apache , according to the instructions :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | location / { # try to serve file directly, fallback to rewrite try_files $uri @rewriteapp; } location @rewriteapp { # rewrite all to index.php rewrite ^(.*)$ /index.php/$1 last; } location ~ ^/index.php(/|$) { try_files @heroku-fcgi @heroku-fcgi; # ensure that /index.php isn't accessible directly, but only through a rewrite internal; } |
1 2 | web: vendor/bin/heroku-php-nginx -C heroku-nginx.conf public/ |
Deploy and reopen to see why this time and the correct Laravel page but 500 page error ?? Well first of all, there is no .env
file .env
.
Environment variable
Because the filesystem on Heroku is special in that the changes on the filesystem (not through git) will only be kept until dyno shutdown or restart, or every time deploy or restart all files change or add new. During the run (eg laravel.log file) will be deleted, only the files on git are kept.
So here we will not use the .env
file because this file is usually not added to git. Instead environment variables will be set in your app’s settings on heroku (called Config Vars ) or maybe through heroku cli.
The most important environment variable with Laravel is APP_KEY
, we will generate the key and use heroku cli to set the environment variable for the app:
Generate key with artisan command:
1 2 | php artisan key:generate --show |
Then set the environment variable for heroku app with heroku cli:
1 2 | heroku config:set APP_KEY <span class="token operator">=</span> key_generated_above |
In addition, we need to reconfigure the log to be able to view the log via the heroku logs
command because if you use log in the file, the log file will be deleted after each restart => https://devcenter.heroku.com/articles/getting-started -with-laravel # changing-the-log-destination-for-production
1 2 | heroku config:set LOG_CHANNEL=errorlog |
Deploy Socket.io
To get started we’ll refer to a simple socketio code in repo: https://github.com/heroku-examples/node-socket.io , which displays the server time realtime via socketio:
1 2 | <span class="token function">mkdir</span> socketio-server |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <span class="token punctuation">{</span> <span class="token property">"name"</span> <span class="token operator">:</span> <span class="token string">"socketio-server"</span> <span class="token punctuation">,</span> <span class="token property">"version"</span> <span class="token operator">:</span> <span class="token string">"1.0.0"</span> <span class="token punctuation">,</span> <span class="token property">"description"</span> <span class="token operator">:</span> <span class="token string">""</span> <span class="token punctuation">,</span> <span class="token property">"main"</span> <span class="token operator">:</span> <span class="token string">"index.js"</span> <span class="token punctuation">,</span> <span class="token property">"scripts"</span> <span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"test"</span> <span class="token operator">:</span> <span class="token string">"echo "Error: no test specified" && exit 1"</span> <span class="token punctuation">}</span> <span class="token punctuation">,</span> <span class="token property">"keywords"</span> <span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token punctuation">]</span> <span class="token punctuation">,</span> <span class="token property">"author"</span> <span class="token operator">:</span> <span class="token string">""</span> <span class="token punctuation">,</span> <span class="token property">"license"</span> <span class="token operator">:</span> <span class="token string">"ISC"</span> <span class="token punctuation">,</span> <span class="token property">"dependencies"</span> <span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"express"</span> <span class="token operator">:</span> <span class="token string">"^4.17.1"</span> <span class="token punctuation">,</span> <span class="token property">"socket.io"</span> <span class="token operator">:</span> <span class="token string">"^2.3.0"</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <span class="token keyword">const</span> express <span class="token operator">=</span> <span class="token function">require</span> <span class="token punctuation">(</span> <span class="token string">'express'</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">const</span> socketIO <span class="token operator">=</span> <span class="token function">require</span> <span class="token punctuation">(</span> <span class="token string">'socket.io'</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">const</span> <span class="token constant">PORT</span> <span class="token operator">=</span> <span class="token number">3000</span> <span class="token punctuation">;</span> <span class="token keyword">const</span> server <span class="token operator">=</span> <span class="token function">express</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">listen</span> <span class="token punctuation">(</span> <span class="token constant">PORT</span> <span class="token punctuation">,</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token operator">=></span> console <span class="token punctuation">.</span> <span class="token function">log</span> <span class="token punctuation">(</span> <span class="token template-string"><span class="token string">`Listening on </span> <span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span> <span class="token constant">PORT</span> <span class="token interpolation-punctuation punctuation">}</span></span> <span class="token string">`</span></span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">const</span> io <span class="token operator">=</span> <span class="token function">socketIO</span> <span class="token punctuation">(</span> server <span class="token punctuation">)</span> <span class="token punctuation">;</span> io <span class="token punctuation">.</span> <span class="token function">on</span> <span class="token punctuation">(</span> <span class="token string">'connection'</span> <span class="token punctuation">,</span> <span class="token punctuation">(</span> socket <span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> console <span class="token punctuation">.</span> <span class="token function">log</span> <span class="token punctuation">(</span> <span class="token string">'Client connected'</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> socket <span class="token punctuation">.</span> <span class="token function">on</span> <span class="token punctuation">(</span> <span class="token string">'disconnect'</span> <span class="token punctuation">,</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token operator">=></span> console <span class="token punctuation">.</span> <span class="token function">log</span> <span class="token punctuation">(</span> <span class="token string">'Client disconnected'</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token function">setInterval</span> <span class="token punctuation">(</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token operator">=></span> io <span class="token punctuation">.</span> <span class="token function">emit</span> <span class="token punctuation">(</span> <span class="token string">'time'</span> <span class="token punctuation">,</span> <span class="token keyword">new</span> <span class="token class-name">Date</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">toTimeString</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">,</span> <span class="token number">1000</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> |
Now how to run both Laravel and socketio Node at port 3000. We have the following solutions:
- Adding a new process type => Not feasible because the process type runs on an independent dyno so it cannot connect between Laravel (web) and Socket.IO server
- Add 1 new app just to run Socket.IO => Need more apps (Free plan can only create up to 5 App), or if the plan pays, it will cost an extra $
- Running on the same dyno with Laravel (
web
processs type) => It seems fine for the demo, but the configuration of dyno is only 512MB so if the application is a bit bigger it will be not ok. Since the purpose is a free demo, we will try to run on the same dyno with theweb
Fortunately, Heroku also has a topic on this issue => https://help.heroku.com/CTFS2TJK/how-do-i-run-multiple-processes-on-a-dyno . The way to do this is to use Shell’s background jobs to run multiple commands at the same time by adding the &
character at the end of each statement.
1 2 | web: vendor/bin/heroku-php-nginx -C heroku-nginx.conf public/ <span class="token operator">&</span> <span class="token punctuation">(</span> cd public/socketio-server <span class="token operator">&&</span> node server.js <span class="token punctuation">)</span> <span class="token operator">&</span> <span class="token function">wait</span> -n |
wait -n
is the Shell command, it will exit when there is at least one command exit and therefore will trigger restart dyno.
Need to fix the build step in package.json, to socketio-server
dependency in the socketio-server
directory:
1 2 3 4 5 6 | <span class="token punctuation">{</span> <span class="token property">"scripts"</span> <span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"heroku-postbuild"</span> <span class="token operator">:</span> <span class="token string">"(cd socketio-server && npm install) && npm run prod"</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
The client (Browser) needs to connect to the socketio server, but since Heroku only opens a single port for the web process, it can’t connect to the 3000 port on the client. So we need to create a reverse proxy with nginx to proxy the request to localhost: 3000, in addition to the nginx config file:
1 2 3 4 5 6 7 | location /socket.io/ { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; } |
When there is a request to url /socketio/
, Nginx will request to http://localhost:3000/socket.io/
.
Using socketio on client:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <a href="https://vapor.laravel.com">Vapor</a> <a href="https://github.com/laravel/laravel">GitHub</a> </div> <span class="token inserted">+ <p id="server-time"></p></span> </div> </div> <span class="token inserted">+ <script src="/socket.io/socket.io.js"></script></span> <span class="token inserted">+ <script></span> <span class="token inserted">+ var socket = io();</span> <span class="token inserted">+ var el = document.getElementById('server-time');</span> <span class="token inserted">+ socket.on('time', function(timeString) {</span> <span class="token inserted">+ el.innerHTML = 'Server time: ' + timeString;</span> <span class="token inserted">+ });</span> <span class="token inserted">+ </script></span> |
Deploy and demo succeeded =))