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
- PHP-FPM
- Zip PHP Extension
- GD PHP Extension
- PHP CLI
- Composer
- MySQL PHP Extension
- Amazon RDS as MySQL server
Setup EC2 instance
- First you need to have an AWS account, sign in or register at https://aws.amazon.com/. We won’t go into the registration process itself.
- When you log in it should look something like this:
- Click on the EC2 link in the top bar.
Alternatively, you can find it in the Services menu under Compute category:
- Now you should be in your EC2 Dashboard, it looks like that:
- 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
.
- 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.
- Enter a name for your server, e.g.
Demo web server
- 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.
- 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.
- To access created instance later you’re going to need the SSH key for the server.
Press Create new key pair button:
Enter a unique key pair name and select the following options:
- Key pair type: RSA
- Private key file format: .pem
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:
- 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.
- Then you might want to configure your storage to add more or bigger volumes for your data. We left that unchanged for this tutorial.
- 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:
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.
- Navigate to the > Connect to instance page.
This can be done by clicking on the Connect to your instance button from the success page
or optionally from the instances menu by selecting instance and pressing connect button on the top right corner
- The page should look like this:
Optionally you can copy the public IP for later somewhere else.
- To connect to your instance from the terminal, we need to choose the SSH Client tab:
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
18.195.117.231 ubuntu-aws
line to the/etc/hosts
file:
root@local# echo "18.195.117.231 ubuntu-aws" >> /etc/hosts
Now you can substitute the IP address with ubuntu-aws
in your shell commands and it resolves into 18.195.117.231
.
By default server drops all ICMP requests, which means if you ping it won’t respond
user@local$ ping ubuntu-awsPING ubuntu-aws (18.195.117.231) 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 ( https://nmap.org ) at 2022-11-01 01:23 EETNmap scan report for ubuntu-aws (18.195.117.231)Host 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: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage 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: 172.31.44.101 Swap usage: 0% * Ubuntu Pro delivers the most comprehensive open-source security and compliance features. https://ubuntu.com/aws/pro79 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 xx.xx.xxx.xxxTo 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.
- 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:~#
- 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 thescreen
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
.
- To install updates enter:
root@ip-172-31-44-101:~# apt-get update && apt-get upgrade
- 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.
- 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:
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.
- To install NginX enter:
root@ip-172-31-44-101:~# apt-get install nginx
- 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
- 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 127.0.0.53:53 (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)
- Verify that the server is reachable: enter the server’s IP address in the browser
http://18.195.117.231/
.
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 nothttps://
because we have no services listening on port 443 (https) as we checked with the previous commandlsof -i -P -n | grep LISTEN
You should see something like this:
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.# https://www.nginx.com/resources/wiki/start/# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/# https://wiki.debian.org/Nginx/DirectoryStructure## 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: https://bugs.debian.org/773332 # # Read up on ssl_ciphers to ensure a secure configuration. # See: https://bugs.debian.org/765782 # # 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 127.0.0.1:9000; #} # deny access to .htaccess files, if Apache's document root # concurs with nginx's one # #location ~ /\.ht { # deny all; #}}# Virtual Host configuration for example.com## 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 example.com;## root /var/www/example.com;# 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="http://nginx.org/">nginx.org</a>.<br/>Commercial support is available at<a href="http://nginx.com/">nginx.com</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.
- 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, providesmysqli
,mysqlnd
andpdo_mysql
PHP modules
Optional:
- 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 andredis-server
- 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
- 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
- Verify composer is installed and check the version:
Do not run Composer as a root/super user! See https://getcomposer.org/root for details
ubuntu@ip-172-31-44-101:~$ composer --versionComposer 2.2.6 2022-02-04 17:00:38
- 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
- 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.
- 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:
<?phpphpinfo();
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 http://18.195.117.231/1af3503416.php
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 http://18.195.117.231/1af3503416.php
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:
- Click on the RDS link in the top bar to navigate to RDS Dashboard:
Alternatively you can find RDS link under Services > Database > RDS:
- In the second card named Create database on Dashboard press
[Create database]
button:
- On the Choose a database creation method section choose
Standard create
because we want to define some configuration options ourselves.
- Pick MySQL engine type, the default MySQL version is 8, let’s keep it this way:
- In the Templates section we chose
Free tier
for tutorial purposes:
- 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.
- 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.
- Submit form by hitting
[Create database]
button on the bottom 🙂 - 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:
- 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.
- 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.
- 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
- 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
- Connect to the RDS server using this command
mysql -u admin -p -h database-1.cbd1u2cfua0q.eu-central-1.rds.amazonaws.com
, 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 database-1.cbd1u2cfua0q.eu-central-1.rds.amazonaws.comEnter 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.
- We need to create a database for our project, let’s call it
demo_project
. You can do it by entering theCREATE 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 serverinformation_schema
– provides access to database metadataperformance_schema
– is a feature for monitoring MySQL Server execution at a low levelsys
– 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.
- 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 database-1.cbd1u2cfua0q.eu-central-1.rds.amazonaws.com
- 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:~$
- Now we have prepared the
demo_project
database anddemo_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.
- 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
- 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.
- 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.
- Add the user to the
www-data
group. Current present groups our new users are in can be checked with thegroups 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.
- Change
/home/web
folder permissions to allow services to read it using thechmod 755 /home/web
command.
web@ip-172-31-44-101:~$ chmod 755 /home/web
- 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.
- 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.
From
user = www-datagroup = www-data
update it to
user = webgroup = web
And exit by saving the file with CTRL-X.
- 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.
- 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 https://github.com/mc0de/rds-ec2-project.git 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 "your_email@example.com"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/id_ed25519.pub
Now copy this key, go to your account settings on GitHub https://github.com/settings/keys and add it by pressing the [New SSH Key]
button
Fill in the form and submit it by pressing [Add SSH key]
.
Now go to your repository and copy the SSH URL:
And clone the repository:
web@ip-172-31-44-101:~$ git clone git@github.com:mc0de/rds-ec2-project.git 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 git@github.com:mc0de/rds-ec2-project.git demoproject
command is the directory where it should be stored otherwise it will use the GitHub repository name which is not always ideal.
- 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 https://laravel.com/docs/9.x/deployment#nginx 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 https://www.nginx.com/resources/wiki/start/#pre-canned-configurations.
- 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
DB_HOST=database-1.cbd1u2cfua0q.eu-central-1.rds.amazonaws.comDB_DATABASE=demo_projectDB_USERNAME=demo_userDB_PASSWORD=******
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
- 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 github.com:mc0de/rds-ec2-project 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
- 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 aws-course.laraveldaily.com 18.195.117.231 # 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 @ 18.195.117.231
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 aws-course.laraveldaily.com