So, we need to set up the AWS server for our Laravel project. Let’s start with the requirements – for Laravel as a framework, and for our project specifically.

Laravel Server Requirements

According to Official Documentation we have following server requirements:

  • PHP >= 8.0
  • BCMath PHP Extension
  • Ctype PHP Extension
  • cURL PHP Extension
  • DOM PHP Extension
  • Fileinfo PHP Extension
  • JSON PHP Extension
  • Mbstring PHP Extension
  • OpenSSL PHP Extension
  • PCRE PHP Extension
  • PDO PHP Extension
  • Tokenizer PHP Extension
  • XML PHP Extension

Our Demo Project Requirements

These dependencies may vary depending on what your project actually needs or what organization plans to use, but for now, let’s stick to the list below to save you a bit of time in the future:

  • NginX HTTP Server
  • Zip PHP Extension
  • GD PHP Extension
  • Composer
  • MySQL PHP Extension
  • Amazon RDS as MySQL server

Setup EC2 instance

  1. First you need to have an AWS account, sign in or register at We won’t go into the registration process itself.
  2. When you log in it should look something like this:
Console Home
  1. Click on the EC2 link in the top bar.

Alternatively, you can find it in the Services menu under Compute category:

Services Compute EC2
  1. Now you should be in your EC2 Dashboard, it looks like that:
EC2 Dashboard
  1. In the top right corner, you need to choose your region first. All the stats dashboard is displaying are for this selected region including your servers (instances). Pick the one which seems most suitable for your user base as it affects how fast it can be reached. In this example, we used Europe (Frankfurt) eu-central-1.
  1. On the second row of the dashboard, there is a card named Launch instance. Your servers are called instances on EC2. Click on the Launch instance button to proceed.
Launch Instance
  1. Enter a name for your server, e.g. Demo web server
Server Name
  1. Choose an OS image. We picked Ubuntu because it provides tools out of the box, and doesn’t need any custom installation of packages.

The image was left unchanged, other images are for more specific scenarios.

OS Image
  1. For tutorial purposes we left the t2.micro instance type unchanged, since we do not need more power for demo purposes, also it is a free tier.

According to your project requirements, you might want to change this setting to something more powerful.

Instance Type
  1. To access created instance later you’re going to need the SSH key for the server.

Press Create new key pair button:

Key Pair Login

Enter a unique key pair name and select the following options:

  • Key pair type: RSA
  • Private key file format: .pem
Create Key Pair

And press Create key pair on the bottom right corner of the dialog to save and download the key.

Your newly generated key will be automatically downloaded:

Downloaded Key
  1. In network settings choose My IP, to allow access only from your IP Address, sometimes you might want to leave it from Anywhere but it is not recommended or add your custom rules.

Check both Allow HTTPS traffic from the internet and Allow HTTP traffic from the internet, since we are setting a web server and want connections to be accepted on 80 and 443 ports by default.

Network Settings
  1. Then you might want to configure your storage to add more or bigger volumes for your data. We left that unchanged for this tutorial.
Configure Storage
  1. No changes are needed in the Advanced details section. Review the summary in the right sidebar, and press the Launch Instance button in the bottom right corner.

After that you should see that launch successfully initiated:

Launch Instance Success

To launch all the Laravel-related commands later, and to install/configure some software beforehand, we will need to connect to our server via SSH, with Terminal. Let’s set it up.

  1. Navigate to the > Connect to instance page.

This can be done by clicking on the Connect to your instance button from the success page

Connect to your instance

or optionally from the instances menu by selecting instance and pressing connect button on the top right corner

Instances connect
  1. The page should look like this:
Connect to instance

Optionally you can copy the public IP for later somewhere else.

  1. To connect to your instance from the terminal, we need to choose the SSH Client tab:
SSH Client

Here are exact instructions on how to connect to your server using the key you generated and downloaded when creating an EC2 instance and it works perfectly fine.

The only problem with that is it’s not very convenient to have such a long command to remember or paste every time, and in addition, you need to be in a directory where the key file lies. So let’s tweak this a bit.

These steps are optional

  • Create a .ssh folder in your home directory if it doesn’t exist, this is where usually SSH keys are stored
user@local$ mkdir -p ~/.ssh
  • Move the downloaded key to the .ssh directory
user@local$ mv Downloads/ec2-demo-web-ubuntu-server.pem ~/.ssh
  • Modify permissions so only your user can read the key
user@local$ chmod 400 ~/.ssh/ec2-demo-web-ubuntu-server.pem
  • You can have your own IP address to URL mapping by overriding returned DNS value or trying to remember the exact IP address. Let’s append the ubuntu-aws line to the /etc/hosts file:
root@local# echo " ubuntu-aws" >> /etc/hosts

Now you can substitute the IP address with ubuntu-aws in your shell commands and it resolves into

By default server drops all ICMP requests, which means if you ping it won’t respond

user@local$ ping ubuntu-awsPING ubuntu-aws ( 56(84) bytes of data.^C--- ubuntu-aws ping statistics ---3 packets transmitted, 0 received, 100% packet loss, time 2072ms

You can check if SSH is accessible instead

user@local$ nmap -p 22 ubuntu-awsStarting Nmap 7.93 ( ) at 2022-11-01 01:23 EETNmap scan report for ubuntu-aws ( is up (0.033s latency).PORT   STATE SERVICE22/tcp open  sshNmap done: 1 IP address (1 host up) scanned in 0.09 seconds

The host is reachable, great!

  • Connect to your server with this command from any folder in your shell:
user@local$ ssh -i ~/.ssh/ec2-demo-web-ubuntu-server.pem ubuntu@ubuntu-aws

And also you can have an alias by entering alias connect-ubuntu-aws="ssh -i ~/.ssh/ec2-demo-web-ubuntu-server.pem ubuntu@ubuntu-aws" so you can connect to your server only by typing:

user@local$ connect-ubuntu-aws

For this alias to persist add the alias connect-ubuntu-aws="ssh -i ~/.ssh/ec2-demo-web-ubuntu-server.pem ubuntu@ubuntu-aws" command to your ~/.bashrc or ~/.zshrc file, depending what shell you do use.

After a successful connection your terminal window might look similar to this:

user@local$ connect-ubuntu-awsWelcome to Ubuntu 22.04.1 LTS (GNU/Linux 5.15.0-1019-aws x86_64) * Documentation: * Management: * Support:  System information as of Tue Nov  1 01:23:59 UTC 2022  System load:  0.0               Processes:             99  Usage of /:   25.5% of 7.57GB   Users logged in:       0  Memory usage: 24%               IPv4 address for eth0:  Swap usage:   0% * Ubuntu Pro delivers the most comprehensive open-source security and   compliance features. updates can be applied immediately.45 of these updates are standard security updates.To see these additional updates run: apt list --upgradableLast login: Tue Nov  1 01:23:59 2022 from run a command as administrator (user "root"), use "sudo <command>".See "man sudo_root" for details.ubuntu@ip-172-31-44-101:~$

Don’t get confused by a different IP address on the ubuntu@ip-172-31-44-101:~$ prompt, this is the server’s local IP and not the public one you used to connect.

We continue preparing our server for the upcoming Laravel project. Usually, the first thing after fresh installation, before everything else, are OS and software updates. So, let’s take care of that.

  1. Since updates are system-wide changes, you need root privileges for that. Very often, tutorials prepend all commands with sudo which is not super convenient when doing administrative work on servers. To elevate privileges to root enter:
ubuntu@ip-172-31-44-101:~$ sudo su -root@ip-172-31-44-101:~#
  1. The most overlooked command when administrating the server is the screen command. It provides the ability to launch and use multiple shell sessions. What’s the deal with the screen command and updates you may ask?

While it may be not a big deal executing simple commands such as cd or ls, on processes that take more time and do system-wide changes such as updates it is crucial. Imagine you lose your connection to the server due to whatever reason, then the whole session terminates and the running process in the foreground gets interrupted. Combine that with system updates and your server might get bricked and not even boot anymore. When running commands in the screen session it persists on the server even if you get disconnected.

Open a screen session and press <space> to continue:

root@ip-172-31-44-101:~# screen

In case you get disconnected reconnect to the server and resume where you left off by issuing:

root@ip-172-31-44-101:~# screen -r

More information on the screen command can be found on man pages or directly in the shell man screen.

  1. To install updates enter:
root@ip-172-31-44-101:~# apt-get update && apt-get upgrade
  1. While writing this article it didn’t go as well as planned.

If you didn’t encounter this error just skip to step 5.

During the apt-get upgrade command error message appeared:

Some packages could not be installed. This may mean that you haverequested an impossible situation or if you are using the unstabledistribution that some required packages have not yet been createdor been moved out of Incoming.The following information may help to resolve the situation:The following packages have unmet dependencies: grub-efi-amd64-signed : Depends: grub-efi-amd64-bin (= 2.06-2ubuntu7) but 2.06-2ubuntu10 is to be installedE: Broken packages

To quickly fix that enter:

root@ip-172-31-44-101:~# apt --only-upgrade install grub-efi-amd64-signed

And repeat step 3.

  1. You will get a prompt if you want to continue, so enter Y to proceed
69 upgraded, 0 newly installed, 0 to remove and 6 not upgraded.39 standard security updatesNeed to get 44.6 MB/78.4 MB of archives.After this operation, 6882 kB of additional disk space will be used.Do you want to continue? [Y/n] Y

After a few miles of text you get another prompt to choose which services to restart:

Package configuration

At this point, just press the <TAB> key to select the <Ok> button and press <Enter>, what you pick doesn’t matter at all this time, because we are going to reboot the server anyway to boot up into newest kernel if any were installed.

Finally, reboot the server:

root@ip-172-31-44-101:~# reboot

Your connection will get closed and in 1-3 minutes updated server should be up and running so you will be able to reconnect.

Congratulations, you’ve installed the latest updates on your server!

We need to install a web-server to our EC2 server, to actually serve our web-project.

  1. To install NginX enter:
root@ip-172-31-44-101:~# apt-get install nginx
  1. Check the status of the NginX server using systemctl status nginx:
root@ip-172-31-44-101:~# systemctl status nginx● nginx.service - A high performance web server and a reverse proxy server     Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)     Active: active (running) since Wed 2022-11-09 17:18:47 UTC; 13min ago       Docs: man:nginx(8)   Main PID: 1679 (nginx)      Tasks: 2 (limit: 1143)     Memory: 1.7M        CPU: 20ms     CGroup: /system.slice/nginx.service             ├─1679 "nginx: master process /usr/sbin/nginx -g daemon on; master_process on;"             └─1680 "nginx: worker process" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""

Launch NginX on boot

If service is enabled:

on 2nd line we see:

Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)

enabled; means that the nginx service starts on boot and vendor preset: enabled means that by default it is enabled, so no changes are needed.

If service is disabled:

Loaded: loaded (/lib/systemd/system/nginx.service; disabled; vendor preset: enabled)

In case it says disabled; run systemctl enable nginx to start nginx on boot.

root@ip-172-31-44-101:~# systemctl enable nginxSynchronizing state of nginx.service with SysV service script with /lib/systemd/systemd-sysv-install.Executing: /lib/systemd/systemd-sysv-install enable nginx

Start NginX now

NginX is running:

3rd line is also in our interest:

Active: active (running) since Wed 2022-11-09 17:18:47 UTC; 13min ago

This means the server is actually running and no further action is required.

Nginx is dead:

Active: inactive (dead) since Wed 2022-11-09 17:36:17 UTC; 1s ago

If the nginx server is not started, run systemctl start nginx and check the status again systemctl status nginx:

root@ip-172-31-44-101:~# systemctl start nginx
  1. Optionally if you’re interested to see what processes are bound to each port run lsof -i -P -n | grep LISTEN. This might be handy in the future. The output should be similar to that:
root@ip-172-31-44-101:~# lsof -i -P -n | grep LISTENsystemd-r  410 systemd-resolve   14u  IPv4  16617      0t0  TCP (LISTEN)sshd       766            root    3u  IPv4  18688      0t0  TCP *:22 (LISTEN)sshd       766            root    4u  IPv6  18699      0t0  TCP *:22 (LISTEN)nginx     2345            root    6u  IPv4  27464      0t0  TCP *:80 (LISTEN)nginx     2345            root    7u  IPv6  27465      0t0  TCP *:80 (LISTEN)nginx     2346        www-data    6u  IPv4  27464      0t0  TCP *:80 (LISTEN)nginx     2346        www-data    7u  IPv6  27465      0t0  TCP *:80 (LISTEN)
  1. Verify that the server is reachable: enter the server’s IP address in the browser

If you forgot or didn’t save the public IP from earlier, you can find it directly in the terminal by entering:

root@ip-172-31-44-101:~# wget -qO- icanhazip.com18.195.117.231

Note: protocol here is http:// and not https:// because we have no services listening on port 443 (https) as we checked with the previous command lsof -i -P -n | grep LISTEN

You should see something like this:

NginX running

The default nginx configuration file is located at /etc/nginx/sites-enabled/default -> /etc/nginx/sites-available/default.

root@ip-172-31-44-101:/etc/nginx/sites-enabled# cat /etc/nginx/sites-enabled/default
### You should look at the following URL's in order to grasp a solid understanding# of Nginx configuration files in order to fully unleash the power of Nginx.# In most cases, administrators will remove this file from sites-enabled/ and# leave it as reference inside of sites-available where it will continue to be# updated by the nginx packaging team.## This file will automatically load configuration files provided by other# applications, such as Drupal or WordPress. These applications will be made# available underneath a path with that package name, such as /drupal8.## Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.### Default server configuration#server {        listen 80 default_server;        listen [::]:80 default_server;        # SSL configuration        #        # listen 443 ssl default_server;        # listen [::]:443 ssl default_server;        #        # Note: You should disable gzip for SSL traffic.        # See:        #        # Read up on ssl_ciphers to ensure a secure configuration.        # See:        #        # Self signed certs generated by the ssl-cert package        # Don't use them in a production server!        #        # include snippets/snakeoil.conf;        root /var/www/html;        # Add index.php to the list if you are using PHP        index index.html index.htm index.nginx-debian.html;        server_name _;        location / {                # First attempt to serve request as file, then                # as directory, then fall back to displaying a 404.                try_files $uri $uri/ =404;        }        # pass PHP scripts to FastCGI server        #        #location ~ \.php$ {        #       include snippets/fastcgi-php.conf;        #        #       # With php-fpm (or other unix sockets):        #       fastcgi_pass unix:/run/php/php7.4-fpm.sock;        #       # With php-cgi (or other tcp sockets):        #       fastcgi_pass;        #}        # deny access to .htaccess files, if Apache's document root        # concurs with nginx's one        #        #location ~ /\.ht {        #       deny all;        #}}# Virtual Host configuration for You can move that to a different file under sites-available/ and symlink that# to sites-enabled/ to enable it.##server {#       listen 80;#       listen [::]:80;##       server_name;##       root /var/www/;#       index index.html;##       location / {#               try_files $uri $uri/ =404;#       }#}

We only have document root defined as /var/www/html and our default file we seen in browser is index.nginx-debian.html.

root@ip-172-31-44-101:/etc/nginx/sites-enabled# cat /var/www/html/index.nginx-debian.html
<!DOCTYPE html><html><head><title>Welcome to nginx!</title><style>    body {        width: 35em;        margin: 0 auto;        font-family: Tahoma, Verdana, Arial, sans-serif;    }</style></head><body><h1>Welcome to nginx!</h1><p>If you see this page, the nginx web server is successfully installed andworking. Further configuration is required.</p><p>For online documentation and support please refer to<a href=""></a>.<br/>Commercial support is available at<a href=""></a>.</p><p><em>Thank you for using nginx.</em></p></body></html>

Let’s leave it for now and install PHP and other dependencies.

We’re getting closer to actually installing our Laravel project. But Laravel is a PHP framework, and guess what – we don’t have PHP installed on the server yet. Let’s fix this.

  1. Time to review our requirements once again, and determine which Ubuntu packages we need to install:

As per Laravel requirements:

  • BCMath PHP Extension – php8.1-bcmath – provides arbitrary-precision arithmetic
  • Ctype PHP Extension – php8.1-common – checks whether a character or string falls into a certain character class according to the current locale
  • cURL PHP Extension – php8.1-curl – allows you to connect and communicate to many different types of servers with many different types of protocols
  • DOM PHP Extension – php8.1-xml – allows you to operate on XML documents through the DOM API with PHP
  • Fileinfo PHP extension – php8.1-common – functions in this module try to guess the content type and encoding of a file
  • JSON PHP Extension – Always available – implements the JavaScript Object Notation (JSON) data-interchange format
  • Mbstring PHP Extension – php8.1-mbstring – provides multibyte specific string functions that help you deal with multibyte encodings
  • OpenSSL PHP Extension – openssl – library for symmetric and asymmetric encryption and decryption
  • PCRE PHP Extension – Always available – PHP Core library with JIT support
  • PDO PHP Extension – php8.1-common – defines a lightweight, consistent interface for accessing databases
  • Tokenizer PHP Extension – php8.1-common – interface to the PHP tokenizer
  • XML PHP Extension – php8.1-xml – implements support for DOM, SimpleXML, XML, and XSL

And additional dependencies for our demo project:

  • PHP-FPM – php8.1-fpm – server-side, FastCGI implementation (FPM-CGI binary), high performance interface between Web Server and PHP programs, allowing a server to handle more web page requests per unit of time
  • Zip PHP Extension – php8.1-zip – this will be useful if you plan to have ability to work with archives
  • GD PHP Extension – php8.1-gd – library for working with images, if you plan to use Laravel package like spatie/laravel-medialibrary
  • PHP cli – php8.1-cli – PHP command-line interpreter if you plan to have some scripts you want to in command line
  • Composer – composer – dependency manager for PHP
  • MySQL PHP Extension – php8.1-mysql – MySQL Native Driver, provides mysqlimysqlnd and pdo_mysql PHP modules


  • Redis PHP Extension – php8.1-redis – extension for interfacing with Redis, in-memory storage, used for cache and queues. Used in combination with Laravel Horizon and redis-server
  1. Install all dependencies:
root@ip-172-31-44-101:~# apt-get install --no-install-recommends php8.1 php8.1-{bcmath,cli,common,curl,fpm,gd,mbstring,mysql,xml,zip} openssl composer

Note that --no-install-recommends flag tells to install only required packages and ignore other recommended packages like Apache web server which we do not intend to use

  1. Verify PHP is installed and check the version:
root@ip-172-31-44-101:~# php -vPHP 8.1.2-1ubuntu2.8 (cli) (built: Nov  2 2022 13:35:25) (NTS)Copyright (c) The PHP GroupZend Engine v4.1.2, Copyright (c) Zend Technologies    with Zend OPcache v8.1.2-1ubuntu2.8, Copyright (c), by Zend Technologies
  1. Verify composer is installed and check the version:

Do not run Composer as a root/super user! See for details

ubuntu@ip-172-31-44-101:~$ composer --versionComposer 2.2.6 2022-02-04 17:00:38
  1. Optionally you can check which modules are enabled for your php installation:
root@ip-172-31-44-101:~# php -i | grep enabledZend Signal Handling => enabledZend Memory Manager => enabledIPv6 Support => enabledBCMath support => enabledCalendar support => enabledctype functions => enabledcURL support => enableddate/time support => enabledDOM/XML => enabledHTML Support => enabledXPath Support => enabledXPointer Support => enabledSchema Support => enabledRelaxNG Support => enabledEXIF Support => enabledMultibyte decoding support using mbstring => enabledFFI support => enabledfileinfo support => enabledInput Validation and Filtering => enabledFTP support => enabledFTPS support => enabledGD Support => enabledFreeType Support => enabledGIF Read Support => enabledGIF Create Support => enabledJPEG Support => enabledPNG Support => enabledWBMP Support => enabledXPM Support => enabledXBM Support => enabledWebP Support => enabledBMP Support => enabledTGA Read Support => enabledGetText Support => enabledhash support => enablediconv support => enabledInternationalization support => enabledjson support => enabledlibXML streams => enabledMultibyte Support => enabledMultibyte (japanese) regex support => enabledMysqlI Support => enabledmysqlnd => enabledOpenSSL support => enabledpcntl support => enabledPCRE (Perl Compatible Regular Expressions) Support => enabledPCRE JIT Support => enabledPDO support => enabledPDO Driver for MySQL => enabledPhar: PHP Archive support => enabledPhar-based phar archives => enabledTar-based phar archives => enabledZIP-based phar archives => enabledgzip compression => enabledNative OpenSSL support => enabledPOSIX support => enabledReadline Support => enabledReflection => enabledSession Support => enabledsession.upload_progress.enabled => On => Onshmop support => enabledSimpleXML support => enabledSchema support => enabledSockets Support => enabledsodium support => enabledSPL support => enabledDynamic Library Support => enabledsysvmsg support => enabledsysvsem support => enabledsysvshm support => enabledTokenizer Support => enabledXMLReader => enabledXMLWriter => enabledXSL => enabledEXSLT => enabledZip => enabledZLib Support => enabled
  1. Verify the status of PHP-FPM:
root@ip-172-31-44-101:~# systemctl status php8.1-fpm● php8.1-fpm.service - The PHP 8.1 FastCGI Process Manager     Loaded: loaded (/lib/systemd/system/php8.1-fpm.service; enabled; vendor preset: enabled)     Active: active (running) since Wed 2022-11-09 16:27:55 UTC; 31s ago       Docs: man:php-fpm8.1(8)    Process: 36268 ExecStartPost=/usr/lib/php/php-fpm-socket-helper install /run/php/php-fpm.sock /etc/php/8.1/fpm/pool.d/www.conf 81 (code=exited, status=0/SUCCESS)   Main PID: 36265 (php-fpm8.1)     Status: "Processes active: 0, idle: 2, Requests: 0, slow: 0, Traffic: 0req/sec"      Tasks: 3 (limit: 1143)     Memory: 10.2M        CPU: 54ms     CGroup: /system.slice/php8.1-fpm.service             ├─36265 "php-fpm: master process (/etc/php/8.1/fpm/php-fpm.conf)" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""             ├─36266 "php-fpm: pool www" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""             └─36267 "php-fpm: pool www" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""

PHP-FPM service is enabled and running so no action is needed.

  1. Enable PHP on NginX

Let’s create a PHP file with a random name in our document root /var/www/html/ and see if it is working.

root@ip-172-31-44-101:~# cd /var/www/html/root@ip-172-31-44-101:/var/www/html# nano 1af3503416.php  # pick different file name

Then enter PHP contents:


phpinfo() function outputs a large amount of information about the current state of PHP.

and press ^X (CTRL+x) to exit. Caret ^ symbol means [control] key. Editor asks you to confirm changes, enter Y and press [enter] to exit the editor.

Save modified buffer? Y Y Yes N No           ^C Cancel

Note we chose a random filename because often people forget to delete such files, and if the file is named test.php this could pose a potential leak of sensitive system data.

Now navigate to and see what happens. Right, the browser just downloaded the file. This is because NginX doesn’t know yet what to do with that PHP file. It needs to be passed to PHP FPM to php code to execute so NginX can return a proper response.

For it to work, we need to add one configuration block to the NginX site config.

Edit the/etc/nginx/sites-enabled/default file

root@ip-172-31-44-101:~# nano /etc/nginx/sites-enabled/default

To server section after location / {...} add:

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

It should look something like this:

server {        listen 80 default_server;        listen [::]:80 default_server;        # <...>        root /var/www/html;        # Add index.php to the list if you are using PHP        index index.html index.htm index.nginx-debian.html;        server_name _;        location / {                # First attempt to serve request as file, then                # as directory, then fall back to displaying a 404.                try_files $uri $uri/ =404;        }        location ~ \.php$ {                fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;                fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;                include fastcgi_params;        }        # <...>}

This is a very basic NginX setup just to check if PHP is working properly, and is not suitable for production. We will come back to the NginX configuration for Laravel later.

For changes to take effect you need to restart the NginX server. But before that, make sure you have no errors in config files. This can be checked with the nginx -t command.

root@ip-172-31-44-101:~# nginx -tnginx: the configuration file /etc/nginx/nginx.conf syntax is oknginx: configuration file /etc/nginx/nginx.conf test is successful

If everything is ok restart NginX:

root@ip-172-31-44-101:~# systemctl restart nginx

Now navigate to again, PHP should be working:


And now you can delete this test file.

root@ip-172-31-44-101:~# rm /var/www/html/1af3503416.php

Our Laravel project will use MySQL database, so let’s install the database engine now. It comes from Amazon itself and it is called RDS.

This tutorial will teach you how to create an environment to run your MySQL database. We’ll use Amazon Relational Database Service (Amazon RDS) for this, and everything in this tutorial is Free Tier eligible. In addition, we’ll provide some MySQL commands to do basic things such as create/list users/tables.

Let’s get started:

  1. Click on the RDS link in the top bar to navigate to RDS Dashboard:
Console home

Alternatively you can find RDS link under Services > Database > RDS:

Services Database RDS
  1. In the second card named Create database on Dashboard press [Create database] button:
Create database
  1. On the Choose a database creation method section choose Standard create because we want to define some configuration options ourselves.
Standard create
  1. Pick MySQL engine type, the default MySQL version is 8, let’s keep it this way:
Engine MySQL
  1. In the Templates section we chose Free tier for tutorial purposes:
RDS Templates
  1. On the Settings card you can choose your DB instance identifier and credentials. Everything looks fine. We just checked Auto generate a password. Password will be given to you once the instance is created.
RDS Settings
  1. On the Connectivity card pick Connect to an EC2 compute resource:

This option automatically adds the database to the same VPC (Virtual Private Cloud) and DB subnet group, this ensures you can safely reach the database server within the private network from your server without exposing the database to the public (note that Public access is not available). Leave everything else on default, and we are good to go.

RDS Connectivity
  1. Submit form by hitting [Create database] button on the bottom 🙂
  2. After submitting you will be redirected to your databases list. The blue alert above will indicate that the database is being created and will allow you to view the credentials:
Creating database
  1. Press the [View credential details] button and save your database credentials somewhere safe:

This is the only time you will be able to view this password.

Password for your database
  1. It might take several minutes for the process to finish, stay patient. Afterward green alert will notify you when the database is up and ready for use.
Created database
  1. Press the [View connection details] button, and save your endpoint URL, this will be used to connect to your database from the server.

Please note your RDS instance is not accessible publicly

RDS Connection details
  1. Now that we have MySQL instance running we need to install MySQL client on your server, this can be done using this command apt-get install mysql-client:
root@ip-172-31-44-101:~# apt-get install mysql-clientReading package lists... DoneBuilding dependency tree... DoneReading state information... DoneThe following additional packages will be installed:  mysql-client-8.0 mysql-client-core-8.0 mysql-commonThe following NEW packages will be installed:  mysql-client mysql-client-8.0 mysql-client-core-8.0 mysql-common0 upgraded, 4 newly installed, 0 to remove and 12 not upgraded.Need to get 2702 kB of archives.After this operation, 62.3 MB of additional disk space will be used.Do you want to continue? [Y/n] Y
  1. Connect to the RDS server using this command mysql -u admin -p -h, replace hostname with the one you got provided when the instance was created and use the password we saved earlier:
ubuntu@ip-172-31-44-101:~$ mysql -u admin -p -h password:Welcome to the MySQL monitor.  Commands end with ; or \g.Your MySQL connection id is 531Server version: 8.0.28 Source distributionCopyright (c) 2000, 2022, Oracle and/or its affiliates.Oracle is a registered trademark of Oracle Corporation and/or itsaffiliates. Other names may be trademarks of their respectiveowners.Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.mysql>

Prompt mysql> will indicate we have connected to our MySQL instance successfully.

  1. We need to create a database for our project, let’s call it demo_project. You can do it by entering the CREATE DATABASE demo_project; command:
mysql> CREATE DATABASE demo_project;Query OK, 1 row affected (0.04 sec)

In case you made a typo creating your database you can DROP (delete) it with this command DROP DATABASE your_db_name;

To list databases enter SHOW DATABASES;

mysql> SHOW DATABASES;+--------------------+| Database           |+--------------------+| demo_project       || information_schema || mysql              || performance_schema || sys                |+--------------------+5 rows in set (0.01 sec)

As we can see we have our newly created database listed.

You will see there are more databases, here’s a short description of what they’re for:

  • mysql – is the system database that contains tables that store information required by the MySQL server
  • information_schema – provides access to database metadata
  • performance_schema – is a feature for monitoring MySQL Server execution at a low level
  • sys – a set of objects that helps DBAs and developers interpret data collected by the Performance Schema

Leave them as is, since they’re the default ones required for MySQL to function properly unless you know what you’re doing.

  1. After creating a new database for our project, we need to create a separate user for our application that will use that database. Our default admin user has full control over the MySQL server, which means it has access to every database, table, user, etc. As a result, it’s best to avoid using this account for anything else than administrative purposes.

To create new user enter CREATE USER 'demo_user'@'%' IDENTIFIED BY '<your_password>';

mysql> CREATE USER 'demo_user'@'%' IDENTIFIED BY '********';Query OK, 0 rows affected (0.01 sec)

To list current users on database enter SELECT user FROM mysql.user;

mysql> SELECT user FROM mysql.user;+------------------+| user             |+------------------+| admin            || demo_user        || mysql.infoschema || mysql.session    || mysql.sys        || rdsadmin         |+------------------+6 rows in set (0.00 sec)

We can check current permissions for our newly created demo_user account using SHOW GRANTS for demo_user;

mysql> SHOW GRANTS for demo_user;+---------------------------------------+| Grants for demo_user@%                |+---------------------------------------+| GRANT USAGE ON *.* TO `demo_user`@`%` |+---------------------------------------+1 row in set (0.00 sec)

As we can see it has no permissions at all. USAGE is a synonym for no permissions.

In most cases, we want to grant MySQL users privileges based on the database to which they should have access. It is standard procedure.

We can grant all privileges for demo_user on demo_project with this command GRANT ALL PRIVILEGES ON demo_project.* TO 'demo_user'@'%';.

mysql> GRANT ALL PRIVILEGES ON demo_project.* TO 'demo_user'@'%';Query OK, 0 rows affected (0.01 sec)

If the MySQL server is started without the --skip-grant-tables option, it reads all grant table contents into memory during its startup sequence. The in-memory tables become effective for access control at that point.

To be sure privileges are updated without restarting the server in case it has the --skip-grant-tables flag set, we can force update privileges using the FLUSH PRIVILEGES; command.

mysql> FLUSH PRIVILEGES;Query OK, 0 rows affected (0.09 sec)

Finally, we check demo_user permissions again using SHOW GRANTS for demo_user;

mysql> SHOW GRANTS for demo_user;+-------------------------------------------------------------+| Grants for demo_user@%                                      |+-------------------------------------------------------------+| GRANT USAGE ON *.* TO `demo_user`@`%`                       || GRANT ALL PRIVILEGES ON `demo_project`.* TO `demo_user`@`%` |+-------------------------------------------------------------+2 rows in set (0.00 sec)

Now you can log out with the admin user and try to log in with demo_user.

mysql> exit;Bye
root@ip-172-31-44-101:~# mysql -u demo_user -p -h
  1. Optionally we can check if privileges are really in effect.

Select the current working database and enter USE demo_project;

mysql> USE demo_project;Database changed

Create a table my_table with CREATE TABLE my_table (id int);

mysql> CREATE TABLE my_table (id int);Query OK, 0 rows affected (0.07 sec)

To list tables enter SHOW tables;

mysql> SHOW TABLES;+------------------------+| Tables_in_demo_project |+------------------------+| my_table               |+------------------------+1 row in set (0.01 sec)

Great, everything is working as expected, finally, we can delete this test table using DROP TABLE my_table;

mysql> DROP TABLE my_table;Query OK, 0 rows affected (0.03 sec)

You can exit MySQL console now with EXIT;

mysql> EXIT;Byeubuntu@ip-172-31-44-101:~$
  1. Now we have prepared the demo_project database and demo_user account for our Laravel application.

Finally! After all those preparations, we can see our project working in our browser.

Usually, NginX and PHP-FPM service runs as www-data user and it is a system user for services. In the best case scenario, we would like to isolate our web project from any system services and maybe have a different directory for example /home/web/demoproject as opposed to /var/www/html where you need root user explicitly.

  1. To add a new user enter the adduser web command as root. You will be prompted to define a password for a web user add fill in optional details. Make sure to choose a secure password.
root@ip-172-31-44-101:~# adduser webAdding user `web' ...Adding new group `web' (1001) ...Adding new user `web' (1001) with group `web' ...Creating home directory `/home/web' ...Copying files from `/etc/skel' ...New password:Retype new password:passwd: password updated successfullyChanging the user information for webEnter the new value, or press ENTER for the default        Full Name []:        Room Number []:        Work Phone []:        Home Phone []:        Other []:Is the information correct? [Y/n] Y
  1. Now we can log in with a web user and create a structure for how we want our future laravel project served.

We can easily do that by entering sudo su web or just su web if you’re a root. The user you’re currently logged in as can be seen in your command prompt or optionally can be checked with the whoami command.

ubuntu@ip-172-31-44-101:~$ sudo su webweb@ip-172-31-44-101:/home/ubuntu$ whoamiweb

Navigate to your home directory, this can be done by entering cd without any parameters or cd ~ or cd /home/web. The present working directory can be checked with the pwd command.

web@ip-172-31-44-101:/home/ubuntu$ cdweb@ip-172-31-44-101:~$ pwd/home/web

Now create a new directory for our demo project:

web@ip-172-31-44-101:~$ mkdir demoproject

And for this step, we just create a single PHP file to test future configurations.

web@ip-172-31-44-101:~$ cd demoproject/web@ip-172-31-44-101:~/demoproject$ nano index.php

Here are the contents of our index.php file for now.

<?phpecho 'test';

Then press CTRL-X to save and exit the editor.

  1. Now we have a new user for our project and directory with some test files.

Currently, NginX and PHP-FPM would have no access to our /home/web directory, and we need a few more things to do for NginX to be able to serve and process this php file.

  1. Add the user to the www-data group. Current present groups our new users are in can be checked with the groups web command.
root@ip-172-31-44-101:~# groups webweb : web

To add our user to the www-data group. we can use the usermod command with -aG flags.

root@ip-172-31-44-101:~# usermod -aG www-data web

And then check groups again.

root@ip-172-31-44-101:~# groups webweb : web www-data

As we can see, the user has been added to the www-data group.

  1. Change /home/web folder permissions to allow services to read it using the chmod 755 /home/web command.
web@ip-172-31-44-101:~$ chmod 755 /home/web
  1. Edit NginX config
root@ip-172-31-44-101:/home/web# nano /etc/nginx/sites-enabled/default

Lines that we are interested in are near each other starting with root /var/www/html

It should be the 41st and 44th lines, they look like that:

root /var/www/html;# Add index.php to the list if you are using PHPindex index.html index.htm index.nginx-debian.html;

Change root directive root /var/www/html to root /home/web/demoproject.

And as you have guessed the comment suggests we need to append index.php to the index directive:

Update index index.html index.htm index.nginx-debian.html; to index index.html index.htm index.nginx-debian.html index.php;.

The result should look like that:

root /home/web/demoproject;# Add index.php to the list if you are using PHPindex index.html index.htm index.nginx-debian.html index.php;

Full NginX default site configuration without comments looks like this:

server {        listen 80 default_server;        listen [::]:80 default_server;        root /home/web/demoproject;        index index.html index.htm index.nginx-debian.html index.php;        server_name _;        location / {                try_files $uri $uri/ =404;        }        location ~ \.php$ {                fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;                fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;                include fastcgi_params;        }}

For sake of simplicity, we won’t go deeper into NginX configuration nuances.

  1. Another config we need to update is the PHP-FPM pool. To be able to process php files we need to change the user and group php-fpm process pool is running on. It is possible to configure another pool, but now let’s just update the default one. This can be done by editing /etc/php/8.1/fpm/pool.d.
root@ip-172-31-44-101:~# nano /etc/php/8.1/fpm/pool.d/www.conf

In the [www] section find lines user = www-data and group = www-data, they should not be too far from the beginning.


user = www-datagroup = www-data

update it to

user = webgroup = web

And exit by saving the file with CTRL-X.

  1. For changes to take effect it is necessary to restart NginX and PHP-FPM. Let’s proceed with systemctl command from previous chapters.
root@ip-172-31-44-101:~# systemctl restart nginxroot@ip-172-31-44-101:~# systemctl restart php8.1-fpm

If you navigate to your site URL http://<YOUR-SERVER-IP-ADDRESS> you should see the site echoing test to confirm the configuration of services is successful.

  1. From this point there are several options for how to populate your Laravel project files into our new /home/web/demoproject directory. The simplest form would be to download your Laravel project archive on the server and extract it to the /home/web/demoproject directory or pull it from the GIT repository.

We will cover how to pull it from your GitHub repository by using the git clone command.

Since we will be cloning the existing repository we can delete the existing demoproject directory, because it will be created when we clone the Laravel project.

web@ip-172-31-44-101:~$ rm -fr demoproject/

To be able to clone the repository and later pull from it using the ssh method we need to generate a new ssh-key pair.

Note: even if the repository is public, you need to add your ssh key otherwise access would be denied.

Note: If you wish to clone the public repository you do not own, we suggest skipping a key generation and using the HTTPS method. For example, to copy the demo project we used in this tutorial, use git clone demoproject and proceed to step 10.

To generate new ssh-key pair use the ssh-keygen command.

web@ip-172-31-44-101:~$ ssh-keygen -t ed25519 -C ""Generating public/private ed25519 key pair.Enter file in which to save the key (/home/web/.ssh/id_ed25519):Enter passphrase (empty for no passphrase):Enter same passphrase again:Your identification has been saved in /home/web/.ssh/id_ed25519Your public key has been saved in /home/web/.ssh/id_ed25519.pubThe key fingerprint is:SHA256:/B0X+6/9X83V+PXvXGTIVYUTSPUjNSp8PMwBC+rvwvU your_email@example.comThe key's randomart image is:+--[ED25519 256]--+|         . oo+o=+||        . o * *.o||       .   + O.oo||      ..    o.o*o||       .S   . * *||        .o . o *=||      . ..o .   O||       o.  E   o=||        ..    .o@|+----[SHA256]-----+

Your public key can be previewed using this command:

web@ip-172-31-44-101:~$ cat /home/web/.ssh/

Now copy this key, go to your account settings on GitHub and add it by pressing the [New SSH Key] button

GitHub New SSH Key

Fill in the form and submit it by pressing [Add SSH key].

Add new SSH key

Now go to your repository and copy the SSH URL:

GitHub repo SSH

And clone the repository:

web@ip-172-31-44-101:~$ git clone demoprojectCloning into 'demoproject'...remote: Enumerating objects: 173, done.remote: Counting objects: 100% (173/173), done.remote: Compressing objects: 100% (130/130), done.remote: Total 173 (delta 26), reused 173 (delta 26), pack-reused 0Receiving objects: 100% (173/173), 119.98 KiB | 706.00 KiB/s, done.Resolving deltas: 100% (26/26), done.

The last argument of the git clone demoproject command is the directory where it should be stored otherwise it will use the GitHub repository name which is not always ideal.

  1. Up until this moment, a very simple NginX configuration was enough to test a single PHP file and our project isolation, but as you know Laravel is a lot more, and doesn’t even have index.php in its root directory. So we need to update our NginX configuration once again. According to Official Documentation our configuration now should look like that:
root@ip-172-31-44-101:~# nano /etc/nginx/sites-enabled/default
server {    listen 80 default_server;    listen [::]:80 default_server;    root /home/web/demoproject/public;    add_header X-Frame-Options "SAMEORIGIN";    add_header X-Content-Type-Options "nosniff";    index index.php;    charset utf-8;    server_name _;    location / {        try_files $uri $uri/ /index.php?$query_string;    }    location = /favicon.ico { access_log off; log_not_found off; }    location = /robots.txt  { access_log off; log_not_found off; }    error_page 404 /index.php;    location ~ \.php$ {        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;        include fastcgi_params;    }    location ~ /\.(?!well-known).* {        deny all;    }}

As we see our document root is now /home/web/demoproject/public, index directive has only an index.php value because this is the only entry point the Laravel application has and a bunch of other settings. More information on various configurations can be found on Official NginX Documentation

  1. From this point road will be a lot less bumpy. The hardest part in the past. It is time to set up the Laravel project using the usual steps you do in your development environment.

Navigate to your project directory:

web@ip-172-31-44-101:~$ cd demoproject/

Copy .env.example to .env

web@ip-172-31-44-101:~/demoproject$ cp .env.example .env

Fill in your database credentials in the .env file when we were settings the RDS instance in the previous chapter.

web@ip-172-31-44-101:~/demoproject$ nano .env******

Run composer install

web@ip-172-31-44-101:~/demoproject$ composer installInstalling dependencies from lock file (including require-dev)Verifying lock file contents can be installed on current platform.Package operations: 108 installs, 0 updates, 0 removalsAs there is no 'unzip' nor '7z' command installed zip files are being unpacked using the PHP zip extension.This may cause invalid reports of corrupted archives. Besides, any UNIX permissions (e.g. executable) defined in the archives will be lost.Installing 'unzip' or '7z' may remediate them.  - Downloading doctrine/inflector (2.0.6)  - Downloading doctrine/lexer (1.2.3)<...>   INFO  Discovering packages.  laravel/breeze .............................................................................................................................. DONE  laravel/sail ................................................................................................................................ DONE  laravel/sanctum ............................................................................................................................. DONE  laravel/tinker .............................................................................................................................. DONE  nesbot/carbon ............................................................................................................................... DONE  nunomaduro/collision ........................................................................................................................ DONE  nunomaduro/termwind ......................................................................................................................... DONE  spatie/laravel-ignition ..................................................................................................................... DONE81 packages you are using are looking for funding.Use the `composer fund` command to find out more!

Run php artisan key:generate

web@ip-172-31-44-101:~/demoproject$ php artisan key:generate   INFO  Application key set successfully.

Run php artisan storage:link to make necessary symlinks for accessing files in public storage

web@ip-172-31-44-101:~/demoproject$ php artisan storage:link   INFO  The [public/storage] link has been connected to [storage/app/public].

Run migrations using php artisan migrate

web@ip-172-31-44-101:~/demoproject$ php artisan migrate   INFO  Preparing database.  Creating migration table ............................................................................................................... 58ms DONE   INFO  Running migrations.  2014_10_12_000000_create_users_table ................................................................................................... 78ms DONE  2014_10_12_100000_create_password_resets_table ......................................................................................... 34ms DONE  2019_08_19_000000_create_failed_jobs_table ............................................................................................. 40ms DONE  2019_12_14_000001_create_personal_access_tokens_table .................................................................................. 62ms DONE
  1. To later update your repository on the server after you pushed some changes:

Login to server:

$ ssh -i ~/.ssh/ec2-demo-web-ubuntu-server.pem ubuntu@ubuntu-aws

Login as a web user:

ubuntu@ip-172-31-44-101:~$ sudo su web

Navigate to your project’s directory:

web@ip-172-31-44-101:/home/ubuntu$ cd /home/web/demoproject/

Issue git pull command:

web@ip-172-31-44-101:~/demoproject$ git pullremote: Enumerating objects: 12, done.remote: Counting objects: 100% (12/12), done.remote: Compressing objects: 100% (6/6), done.remote: Total 9 (delta 3), reused 9 (delta 3), pack-reused 0Unpacking objects: 100% (9/9), 54.81 KiB | 597.00 KiB/s, done.From   901ecc4..50ae9b6  main       -> origin/mainUpdating 901ecc4..50ae9b6Fast-forward .gitignore                           |  1 - public/build/assets/app.73cd3409.css |  1 + public/build/assets/app.d426e523.js  | 32 ++++++++++++++++++++++++++++++++ public/build/manifest.json           | 12 ++++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 public/build/assets/app.73cd3409.css create mode 100644 public/build/assets/app.d426e523.js create mode 100644 public/build/manifest.json
  1. Now your project is LIVE, let’s share it with some friends! But wait, the URL http://<YOUR-SERVER-IP-ADDRESS> is very hard to memorize and not convenient at all. You may purchase a domain name on one of the providers and add DNS A type record with the value of your public IP address.

On your domain provider’s panel entry should be similar to this:

A # your actual server ip address

or if you don’t use a subdomain it even may look similar to that, using @ instead of name:

A    @

Configuration used in this tutorial doesn’t need any additional changes on the server to support the domain name, all HTTP requests will resolve to the default site.

Congratulations, you’ve completed this “Deploy Laravel to AWS” course!

Our demo project used in this tutorial with all exact configurations can be accessed at

Leave a Reply

Your email address will not be published. Required fields are marked *