Deploy Laravel with GitHub Actions
Automate Laravel deployments to AWS Elastic Beanstalk on every PR merge.
Introduction
30 Second Summary
Every time you finish a code change, you face the same question: how do I get this onto the live server? Copying files manually is slow, error-prone, and impossible to scale across a team.
In this project, you will build a fully automated deployment pipeline that takes your Laravel app from a merged pull request on GitHub to a live public URL on AWS Elastic Beanstalk without you touching a server.
What You'll Build
You merge a PR on GitHub, watch GitHub Actions pick up the change in real-time, and within minutes your updated Laravel app is live at a public URL you can share with anyone.
By the end of this project, you'll have:
- A live Laravel app at a public Elastic Beanstalk URL that you deployed entirely through a CI/CD pipeline.
- A GitHub Actions workflow that fires only when a pull request is merged to main, giving you a precise deployment gate that ignores direct pushes.
- Production-ready Elastic Beanstalk configuration files that automatically handle Laravel's document root, storage permissions, and artisan caching on every deploy.
- A complete deploy cycle you can demo end-to-end: make a code change on a branch, open a PR, merge it, and watch it go live.
- Secret Mission: Add a PHPUnit test gate so the workflow blocks deployment if tests fail.
Are there any prerequisites?
You will need an AWS account (free tier eligible), a GitHub account, and PHP with Composer installed locally. Familiarity with Laravel basics and Git is helpful but not required to follow along.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
You're going to wire up a complete deployment pipeline where merging a pull request on GitHub automatically ships your Laravel app to a live AWS environment. This is the same zero-touch deploy workflow that real engineering teams rely on every day.
Set Up Laravel and GitHub Repository
Before you can automate deployment, you need something to deploy. This step creates the foundation: a working Laravel application stored in a GitHub repository.
Without the app and repo in place, there is nothing for the pipeline to pick up and push to AWS. You will scaffold a fresh Laravel project with Composer, initialize version control with Git, and push everything to GitHub.
In this step, get ready to:
- Create a fresh Laravel application on your Desktop.
- Push the code to a new GitHub repository.
- Verify the app serves locally and the code is visible on GitHub.
Create a new Laravel application
Laravel projects are scaffolded using Composer, the PHP package manager. One command downloads the framework, its dependencies, and generates the default project structure.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal, and pressing Enter.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your Laravel application by running:
composer create-project laravel/laravel laravel-eb-deploy
What does this command do?
This tells Composer to download the laravel/laravel package and install it into a new folder called laravel-eb-deploy.
Composer resolves all of Laravel's dependencies, generates an .env file from .env.example, and generates a unique application key automatically.
This takes 30 to 60 seconds depending on your connection speed. Once finished, you will see a message confirming the application key was generated.
Seeing 'command not found'?
Make sure Composer is installed and available in your terminal. Run composer --version to check. If it is not found, revisit the Composer installation guide at getcomposer.org.
Help me install Composer on my system
- Move into your new project directory and initialize a Git repository by running:
cd laravel-eb-deploy
git init
git add .
git commit -m "Initial commit"
What do these commands do?
These commands turn your Laravel folder into a Git repository.
- git init creates a new .git directory to track changes.
- git add . stages all the generated files.
- git commit -m "Initial commit" saves the current state as your first snapshot.
You should see output listing the files committed, ending with a summary like create mode 100644 for each file.
Seeing 'Please tell me who you are'?
Git needs your name and email before it can create a commit. Run these two commands, replacing the placeholder values with your own details:
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
Then re-run git add . followed by git commit -m "Initial commit".
Help me fix the Git identity error
Create a GitHub repository and push your code
Your code needs to live on GitHub so the deployment pipeline can access it later. You will create an empty repository and push your local code to it.
- Open github.com in your browser and sign in.
- Click the + icon in the top-right corner.
- Click New repository.
- Enter laravel-eb-deploy in the Repository name field.
- Leave Add a README file unchecked.
- Click Create repository.
Why leave it empty?
You already have a local Git repo with an initial commit. Adding a README or .gitignore on GitHub would create a conflicting history that makes the first push more complicated.
After creating the repository, GitHub shows a Quick Setup page with your repository URL.
- Connect your local repository to GitHub and push your code by running:
git remote add origin https://github.com/[[GITHUB_USERNAME="your GitHub username"]]/laravel-eb-deploy.git
git branch -M main
git push -u origin main
What do these commands do?
- git remote add origin tells Git where to push your code (your new GitHub repo).
- git branch -M main ensures your default branch is named main.
- git push -u origin main uploads your code to GitHub and sets main as the tracked upstream branch.
- Refresh your GitHub repository page in the browser.
- You should see all your Laravel files listed, including app/, routes/, composer.json, and artisan.
Push rejected or authentication failed?
If you see a 403 or authentication error, GitHub may need a personal access token instead of your password. Go to Settings, then Developer settings, then Personal access tokens on GitHub and generate a token with repo scope. Use this token as your password when prompted.
Help me set up GitHub authentication
Verify the app runs locally
The final check confirms that your Laravel app actually works before you hand it off to a deployment pipeline.
What do you think you will see when you serve a brand-new Laravel app locally?
- Start the Laravel development server by running:
php artisan serve
- Open http://localhost:8000 in your browser.
You should see the Laravel welcome page with the Laravel logo and links to documentation. This confirms your application is working correctly.
How do I stop the server?
The development server takes over your terminal while it runs. Press Ctrl+C to stop it when you are done verifying. You will not need it running for the next step.
Not seeing the welcome page?
Make sure you are inside the laravel-eb-deploy directory when you run php artisan serve. If you see a port error, another process may be using port 8000. Try php artisan serve --port=8080 instead and visit http://localhost:8080.
Help me troubleshoot the Laravel server
Your Laravel app is live locally and safely stored on GitHub. Next up, you will create the AWS environment where this app gets deployed.
Create the Elastic Beanstalk Application and Environment
Your Laravel app is ready and living on GitHub. Now you need a place for it to run in the cloud.
In this step, you will create an AWS Elastic Beanstalk application and environment using the AWS Console. This gives your GitHub Actions pipeline a deployment target to push code to.
In this step, get ready to:
- Create an Elastic Beanstalk application and PHP environment in the AWS Console.
- Set up the required IAM roles for Elastic Beanstalk.
- Verify the environment is healthy and accessible at a public URL.
Create a new Elastic Beanstalk application
Elastic Beanstalk needs two things: an application (the logical container for your project) and an environment (the actual running infrastructure). You will create both using the console wizard.
- Open your browser and go to the AWS Management Console at https://console.aws.amazon.com.
- Sign in with your AWS account credentials.
- In the top-right corner, confirm your region is set to US East (N. Virginia) us-east-1. If it shows a different region, click the region name and select US East (N. Virginia).
- In the search bar at the top, type Elastic Beanstalk and select the service from the results.
- Click Create application.
What is Elastic Beanstalk?
Elastic Beanstalk is a managed service that handles provisioning EC2 instances, configuring web servers, and managing deployments for you. You give it your code and it handles the infrastructure.
Think of it as a layer on top of EC2 that removes the need to manually SSH into servers and configure them yourself.
You are now in the Create application wizard. Fill in the application and environment details:
- For Application name, enter laravel-eb-deploy.
- For Environment name, enter laravel-eb-deploy-env.
- For Platform, choose PHP.
- For the platform branch, select PHP 8.4 running on 64bit Amazon Linux 2023. The console will automatically select the recommended version (64bit Amazon Linux 2023 v4.13.3 running PHP 8.4).
- For Application code, select Sample application. You will deploy your Laravel code through the pipeline later.
- For Presets, select Single instance (free tier eligible).
- Click Next.
Why Single instance?
The Single instance preset skips the load balancer and runs your app on a single EC2 instance. This keeps you within the AWS free tier (750 hours/month of t3.micro).
A load-balanced environment is what you would use in production, but for learning CI/CD it adds cost without adding value.
Configure service access and IAM roles
Elastic Beanstalk needs IAM roles to manage your EC2 instances and to perform service actions on your behalf. The console will prompt you to set these up.
- You are now on the Configure service access page.
- For Service role, select Create and use new service role. This creates aws-elasticbeanstalk-service-role with the permissions EB needs.
- For EC2 instance profile, you may see an empty dropdown. If aws-elasticbeanstalk-ec2-role appears in the list, select it and skip ahead. If the dropdown is empty, you need to create the instance profile first.
Why does the EC2 instance profile dropdown appear empty?
AWS no longer auto-creates a default EC2 instance profile for Elastic Beanstalk. Recent security guidelines prevent AWS services from automatically creating roles with trust policies to other services. You need to create it manually through IAM.
If you need to create the instance profile, follow these steps in a new browser tab:
- Open the IAM console in a new tab by going to https://console.aws.amazon.com/iam.
- In the left navigation, click Roles.
- Click Create role.
- For Trusted entity type, choose AWS service.
- For Service or use case, choose Elastic Beanstalk.
- For Use case, select Elastic Beanstalk – Compute.
- Click Next.
- Verify that the permissions policies include AWSElasticBeanstalkWebTier, AWSElasticBeanstalkWorkerTier, and AWSElasticBeanstalkMulticontainerDocker.
- Click Next.
- For Role name, enter aws-elasticbeanstalk-ec2-role.
- Click Create role.
Now return to the Elastic Beanstalk tab where you were configuring service access:
- Click the refresh icon next to the EC2 instance profile dropdown.
- Select aws-elasticbeanstalk-ec2-role from the dropdown.
- Click Skip to Review. This skips the optional networking, database, and scaling configuration pages.
- On the Review page, confirm your settings look correct.
- Click Submit at the bottom of the page to start creating your environment.
Don't see the role after refreshing?
Make sure you created the role in the same AWS account you are using for Elastic Beanstalk. Double check that the role name is exactly aws-elasticbeanstalk-ec2-role with no extra spaces.
help me troubleshoot why my EC2 instance profile isn't showing in the Elastic Beanstalk dropdown
Verify the environment is healthy
Elastic Beanstalk is now provisioning your EC2 instance, configuring the web server, and deploying the sample application. This takes 2-3 minutes.
- Watch the Events section at the bottom of the environment page. You will see progress messages as resources are created.
- Wait until the environment health indicator at the top turns green and shows Ok.
Before you visit the URL, what do you think you will see? Remember, you deployed the EB sample application, not your Laravel code.
- Click the URL shown beneath the environment name (it looks like laravel-eb-deploy-env.us-east-1.elasticbeanstalk.com). You can also click Go to environment.
You should see the Elastic Beanstalk sample application page. This is a default placeholder page confirming your environment is running and healthy.
Why deploy the sample app first?
You want to confirm the infrastructure works before adding your own code to the mix. If you deployed Laravel directly and something broke, you would not know whether the issue was your code or the environment setup.
In the next step, you will set up GitHub Actions to deploy your actual Laravel code through the pipeline.
Environment health is red or gray?
Wait a full 3 minutes before troubleshooting. The environment needs time to provision. If it stays unhealthy, check the Events tab for error messages. Common issues include IAM permission problems or service limits in your AWS account.
help me fix my Elastic Beanstalk environment that won't become healthy
Your deployment target is live and healthy. Next up, you will create a GitHub Actions workflow that deploys your Laravel code to this environment every time you merge a pull request.
Deploy via GitHub Actions (The Broken Deploy)
Your Elastic Beanstalk environment is running and healthy. Now it is time to wire up the automation that deploys your Laravel app every time a pull request merges to main.
For GitHub Actions to deploy to your AWS account, it needs permission. Instead of storing long-lived AWS access keys as secrets, you will configure OIDC (OpenID Connect) so GitHub can request short-lived credentials on each workflow run. You will then create the workflow file, merge it via a PR, and watch the pipeline succeed. The site will not serve Laravel correctly yet. That is intentional.
In this step, get ready to:
- Configure OIDC trust between GitHub Actions and AWS.
- Create the GitHub Actions deployment workflow.
- Trigger the pipeline by merging a PR and observe the result.
Set up OIDC authentication between GitHub and AWS
OIDC lets GitHub prove its identity to AWS without storing static credentials. You will create an identity provider in IAM, then create a role that GitHub can assume.
- In the AWS Management Console, navigate to the IAM service.
- In the left sidebar, click Identity providers.
- Click Add provider.
- Select OpenID Connect as the provider type.
- In the Provider URL field, enter:
https://token.actions.githubusercontent.com
- Click Get thumbprint.
- In the Audience field, enter:
sts.amazonaws.com
- Click Add provider.
You should see token.actions.githubusercontent.com listed under Identity providers.
What is OIDC?
OIDC is a protocol that lets one service prove its identity to another without sharing passwords or long-lived keys. GitHub issues a short-lived token for each workflow run, and AWS verifies that token before granting temporary credentials.
This means no static AWS access keys are stored in your repository. If someone forks your repo, they cannot assume your AWS role because the trust policy is scoped to your specific repository.
Now create an IAM role that GitHub Actions can assume.
- In the IAM left sidebar, click Roles.
- Click Create role.
- Select Web identity as the trusted entity type.
- For Identity provider, select token.actions.githubusercontent.com.
- For Audience, select sts.amazonaws.com.
- Click Next.
- Search for and select the AWSElasticBeanstalkWebTier policy.
- Search for and also select the AmazonS3FullAccess policy.
- Click Next.
- Name the role github-actions-eb-deploy.
- Click Create role.
Next, update the role's trust policy to scope it to your specific repository.
- In the Roles list, click on the github-actions-eb-deploy role you just created.
- Click the Trust relationships tab.
- Click Edit trust policy.
- Replace the entire policy with the following JSON, substituting your GitHub username and repository name:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::[[AWS_ACCOUNT_ID=\"your 12-digit AWS account ID\"]]]:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:[[GITHUB_USERNAME=\"your-github-username\"]]/laravel-eb-deploy:*"
}
}
}
]
}
What does this trust policy do?
The StringLike condition on token.actions.githubusercontent.com:sub restricts this role to tokens issued from your specific repository. The wildcard :* at the end allows any branch or pull request from that repo to assume the role.
The StringEquals condition on token.actions.githubusercontent.com:aud ensures only tokens intended for AWS (audience sts.amazonaws.com) are accepted.
- Click Update policy.
- Copy the role ARN from the top of the role summary page. It looks like arn:aws:iam::123456789012:role/github-actions-eb-deploy. You will need this in the next step.
Now store the role ARN as a secret in your GitHub repository.
- Open your laravel-eb-deploy repository on github.com.
- Click Settings in the repository navigation.
- In the left sidebar under Security, click Secrets and variables, then Actions.
- Click New repository secret.
- Set the name to AWS_ROLE_TO_ASSUME.
- Paste the role ARN you copied as the value.
- Click Add secret.
You should see AWS_ROLE_TO_ASSUME listed under repository secrets.
Don't see the Settings tab?
The Settings tab only appears if you are the repository owner. Make sure you are viewing the repository you created (not a fork or someone else's repo).
help me find the Settings tab in my GitHub repository
Create the GitHub Actions workflow file
The workflow file tells GitHub Actions what to do when a PR is merged. You will create it locally and push it to a feature branch.
- In your terminal, make sure you are inside the laravel-eb-deploy project folder.
- Create the workflow directory by running:
mkdir -p .github/workflows
- Create the workflow file by running:
touch .github/workflows/deploy.yml
- Confirm the file was created by running:
ls .github/workflows
You should see deploy.yml in the output.
- Open deploy.yml in your editor and paste the following workflow configuration:
name: Deploy to Elastic Beanstalk
on:
pull_request:
types:
- closed
branches:
- main
env:
AWS_REGION: us-east-1
APPLICATION_NAME: laravel-eb-deploy
ENVIRONMENT_NAME: laravel-eb-deploy-env
What does this configuration do?
The on.pull_request.types: [closed] trigger fires when any PR targeting main is closed. The job's if condition (coming next) filters to only merged PRs. This is the GitHub-documented pattern for triggering a workflow exclusively on PR merge.
The env block defines reusable values so you do not repeat them in every step.
- Below the env block, add the jobs section:
jobs:
deploy:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
aws-region: ${{ env.AWS_REGION }}
What do these steps do?
The if: github.event.pull_request.merged == true condition ensures the job only runs when a PR is actually merged, not just closed without merging.
The permissions: id-token: write line grants the workflow permission to request an OIDC token from GitHub. Without this, the AWS credentials action cannot authenticate.
The aws-actions/configure-aws-credentials@v4 action exchanges the GitHub OIDC token for temporary AWS credentials using the role ARN stored in your secret.
- Below the Configure AWS Credentials step, add the deploy step:
- name: Deploy to Elastic Beanstalk
uses: aws-actions/aws-elasticbeanstalk-deploy@v1.0.0
with:
aws-region: ${{ env.AWS_REGION }}
application-name: ${{ env.APPLICATION_NAME }}
environment-name: ${{ env.ENVIRONMENT_NAME }}
version-label: ${{ github.sha }}
solution-stack-name: '64bit Amazon Linux 2023 v4.13.3 running PHP 8.4'
option-settings: |
[
{
"Namespace": "aws:autoscaling:launchconfiguration",
"OptionName": "IamInstanceProfile",
"Value": "aws-elasticbeanstalk-ec2-role"
},
{
"Namespace": "aws:elasticbeanstalk:environment",
"OptionName": "ServiceRole",
"Value": "aws-elasticbeanstalk-service-role"
}
]
What does the deploy step do?
The official aws-actions/aws-elasticbeanstalk-deploy@v1.0.0 action packages your code, uploads it to S3, creates an application version tagged with the commit SHA, and tells Elastic Beanstalk to deploy that version.
The option-settings pass the instance profile and service role that Elastic Beanstalk needs to manage the environment.
- Save .github/workflows/deploy.yml.
✔️ Awesome, I've got everything!
Double check you have saved the file, then move on to the next substep.
ⓧ I'd like to double check the full code
Your complete .github/workflows/deploy.yml should look like this:
name: Deploy to Elastic Beanstalk
on:
pull_request:
types:
- closed
branches:
- main
env:
AWS_REGION: us-east-1
APPLICATION_NAME: laravel-eb-deploy
ENVIRONMENT_NAME: laravel-eb-deploy-env
jobs:
deploy:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
Followed by the credential and deploy steps:
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
aws-region: ${{ env.AWS_REGION }}
- name: Deploy to Elastic Beanstalk
uses: aws-actions/aws-elasticbeanstalk-deploy@v1.0.0
with:
aws-region: ${{ env.AWS_REGION }}
application-name: ${{ env.APPLICATION_NAME }}
environment-name: ${{ env.ENVIRONMENT_NAME }}
version-label: ${{ github.sha }}
solution-stack-name: '64bit Amazon Linux 2023 v4.13.3 running PHP 8.4'
option-settings: |
[
{
"Namespace": "aws:autoscaling:launchconfiguration",
"OptionName": "IamInstanceProfile",
"Value": "aws-elasticbeanstalk-ec2-role"
},
{
"Namespace": "aws:elasticbeanstalk:environment",
"OptionName": "ServiceRole",
"Value": "aws-elasticbeanstalk-service-role"
}
]
Trigger the pipeline and observe the designed obstacle
The workflow only triggers on PR merge. You need to push to a feature branch, open a PR, and merge it.
- Create a feature branch and commit the workflow file by running:
git checkout -b add-deploy-workflow
- Stage and commit the new file by running:
git add .github/workflows/deploy.yml
git commit -m "Add GitHub Actions deploy workflow"
- Push the branch to GitHub by running:
git push -u origin add-deploy-workflow
- On github.com, navigate to your laravel-eb-deploy repository.
- You should see a banner suggesting you open a pull request for your recently pushed branch. Click Compare & pull request.
- Leave the title as-is and click Create pull request.
- Once the PR is open, click Merge pull request, then Confirm merge.
Before you check the result, think about this: you deployed the raw Laravel project to Elastic Beanstalk. Do you think the site will show the Laravel welcome page, or something else?
- Click the Actions tab in your repository.
- Click on the running workflow to watch it. Wait for the green checkmark (this takes 2-5 minutes).
The workflow completes successfully with a green checkmark. The pipeline worked.
- Now visit your Elastic Beanstalk environment URL in the browser (the same URL from the previous step that showed the sample app).
You see a 403 Forbidden page or the default nginx welcome page. The Laravel welcome page is nowhere to be found.
This is expected. Your pipeline works, but the site is broken.
The deploy succeeded at the pipeline level. GitHub Actions packaged your code, uploaded it, and Elastic Beanstalk deployed it. But nginx is trying to serve from the project root directory, and there is no index.php there.
Laravel's entry point lives in the /public directory. Elastic Beanstalk does not know this. It serves the project root by default, which produces a 403 because nginx sees no index file. You will fix this in the next step by adding EB configuration files.
Workflow failed instead of succeeding?
If your workflow shows a red X instead of a green checkmark, check these common causes:
- The trust policy sub condition does not match your repository. Double check the GitHub username is correct (case-sensitive) and the repository name is laravel-eb-deploy.
- The AWS_ROLE_TO_ASSUME secret is missing or has the wrong ARN. Confirm it matches the role ARN exactly.
- The IAM role is missing AmazonS3FullAccess or AWSElasticBeanstalkWebTier policies.
help me debug why my GitHub Actions deploy workflow is failing
Your pipeline is live and working end-to-end. The 403 proves that Elastic Beanstalk needs to be told where Laravel's entry point lives. Next up, you will add the EB configuration files that fix the document root, run artisan caching commands, and set storage permissions.
Fix the Deploy with EB Configuration
Your pipeline ran green. GitHub Actions successfully pushed your code to Elastic Beanstalk. But when you visited the environment URL, you got a 403 Forbidden or the default nginx page instead of the Laravel welcome screen.
The problem is that Elastic Beanstalk serves your project root by default. Laravel's entry point lives in the /public directory, so nginx has no index.php to find at the root. On top of that, Laravel needs write access to its storage and bootstrap/cache directories. This step adds the configuration files that fix both issues.
In this step, get ready to:
- Create an .ebextensions config file that points the document root to /public.
- Create a platform hook script that caches Laravel config and sets storage permissions on every deploy.
- Deploy the fix through the pipeline and confirm the Laravel welcome page loads.
Create the document root configuration
Elastic Beanstalk's PHP platform reads .ebextensions files during deployment and applies the settings to the environment. You will tell EB to serve your app from the /public folder instead of the project root.
- In your terminal, make sure you are inside the laravel-eb-deploy project folder.
- Create the .ebextensions directory and the config file by running these commands:
mkdir -p .ebextensions
touch .ebextensions/01_laravel.config
- Open .ebextensions/01_laravel.config in your editor and paste the following:
option_settings:
aws:elasticbeanstalk:container:php:phpini:
document_root: /public
What does this config do?
The aws:elasticbeanstalk:container:php:phpini namespace controls PHP-level settings on the EB environment. Setting document_root: /public tells nginx to serve requests from your Laravel public/ folder, where index.php lives.
Without this, nginx looks for index.php at the project root and finds nothing. That is exactly why you saw the 403.
- Save .ebextensions/01_laravel.config.
- Confirm the file exists by running:
ls .ebextensions/
You should see 01_laravel.config listed.
Don't see the file?
Make sure you ran mkdir -p .ebextensions from the project root (the laravel-eb-deploy folder). If ls .ebextensions/ returns "No such file or directory", you are in the wrong folder.
help me find and fix my .ebextensions directory
Create the post-deploy hook script
The document root fix gets nginx pointing to the right folder. But Laravel also needs its configuration cached and its storage directory writable. Platform hooks run automatically after each deployment completes.
- Create the nested directory structure for the hook script by running:
mkdir -p .platform/hooks/postdeploy
- Create the hook script file by running:
touch .platform/hooks/postdeploy/01_laravel_setup.sh
- Open .platform/hooks/postdeploy/01_laravel_setup.sh in your editor and paste the following:
#!/bin/bash
set -e
cd /var/app/current
# Cache configuration for production performance
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Set storage permissions
chmod -R 775 storage bootstrap/cache
chown -R webapp:webapp storage bootstrap/cache
What does this script do?
This script runs as root on the EC2 instance after every successful deployment. It does two things:
- Caches Laravel's configuration, routes, and views so the app does not re-parse them on every request. This improves production performance.
- Sets the storage and bootstrap/cache directories to be writable by the webapp user. Without this, Laravel cannot write logs or compiled views.
The /var/app/current path is where Elastic Beanstalk places your deployed application on Amazon Linux 2023.
- Save .platform/hooks/postdeploy/01_laravel_setup.sh.
- Confirm both new paths exist by running:
ls .platform/hooks/postdeploy/
You should see 01_laravel_setup.sh listed.
Do I need to chmod +x the hook script?
On Amazon Linux 2023 platforms, Elastic Beanstalk automatically grants execute permissions to hook scripts during deployment. You do not need to manually set the execute bit for the script to run on EB.
File not showing up?
Double check that your directory path is exactly .platform/hooks/postdeploy/ (not .platforms or platform without the dot). EB only recognizes .platform as the hook directory name.
help me check my .platform directory structure
✔️ Awesome, I've got everything!
Great. Make sure both files are saved. You are ready to deploy the fix.
ⓧ I'd like to double check the full code
Here is .ebextensions/01_laravel.config:
option_settings:
aws:elasticbeanstalk:container:php:phpini:
document_root: /public
Here is .platform/hooks/postdeploy/01_laravel_setup.sh:
#!/bin/bash
set -e
cd /var/app/current
# Cache configuration for production performance
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Set storage permissions
chmod -R 775 storage bootstrap/cache
chown -R webapp:webapp storage bootstrap/cache
Deploy the fix through the pipeline
You have the two config files that fix the 403. Now you will push them through the same PR workflow and let the pipeline redeploy your app.
- Create a new branch for the fix by running:
git checkout -b fix/eb-laravel-config
- Stage and commit the new files by running:
git add .ebextensions/ .platform/
git commit -m "Add EB config for Laravel document root and storage permissions"
- Push the branch to GitHub by running:
git push origin fix/eb-laravel-config
- Open your repository on GitHub in your browser.
- You should see a banner offering to open a pull request for the fix/eb-laravel-config branch. Click Compare & pull request.
- Give the PR a title like Fix EB config for Laravel and click Create pull request.
- Click Merge pull request, then Confirm merge.
Merging the PR triggers your GitHub Actions workflow. Before you check the result, take a moment to predict what will happen.
What do you expect to see this time?
Last time, the pipeline succeeded but the site showed a 403. Now you have added the document root config and the storage permissions script. Do you think the Laravel welcome page will load this time?
- Switch to the Actions tab in your GitHub repository.
- Click the running workflow to watch it progress. Wait for the green checkmark (this takes 1-2 minutes).
- Once the workflow completes, return to the Elastic Beanstalk console and click the environment URL for laravel-eb-deploy-env.
You should see the Laravel welcome page. The full Laravel landing page with its default styling confirms that nginx is serving from /public, artisan caching ran, and storage permissions are correct.
Still seeing a 403 or 500 error?
- Check the workflow completed successfully (green checkmark) on the Actions tab. If it failed, click into the logs to see what went wrong.
- Make sure the EB environment finished updating. In the Elastic Beanstalk console, the environment health should show Ok (green). If it still shows Updating, wait another minute.
- Verify your .ebextensions/01_laravel.config file has document_root: /public (not document_root: public without the leading slash).
help me debug why my Laravel app still shows an error after deploying the EB config files
You now have a fully automated deployment pipeline. Every time you merge a PR to main, GitHub Actions deploys your Laravel app to Elastic Beanstalk with the correct document root and storage permissions. Next up, you will add automated testing as a deployment gate so broken code never reaches production.
Secret mission
Add a PHPUnit Test Gate
Your pipeline deploys on every PR merge. But what happens when broken code slips through? Right now, nothing stops a syntax error or failed feature from going live. Add an automated test step that blocks deployment if tests fail.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Cost warning
Your Elastic Beanstalk environment runs a t3.micro EC2 instance continuously. This counts against your AWS free tier allocation of 750 hours per month. If you have other EC2 instances running, you may exceed the free tier and incur charges.
If you are not planning to use this environment again soon, choose Pause or Delete below to avoid unexpected costs.
Resources you used:
- AWS Elastic Beanstalk application and environment (laravel-eb-deploy application, laravel-eb-deploy-env environment running a t3.micro EC2 instance).
- IAM OIDC identity provider (token.actions.githubusercontent.com).
- IAM role for GitHub Actions (with AWSElasticBeanstalkWebTier and AmazonS3FullAccess policies).
- S3 bucket for Elastic Beanstalk deployment versions (auto-created, named elasticbeanstalk-*).
- Local laravel-eb-deploy project folder on your Desktop.
- GitHub repository containing your Laravel app and workflow.
Keep everything running
No action needed. Choose this if you are still actively building on this project or plan to trigger more deploys soon.
- Your Elastic Beanstalk environment stays live and accessible at its public URL.
- Monitor your AWS billing dashboard to ensure you stay within free tier limits.
- Your GitHub Actions workflow will trigger on every merged PR to main.
Pause - I'll come back to this later
Shut down the running EC2 instance to stop free tier hour consumption. Your application configuration and deployment versions are preserved so you can recreate the environment later.
Elastic Beanstalk does not have a native pause feature. To stop charges, terminate the environment while keeping the application.
- Open the Elastic Beanstalk console and select your AWS Region.
- In the navigation pane, choose Environments, then choose laravel-eb-deploy-env.
- Choose Actions, then choose Terminate environment.
- Confirm by typing the environment name and choose Terminate.
When you are ready to return, create a new environment under the same laravel-eb-deploy application and merge a PR to trigger a fresh deploy.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below to ensure nothing is left running.
Terminate the Elastic Beanstalk environment and delete the application
- Open the Elastic Beanstalk console and select your AWS Region.
- In the navigation pane, choose Environments, then choose laravel-eb-deploy-env.
- Choose Actions, then choose Terminate environment.
- Confirm by typing the environment name and choose Terminate. Wait a few minutes for termination to complete.
- In the navigation pane, choose Applications, then select laravel-eb-deploy.
- Choose Actions, then choose Delete application.
- Confirm by typing the application name and choose Delete.
Delete the IAM OIDC provider and GitHub Actions role
- Open the IAM console at https://console.aws.amazon.com/iam/.
- In the navigation pane, choose Identity providers.
- Select the checkbox next to token.actions.githubusercontent.com.
- Type delete in the confirmation field, then choose Delete.
- In the navigation pane, choose Roles.
- Search for and select the GitHub Actions role you created for this project.
- Choose Delete, type the role name to confirm, and choose Delete.
Delete the Elastic Beanstalk S3 bucket
- Open the S3 console.
- Search for a bucket whose name starts with elasticbeanstalk- followed by your region and account ID.
- Select the bucket, choose Empty, confirm by typing permanently delete, then choose Empty.
- Select the bucket again, choose Delete, type the bucket name to confirm, then choose Delete bucket.
Delete the local project folder
- Delete the local project folder by running this command:
cd ~/Desktop
rm -rf laravel-eb-deploy
On Windows, run this instead:
cd ~\Desktop
Remove-Item -Recurse -Force laravel-eb-deploy
Delete the GitHub repository (optional)
- On GitHub, navigate to your repository.
- Choose Settings, then scroll to the bottom of the page.
- Under Danger Zone, choose Delete this repository.
- Follow the prompts to confirm deletion.
That's a Wrap!
That's a Wrap!
Nice work! You have built a production-style CI/CD pipeline that automatically deploys your Laravel application to AWS Elastic Beanstalk every time a pull request is merged to main.
You've learned how to:
- Build a complete CI/CD pipeline using GitHub Actions and the official aws-actions/aws-elasticbeanstalk-deploy@v1.0.0 action with secure OIDC authentication instead of stored AWS keys.
- Configure Elastic Beanstalk for Laravel production using .ebextensions for document root settings and platform hooks for artisan caching and storage permissions.
- Diagnose and fix a real deployment failure (403 without document root config) by pushing a fix through the pipeline itself, proving the CI/CD workflow handles both broken and working deploys.
- Secret Mission: Added automated PHPUnit testing as a deployment gate, blocking broken code from reaching production.
Ready to quiz yourself?