{"id":431,"date":"2025-08-27T21:50:52","date_gmt":"2025-08-27T21:50:52","guid":{"rendered":"https:\/\/1v0.net\/blog\/?p=431"},"modified":"2025-08-27T21:50:54","modified_gmt":"2025-08-27T21:50:54","slug":"deploying-laravel-on-aws-complete-guide-2025","status":"publish","type":"post","link":"https:\/\/1v0.net\/blog\/deploying-laravel-on-aws-complete-guide-2025\/","title":{"rendered":"Deploying Laravel on AWS: Complete Guide (2025)"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\"><strong>Deploying Laravel on AWS: Complete Guide (2025)<\/strong><\/h2>\n\n\n\n<p>AWS offers multiple reliable paths to production: EC2 (you manage the box), Elastic Beanstalk (PaaS-like), and ECS Fargate (serverless containers). This guide gives you a modern end-to-end recipe: robust networking and IAM, RDS + ElastiCache + S3\/CloudFront, ALB health checks, secure env management, CI\/CD with GitHub Actions OIDC, and two deployment tracks (EC2 and ECS Fargate). Where relevant, we\u2019ll link deeper dives like <a href=\"\/blog\/optimizing-laravel-for-aws-deployment-step-by-step\">#49 AWS Step-by-Step<\/a>, <a href=\"\/blog\/cicd-for-laravel-projects-with-github-actions\">#54 CI\/CD<\/a>, and <a href=\"\/blog\/laravel-nginx-best-practices-for-production\">#56 Nginx Best Practices<\/a>.<\/p>\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>1 \u2014 Choose an Architecture<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>EC2 + Nginx + PHP-FPM<\/strong>: maximum control, easy to reason about; scale with an ALB + Auto Scaling Group. See the hands-on <a href=\"\/blog\/optimizing-laravel-for-aws-deployment-step-by-step\">#49<\/a>.<\/li>\n<li><strong>ECS Fargate<\/strong>: serverless containers (no servers to manage). Great for blue\/green and predictable costs.<\/li>\n<li><strong>Elastic Beanstalk<\/strong>: PaaS convenience, but fewer knobs than ECS\/EC2.<\/li>\n<\/ul>\n\n\n\n<p>Common shared services regardless of path: <strong>RDS<\/strong> (MySQL\/Postgres), <strong>ElastiCache (Redis)<\/strong> for cache\/sessions\/queues, <strong>S3 + CloudFront<\/strong> for assets\/uploads, <strong>ALB<\/strong> for routing and health checks, <strong>Parameter Store\/Secrets Manager<\/strong> for secrets, and <strong>CloudWatch<\/strong> for logs\/metrics.<\/p>\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>2 \u2014 IAM for GitHub Actions (OIDC)<\/strong><\/h2>\n\n\n\n<p>Use GitHub\u2019s OIDC to let Actions assume an AWS role without long-lived keys. Create an IAM role with this trust policy:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\">{\n  <span class=\"hljs-attr\">\"Version\"<\/span>: <span class=\"hljs-string\">\"2012-10-17\"<\/span>,\n  <span class=\"hljs-attr\">\"Statement\"<\/span>: &#91;{\n    <span class=\"hljs-attr\">\"Effect\"<\/span>: <span class=\"hljs-string\">\"Allow\"<\/span>,\n    <span class=\"hljs-attr\">\"Principal\"<\/span>: { <span class=\"hljs-attr\">\"Federated\"<\/span>: <span class=\"hljs-string\">\"arn:aws:iam::123456789012:oidc-provider\/token.actions.githubusercontent.com\"<\/span> },\n    <span class=\"hljs-attr\">\"Action\"<\/span>: <span class=\"hljs-string\">\"sts:AssumeRoleWithWebIdentity\"<\/span>,\n    <span class=\"hljs-attr\">\"Condition\"<\/span>: {\n      <span class=\"hljs-attr\">\"StringLike\"<\/span>: { <span class=\"hljs-attr\">\"token.actions.githubusercontent.com:sub\"<\/span>: <span class=\"hljs-string\">\"repo:your-org\/your-repo:*\"<\/span> },\n      <span class=\"hljs-attr\">\"StringEquals\"<\/span>: { <span class=\"hljs-attr\">\"token.actions.githubusercontent.com:aud\"<\/span>: <span class=\"hljs-string\">\"sts.amazonaws.com\"<\/span> }\n    }\n  }]\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This limits role assumption to your repository. Attach policies permitting S3 (artifacts), CodeDeploy\/ECS (deployments), or SSM\/EC2 (remote commands). You\u2019ll use this role in the CI\/CD workflow below. See also <a href=\"\/blog\/cicd-for-laravel-projects-with-github-actions\">#54 CI\/CD<\/a>.<\/p>\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>3 \u2014 CI\/CD (GitHub Actions \u279c AWS CodeDeploy on EC2)<\/strong><\/h2>\n\n\n\n<p>This workflow builds, uploads an artifact to S3, and triggers CodeDeploy to roll it onto your EC2 Auto Scaling Group with zero downtime.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"YAML\" data-shcb-language-slug=\"yaml\"><span><code class=\"hljs language-yaml\"><span class=\"hljs-comment\"># .github\/workflows\/deploy.yml<\/span>\n<span class=\"hljs-attr\">name:<\/span> <span class=\"hljs-string\">Deploy<\/span> <span class=\"hljs-string\">to<\/span> <span class=\"hljs-string\">AWS<\/span> <span class=\"hljs-string\">(EC2<\/span> <span class=\"hljs-string\">via<\/span> <span class=\"hljs-string\">CodeDeploy)<\/span>\n<span class=\"hljs-attr\">on:<\/span>\n  <span class=\"hljs-attr\">push:<\/span>\n    <span class=\"hljs-attr\">branches:<\/span> <span class=\"hljs-string\">&#91;<\/span> <span class=\"hljs-string\">main<\/span> <span class=\"hljs-string\">]<\/span>\n<span class=\"hljs-attr\">jobs:<\/span>\n  <span class=\"hljs-attr\">deploy:<\/span>\n    <span class=\"hljs-attr\">runs-on:<\/span> <span class=\"hljs-string\">ubuntu-latest<\/span>\n    <span class=\"hljs-attr\">permissions:<\/span>\n      <span class=\"hljs-attr\">id-token:<\/span> <span class=\"hljs-string\">write<\/span>\n      <span class=\"hljs-attr\">contents:<\/span> <span class=\"hljs-string\">read<\/span>\n    <span class=\"hljs-attr\">steps:<\/span>\n      <span class=\"hljs-bullet\">-<\/span> <span class=\"hljs-attr\">uses:<\/span> <span class=\"hljs-string\">actions\/checkout@v4<\/span>\n      <span class=\"hljs-bullet\">-<\/span> <span class=\"hljs-attr\">uses:<\/span> <span class=\"hljs-string\">actions\/setup-node@v4<\/span>\n        <span class=\"hljs-attr\">with:<\/span> <span class=\"hljs-string\">{<\/span> <span class=\"hljs-attr\">node-version:<\/span> <span class=\"hljs-number\">20<\/span> <span class=\"hljs-string\">}<\/span>\n      <span class=\"hljs-bullet\">-<\/span> <span class=\"hljs-attr\">run:<\/span> <span class=\"hljs-string\">npm<\/span> <span class=\"hljs-string\">ci<\/span> <span class=\"hljs-string\">&amp;&amp;<\/span> <span class=\"hljs-string\">npm<\/span> <span class=\"hljs-string\">run<\/span> <span class=\"hljs-string\">build<\/span>\n      <span class=\"hljs-bullet\">-<\/span> <span class=\"hljs-attr\">uses:<\/span> <span class=\"hljs-string\">php-actions\/composer@v6<\/span>\n        <span class=\"hljs-attr\">with:<\/span> <span class=\"hljs-string\">{<\/span> <span class=\"hljs-attr\">php_version:<\/span> <span class=\"hljs-string\">\"8.3\"<\/span><span class=\"hljs-string\">,<\/span> <span class=\"hljs-attr\">args:<\/span> <span class=\"hljs-string\">\"--no-dev --optimize-autoloader\"<\/span> <span class=\"hljs-string\">}<\/span>\n      <span class=\"hljs-bullet\">-<\/span> <span class=\"hljs-attr\">run:<\/span> <span class=\"hljs-string\">|\n          php artisan config:cache\n          php artisan route:cache\n          php artisan view:cache\n<\/span>      <span class=\"hljs-bullet\">-<\/span> <span class=\"hljs-attr\">name:<\/span> <span class=\"hljs-string\">Archive<\/span> <span class=\"hljs-string\">artifact<\/span>\n        <span class=\"hljs-attr\">run:<\/span> <span class=\"hljs-string\">zip<\/span> <span class=\"hljs-string\">-r<\/span> <span class=\"hljs-string\">deploy.zip<\/span> <span class=\"hljs-string\">.<\/span> <span class=\"hljs-string\">-x<\/span> <span class=\"hljs-string\">\".git\/*\"<\/span>\n      <span class=\"hljs-bullet\">-<\/span> <span class=\"hljs-attr\">name:<\/span> <span class=\"hljs-string\">Configure<\/span> <span class=\"hljs-string\">AWS<\/span> <span class=\"hljs-string\">creds<\/span> <span class=\"hljs-string\">(OIDC)<\/span>\n        <span class=\"hljs-attr\">uses:<\/span> <span class=\"hljs-string\">aws-actions\/configure-aws-credentials@v4<\/span>\n        <span class=\"hljs-attr\">with:<\/span>\n          <span class=\"hljs-attr\">role-to-assume:<\/span> <span class=\"hljs-string\">arn:aws:iam::123456789012:role\/GitHubDeployRole<\/span>\n          <span class=\"hljs-attr\">aws-region:<\/span> <span class=\"hljs-string\">us-east-1<\/span>\n      <span class=\"hljs-bullet\">-<\/span> <span class=\"hljs-attr\">name:<\/span> <span class=\"hljs-string\">Upload<\/span> <span class=\"hljs-string\">to<\/span> <span class=\"hljs-string\">S3<\/span>\n        <span class=\"hljs-attr\">run:<\/span> <span class=\"hljs-string\">aws<\/span> <span class=\"hljs-string\">s3<\/span> <span class=\"hljs-string\">cp<\/span> <span class=\"hljs-string\">deploy.zip<\/span> <span class=\"hljs-string\">s3:\/\/your-artifacts-bucket\/deploy.zip<\/span>\n      <span class=\"hljs-bullet\">-<\/span> <span class=\"hljs-attr\">name:<\/span> <span class=\"hljs-string\">Trigger<\/span> <span class=\"hljs-string\">CodeDeploy<\/span>\n        <span class=\"hljs-attr\">run:<\/span> <span class=\"hljs-string\">|\n          aws deploy create-deployment \\\n            --application-name laravel-app \\\n            --deployment-group-name laravel-asg \\\n            --s3-location bucket=your-artifacts-bucket,key=deploy.zip,bundleType=zip<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">YAML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">yaml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Actions uses OIDC to assume your AWS role (no access keys). CodeDeploy handles the rolling update across instances. For a fully scripted EC2 approach without CodeDeploy, see the shell-driven flow in <a href=\"\/blog\/optimizing-laravel-for-aws-deployment-step-by-step\">#49<\/a>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"YAML\" data-shcb-language-slug=\"yaml\"><span><code class=\"hljs language-yaml\"><span class=\"hljs-comment\"># appspec.yml (at repo root, used by CodeDeploy)<\/span>\n<span class=\"hljs-attr\">version:<\/span> <span class=\"hljs-number\">0.0<\/span>\n<span class=\"hljs-attr\">os:<\/span> <span class=\"hljs-string\">linux<\/span>\n<span class=\"hljs-attr\">files:<\/span>\n  <span class=\"hljs-bullet\">-<\/span> <span class=\"hljs-attr\">source:<\/span> <span class=\"hljs-string\">\/<\/span>\n    <span class=\"hljs-attr\">destination:<\/span> <span class=\"hljs-string\">\/var\/www\/releases\/{{deployment-id}}<\/span>\n<span class=\"hljs-attr\">hooks:<\/span>\n  <span class=\"hljs-attr\">AfterInstall:<\/span>\n    <span class=\"hljs-bullet\">-<\/span> <span class=\"hljs-attr\">location:<\/span> <span class=\"hljs-string\">scripts\/after_install.sh<\/span>\n      <span class=\"hljs-attr\">timeout:<\/span> <span class=\"hljs-number\">300<\/span>\n  <span class=\"hljs-attr\">ApplicationStart:<\/span>\n    <span class=\"hljs-bullet\">-<\/span> <span class=\"hljs-attr\">location:<\/span> <span class=\"hljs-string\">scripts\/start.sh<\/span>\n      <span class=\"hljs-attr\">timeout:<\/span> <span class=\"hljs-number\">300<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">YAML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">yaml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><code>appspec.yml<\/code> tells CodeDeploy where to unpack and which hook scripts to run (composer, artisan cache, symlink swap, FPM\/Nginx reload). This pattern mirrors the zero-downtime release layout from <a href=\"\/blog\/optimizing-laravel-for-aws-deployment-step-by-step\">#49<\/a>.<\/p>\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>4 \u2014 EC2 User-Data Bootstrap (One-Time)<\/strong><\/h2>\n\n\n\n<p>Use launch templates with user-data to preinstall Nginx\/PHP and create paths expected by CodeDeploy.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\"><span class=\"hljs-meta\">#!\/bin\/bash<\/span>\n<span class=\"hljs-built_in\">set<\/span> -e\napt-get update\napt-get install -y nginx php8.3-fpm php8.3-xml php8.3-mbstring php8.3-zip php8.3-mysql php8.3-bcmath php8.3-curl unzip git\nsystemctl <span class=\"hljs-built_in\">enable<\/span> nginx php8.3-fpm\nmkdir -p \/var\/www\/releases \/var\/www\/shared \/var\/www\/current\nchown -R www-data:www-data \/var\/www\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This ensures new instances in the Auto Scaling Group are ready for CodeDeploy to drop releases into <code>\/var\/www\/releases<\/code> and flip the <code>current<\/code> symlink. For Nginx hardening, see <a href=\"\/blog\/laravel-nginx-best-practices-for-production\">#56<\/a>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"Nginx\" data-shcb-language-slug=\"nginx\"><span><code class=\"hljs language-nginx\"><span class=\"hljs-comment\"># \/etc\/nginx\/sites-available\/laravel.conf<\/span>\n<span class=\"hljs-section\">server<\/span> {\n  <span class=\"hljs-attribute\">listen<\/span> <span class=\"hljs-number\">80<\/span>;\n  <span class=\"hljs-attribute\">server_name<\/span> _;\n  <span class=\"hljs-attribute\">root<\/span> \/var\/www\/current\/public;\n  <span class=\"hljs-attribute\">index<\/span> index.php index.html;\n\n  <span class=\"hljs-attribute\">location<\/span> \/ { <span class=\"hljs-attribute\">try_files<\/span> <span class=\"hljs-variable\">$uri<\/span> <span class=\"hljs-variable\">$uri<\/span>\/ \/index.php?<span class=\"hljs-variable\">$query_string<\/span>; }\n\n  <span class=\"hljs-attribute\">location<\/span> <span class=\"hljs-regexp\">~ \\.php$<\/span> {\n    <span class=\"hljs-attribute\">include<\/span> snippets\/fastcgi-php.conf;\n    <span class=\"hljs-attribute\">fastcgi_pass<\/span> unix:\/run\/php\/php8.3-fpm.sock;\n    <span class=\"hljs-attribute\">fastcgi_read_timeout<\/span> <span class=\"hljs-number\">60s<\/span>;\n  }\n\n  <span class=\"hljs-attribute\">location<\/span> <span class=\"hljs-regexp\">~* \\.(css|js|jpg|jpeg|png|gif|webp|ico|woff2?)$<\/span> {\n    <span class=\"hljs-attribute\">expires<\/span> <span class=\"hljs-number\">7d<\/span>; <span class=\"hljs-attribute\">access_log<\/span> <span class=\"hljs-literal\">off<\/span>;\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Nginx<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">nginx<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Point the site root at the <code>current<\/code> symlink so deploys are atomic. Test and reload Nginx in your hook scripts.<\/p>\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>5 \u2014 RDS, ElastiCache, S3\/CloudFront<\/strong><\/h2>\n\n\n\n<p>Plug AWS services into Laravel via <code>.env<\/code> and <code>config\/<\/code>. Keep secrets in Parameter Store\/Secrets Manager, injected at deploy.<\/p>\n\n\n<!-- DomainException(0): Unknown language: \"dotenv\" --># .env.production (snippets)\nAPP_ENV=production\nAPP_DEBUG=false\nAPP_URL=https:\/\/your-domain.com\n\nDB_CONNECTION=mysql\nDB_HOST=your-rds.cluster-xxxx.us-east-1.rds.amazonaws.com\nDB_PORT=3306\nDB_DATABASE=app\nDB_USERNAME=app_user\nDB_PASSWORD=****    # better: load via SSM param at deploy\n\nCACHE_DRIVER=redis\nSESSION_DRIVER=redis\nREDIS_HOST=your-redis.xxxxxx.use1.cache.amazonaws.com\nREDIS_PORT=6379\n\nFILESYSTEM_DISK=s3\nAWS_BUCKET=your-bucket\nAWS_DEFAULT_REGION=us-east-1\nAWS_URL=https:\/\/dxxxxx.cloudfront.net\n\n\n<p>RDS handles the relational workload; ElastiCache powers cache\/sessions\/queues; S3 stores uploads; CloudFront serves them globally. See <a href=\"\/blog\/caching-strategies-in-laravel-redis-vs-database-vs-file\">#43 Caching<\/a> for why Redis matters under load.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ config\/filesystems.php (snippet)<\/span>\n<span class=\"hljs-string\">'disks'<\/span> =&gt; &#91;\n  <span class=\"hljs-string\">'s3'<\/span> =&gt; &#91;\n    <span class=\"hljs-string\">'driver'<\/span> =&gt; <span class=\"hljs-string\">'s3'<\/span>,\n    <span class=\"hljs-string\">'key'<\/span>    =&gt; env(<span class=\"hljs-string\">'AWS_ACCESS_KEY_ID'<\/span>),\n    <span class=\"hljs-string\">'secret'<\/span> =&gt; env(<span class=\"hljs-string\">'AWS_SECRET_ACCESS_KEY'<\/span>),\n    <span class=\"hljs-string\">'region'<\/span> =&gt; env(<span class=\"hljs-string\">'AWS_DEFAULT_REGION'<\/span>, <span class=\"hljs-string\">'us-east-1'<\/span>),\n    <span class=\"hljs-string\">'bucket'<\/span> =&gt; env(<span class=\"hljs-string\">'AWS_BUCKET'<\/span>),\n    <span class=\"hljs-string\">'url'<\/span>    =&gt; env(<span class=\"hljs-string\">'AWS_URL'<\/span>), <span class=\"hljs-comment\">\/\/ CloudFront URL for public reads<\/span>\n    <span class=\"hljs-string\">'visibility'<\/span> =&gt; <span class=\"hljs-string\">'public'<\/span>,\n  ],\n],<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Setting <code>AWS_URL<\/code> to your CloudFront domain makes <code>Storage::url()<\/code> emit CDN links automatically (immutable hashed file names = long cache TTLs).<\/p>\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>6 \u2014 ALB Health Checks<\/strong><\/h2>\n\n\n\n<p>Create a lightweight health route; point the ALB target group to it for safe rolling updates.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ routes\/web.php<\/span>\nRoute::get(<span class=\"hljs-string\">'\/health'<\/span>, <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">()<\/span> <\/span>{\n  <span class=\"hljs-keyword\">try<\/span> {\n    DB::connection()-&gt;getPdo();\n    Cache::put(<span class=\"hljs-string\">'hc'<\/span>, now(), <span class=\"hljs-number\">5<\/span>);\n    <span class=\"hljs-keyword\">return<\/span> response()-&gt;json(&#91;<span class=\"hljs-string\">'ok'<\/span> =&gt; <span class=\"hljs-keyword\">true<\/span>, <span class=\"hljs-string\">'t'<\/span> =&gt; now()], <span class=\"hljs-number\">200<\/span>);\n  } <span class=\"hljs-keyword\">catch<\/span> (\\Throwable $e) {\n    <span class=\"hljs-keyword\">return<\/span> response()-&gt;json(&#91;<span class=\"hljs-string\">'ok'<\/span> =&gt; <span class=\"hljs-keyword\">false<\/span>], <span class=\"hljs-number\">500<\/span>);\n  }\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The route checks DB and Redis quickly. During deploys, instances failing health are drained by ALB before termination. We used the same pattern in <a href=\"\/blog\/optimizing-laravel-for-aws-deployment-step-by-step\">#49<\/a>.<\/p>\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>7 \u2014 ECS Fargate Option (Containers, No Servers)<\/strong><\/h2>\n\n\n\n<p>Build a Docker image for your Laravel app (PHP-FPM) and run it behind Nginx in a two-container Task. Fargate handles capacity and patching.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"Dockerfile\" data-shcb-language-slug=\"dockerfile\"><span><code class=\"hljs language-dockerfile\"><span class=\"hljs-comment\"># Dockerfile (app)<\/span>\n<span class=\"hljs-keyword\">FROM<\/span> php:<span class=\"hljs-number\">8.3<\/span>-fpm\n<span class=\"hljs-keyword\">RUN<\/span><span class=\"bash\"> apt-get update &amp;&amp; apt-get install -y git unzip libpq-dev libzip-dev libpng-dev &amp;&amp; \\\n    docker-php-ext-install pdo_mysql bcmath zip gd<\/span>\n<span class=\"hljs-keyword\">COPY<\/span><span class=\"bash\"> --from=composer:2.7 \/usr\/bin\/composer \/usr\/bin\/composer<\/span>\n<span class=\"hljs-keyword\">WORKDIR<\/span><span class=\"bash\"> \/var\/www<\/span>\n<span class=\"hljs-keyword\">COPY<\/span><span class=\"bash\"> . .<\/span>\n<span class=\"hljs-keyword\">RUN<\/span><span class=\"bash\"> composer install --no-dev --optimize-autoloader &amp;&amp; php artisan config:cache &amp;&amp; php artisan route:cache &amp;&amp; php artisan view:cache<\/span>\n<span class=\"hljs-keyword\">CMD<\/span><span class=\"bash\"> &#91;<span class=\"hljs-string\">\"php-fpm\"<\/span>]<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Dockerfile<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">dockerfile<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This image bakes in optimized caches so containers start fast. Store <code>.env<\/code> values in ECS task secrets sourced from Parameter Store\/Secrets Manager, not inside the image.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\">{\n  <span class=\"hljs-attr\">\"family\"<\/span>: <span class=\"hljs-string\">\"laravel-app\"<\/span>,\n  <span class=\"hljs-attr\">\"networkMode\"<\/span>: <span class=\"hljs-string\">\"awsvpc\"<\/span>,\n  <span class=\"hljs-attr\">\"requiresCompatibilities\"<\/span>: &#91;<span class=\"hljs-string\">\"FARGATE\"<\/span>],\n  <span class=\"hljs-attr\">\"cpu\"<\/span>: <span class=\"hljs-string\">\"512\"<\/span>,\n  <span class=\"hljs-attr\">\"memory\"<\/span>: <span class=\"hljs-string\">\"1024\"<\/span>,\n  <span class=\"hljs-attr\">\"containerDefinitions\"<\/span>: &#91;\n    {\n      <span class=\"hljs-attr\">\"name\"<\/span>: <span class=\"hljs-string\">\"app\"<\/span>,\n      <span class=\"hljs-attr\">\"image\"<\/span>: <span class=\"hljs-string\">\"123456789012.dkr.ecr.us-east-1.amazonaws.com\/laravel:latest\"<\/span>,\n      <span class=\"hljs-attr\">\"portMappings\"<\/span>: &#91;{ <span class=\"hljs-attr\">\"containerPort\"<\/span>: <span class=\"hljs-number\">9000<\/span>, <span class=\"hljs-attr\">\"protocol\"<\/span>: <span class=\"hljs-string\">\"tcp\"<\/span> }],\n      <span class=\"hljs-attr\">\"secrets\"<\/span>: &#91;\n        { <span class=\"hljs-attr\">\"name\"<\/span>: <span class=\"hljs-string\">\"DB_HOST\"<\/span>, <span class=\"hljs-attr\">\"valueFrom\"<\/span>: <span class=\"hljs-string\">\"arn:aws:ssm:us-east-1:123456789012:parameter\/app\/DB_HOST\"<\/span> }\n      ],\n      <span class=\"hljs-attr\">\"linuxParameters\"<\/span>: { <span class=\"hljs-attr\">\"initProcessEnabled\"<\/span>: <span class=\"hljs-literal\">true<\/span> },\n      <span class=\"hljs-attr\">\"essential\"<\/span>: <span class=\"hljs-literal\">true<\/span>\n    },\n    {\n      <span class=\"hljs-attr\">\"name\"<\/span>: <span class=\"hljs-string\">\"nginx\"<\/span>,\n      <span class=\"hljs-attr\">\"image\"<\/span>: <span class=\"hljs-string\">\"nginx:alpine\"<\/span>,\n      <span class=\"hljs-attr\">\"portMappings\"<\/span>: &#91;{ <span class=\"hljs-attr\">\"containerPort\"<\/span>: <span class=\"hljs-number\">80<\/span>, <span class=\"hljs-attr\">\"protocol\"<\/span>: <span class=\"hljs-string\">\"tcp\"<\/span> }],\n      <span class=\"hljs-attr\">\"mountPoints\"<\/span>: &#91;],\n      <span class=\"hljs-attr\">\"links\"<\/span>: &#91;<span class=\"hljs-string\">\"app\"<\/span>]\n    }\n  ]\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Two containers in one task: PHP-FPM on 9000 and Nginx on 80. The Service is fronted by an ALB with the health check path set to <code>\/health<\/code>. Blue\/green deployments become trivial with ECS.<\/p>\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>8 \u2014 Queues: Horizon on EC2\/ECS<\/strong><\/h2>\n\n\n\n<p>Run Horizon as a separate systemd service on EC2, or as a separate ECS Service using the same image but a different command.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"TOML, also INI\" data-shcb-language-slug=\"ini\"><span><code class=\"hljs language-ini\"><span class=\"hljs-comment\"># EC2: \/etc\/systemd\/system\/horizon.service<\/span>\n<span class=\"hljs-section\">&#91;Unit]<\/span>\n<span class=\"hljs-attr\">Description<\/span>=Laravel Horizon\n<span class=\"hljs-attr\">After<\/span>=network.target\n<span class=\"hljs-section\">&#91;Service]<\/span>\n<span class=\"hljs-attr\">User<\/span>=www-data\n<span class=\"hljs-attr\">WorkingDirectory<\/span>=\/var\/www\/current\n<span class=\"hljs-attr\">ExecStart<\/span>=\/usr\/bin\/php artisan horizon\n<span class=\"hljs-attr\">Restart<\/span>=always\n<span class=\"hljs-attr\">RestartSec<\/span>=<span class=\"hljs-number\">3<\/span>\n<span class=\"hljs-section\">&#91;Install]<\/span>\n<span class=\"hljs-attr\">WantedBy<\/span>=multi-user.target<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TOML, also INI<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">ini<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>For ECS, define a second Service running <code>php artisan horizon<\/code> (no port exposed) with desired count &gt;= 1 and CloudWatch Logs enabled. See <a href=\"\/blog\/how-to-use-laravel-horizon-for-queue-monitoring\">#45 Horizon<\/a> and <a href=\"\/blog\/how-to-use-laravel-queues-for-faster-performance\">#42 Queues<\/a>.<\/p>\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>9 \u2014 Observability &amp; Security<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Logs<\/strong>: ship Nginx\/PHP\/Laravel logs to CloudWatch. Use retention policies.<\/li>\n<li><strong>Telescope<\/strong>: secure <code>\/telescope<\/code> with a Gate; use for deep request\/query introspection (<a href=\"\/blog\/using-laravel-telescope-to-debug-performance-issues\">#48<\/a>).<\/li>\n<li><strong>Secrets<\/strong>: SSM Parameter Store \/ Secrets Manager. Avoid committing credentials.<\/li>\n<li><strong>WAF<\/strong>: attach AWS WAF to ALB or CloudFront for L7 protection.<\/li>\n<li><strong>OPcache\/Octane<\/strong>: enable OPcache; optionally run Octane for high concurrency (<a href=\"\/blog\/optimizing-laravel-for-high-concurrency-with-octane\">#44<\/a>).<\/li>\n<\/ul>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Providers\/TelescopeServiceProvider.php (gate snippet)<\/span>\n<span class=\"hljs-keyword\">protected<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">gate<\/span><span class=\"hljs-params\">()<\/span>\n<\/span>{\n  Gate::define(<span class=\"hljs-string\">'viewTelescope'<\/span>, fn ($user) =&gt; in_array($user-&gt;email, &#91;<span class=\"hljs-string\">'admin@example.com'<\/span>]));\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Locking down Telescope is essential in production; combine app-level auth with security groups and ALB rules. For a final pre-launch audit, use <a href=\"\/blog\/laravel-deployment-checklist-for-2025\">#58 Deployment Checklist<\/a>.<\/p>\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>10 \u2014 Minimal Admin Status UI<\/strong><\/h2>\n\n\n\n<p>A tiny Blade page for on-call engineers to sanity-check DB\/Redis and horizon queue sizes without shell access.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ routes\/web.php<\/span>\nRoute::middleware(&#91;<span class=\"hljs-string\">'auth'<\/span>, <span class=\"hljs-string\">'can:viewAdmin'<\/span>])-&gt;get(<span class=\"hljs-string\">'\/admin\/status'<\/span>, <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">()<\/span> <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> view(<span class=\"hljs-string\">'admin.status'<\/span>, &#91;\n    <span class=\"hljs-string\">'db'<\/span> =&gt; optional(DB::select(<span class=\"hljs-string\">'SELECT 1 as ok'<\/span>))&#91;<span class=\"hljs-number\">0<\/span>]-&gt;ok ?? <span class=\"hljs-number\">0<\/span>,\n    <span class=\"hljs-string\">'redis'<\/span> =&gt; Cache::put(<span class=\"hljs-string\">'status_ping'<\/span>, now(), <span class=\"hljs-number\">5<\/span>) === <span class=\"hljs-keyword\">null<\/span> ? <span class=\"hljs-number\">1<\/span> : <span class=\"hljs-number\">1<\/span>,\n    <span class=\"hljs-string\">'queueSize'<\/span> =&gt; Illuminate\\Support\\Facades\\Queue::size(),\n  ]);\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The route is gated and returns a Blade view with simple indicators. Use Horizon for full visibility, but this helps verify env quickly during incidents.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-comment\">&lt;!-- resources\/views\/admin\/status.blade.php --&gt;<\/span>\n@extends('layouts.app')\n@section('content')\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"container\"<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h1<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"mb-4\"<\/span>&gt;<\/span>System Status<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h1<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">ul<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"list-group\"<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">li<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"list-group-item\"<\/span>&gt;<\/span>DB: {{ $db ? 'OK' : 'FAIL' }}<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">li<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">li<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"list-group-item\"<\/span>&gt;<\/span>Redis: {{ $redis ? 'OK' : 'FAIL' }}<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">li<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">li<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"list-group-item\"<\/span>&gt;<\/span>Queue Size: {{ $queueSize }}<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">li<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">ul<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">a<\/span> <span class=\"hljs-attr\">href<\/span>=<span class=\"hljs-string\">\"\/horizon\"<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"btn btn-theme mt-3\"<\/span>&gt;<\/span>Open Horizon<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">a<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n@endsection<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Keep it minimal, authenticated, and non-sensitive. For real-time job internals, Horizon remains the primary dashboard.<\/p>\n\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\">Wrapping Up<\/h2>\n\n\n\n<p>You now have two solid AWS deployment paths for Laravel in 2025: EC2 with CodeDeploy (fine-grained control) and ECS Fargate (serverless containers). You wired RDS, ElastiCache, S3\/CloudFront, ALB health checks, and secure env management via OIDC + Parameter Store. Add Horizon for queues, Telescope for diagnostics, OPcache\/Octane for speed, and CloudWatch\/WAF for resilience. Pick the path that matches your ops maturity and scaling goals.<\/p>\n\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\">What\u2019s Next<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/blog\/optimizing-laravel-for-aws-deployment-step-by-step\">Optimizing Laravel for AWS Deployment (Step-by-Step)<\/a> \u2014 deeper, box-level configuration on EC2.<\/li>\n<li><a href=\"\/blog\/cicd-for-laravel-projects-with-github-actions\">CI\/CD for Laravel Projects with GitHub Actions<\/a> \u2014 pipelines, caching, and safe rollouts.<\/li>\n<li><a href=\"\/blog\/laravel-nginx-best-practices-for-production\">Laravel &amp; Nginx: Best Practices for Production<\/a> \u2014 timeouts, compression, and buffering.<\/li>\n<li><a href=\"\/blog\/laravel-deployment-checklist-for-2025\">Laravel Deployment Checklist for 2025<\/a> \u2014 run this before each release.<\/li>\n<li><a href=\"\/blog\/optimizing-laravel-for-high-concurrency-with-octane\">Optimizing Laravel for High Concurrency with Octane<\/a> \u2014 when you need serious RPS gains.<\/li>\n<\/ul>\n\n","protected":false},"excerpt":{"rendered":"<p>Deploying Laravel on AWS: Complete Guide (2025) AWS offers multiple reliable paths to production: EC2 (you manage the box), Elastic Beanstalk (PaaS-like), and ECS Fargate (serverless containers). This guide gives you a modern end-to-end recipe: robust networking and IAM, RDS + ElastiCache + S3\/CloudFront, ALB health checks, secure env management, CI\/CD with GitHub Actions OIDC, [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":435,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[75,74,71],"class_list":["post-431","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-laravel","tag-aws","tag-deployment","tag-devops"],"_links":{"self":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/431","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/comments?post=431"}],"version-history":[{"count":1,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/431\/revisions"}],"predecessor-version":[{"id":434,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/431\/revisions\/434"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/media\/435"}],"wp:attachment":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/media?parent=431"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/categories?post=431"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/tags?post=431"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}