Deployment
The starter does not prescribe a single deployment platform. Northwestern teams use a mix of AWS-native, container-based, and serverless architectures depending on application needs. The most common and fastest way to deploy a Laravel application internally is Laravel Vapor, Laravel’s serverless orchestration platform for AWS.
This guide provides:
- A high-level overview of deployment considerations
- A complete Vapor deployment walkthrough
- Certificate + DNS workflow
- Infrastructure-as-Code (IaC) integration (e.g., Terraform / OpenTofu)
- GitHub Actions deployment pipeline examples
- A reference
vapor.yml - Known limitations and best practices
Choosing a Deployment Strategy
Section titled “Choosing a Deployment Strategy”Your options typically include:
- Laravel Vapor (recommended for new projects)
- ECS / EC2 container-based deployments
- Traditional EC2 hosts
Most Northwestern teams choose Vapor, so the remainder of this documentation focuses on building, provisioning, and deploying your application using Vapor.
Laravel Vapor
Section titled “Laravel Vapor”Laravel Vapor is a serverless deployment platform built specifically for Laravel applications. Vapor:
- Builds and deploys your application as AWS Lambda functions
- Creates API Gateway endpoints, CloudFront distributions, and supporting infrastructure
- Manages environment variables, assets, S3 file uploads, and build artifacts
- Works seamlessly with queues, scheduled tasks, and CLI operations
Vapor significantly reduces maintenance overhead for many small and medium-scale applications.
How Northwestern Uses Vapor
Section titled “How Northwestern Uses Vapor”Northwestern mirrors the structure of AWS accounts:
- A nonprod Vapor team → nonprod AWS account
- A prod Vapor team → prod AWS account
Because Vapor associates one project with one team, every application ends up with two Vapor projects, each with their own project ID.
Initial Setup
Section titled “Initial Setup”-
Create the Vapor project
Create one project inside the nonprod Vapor team and one inside the prod Vapor team.
-
Request certificates
Create ACM certificates for each environment hostname:
Terminal window vendor/bin/vapor certificate dev-your-app.example.eduvendor/bin/vapor certificate qa-your-app.example.eduvendor/bin/vapor certificate your-app.example.edu -
Validate certificates via DNS
Use your organization’s DNS request process to add the CNAME records Vapor provides.
-
Request VPC subnet allocations
Obtain subnet IDs from your cloud/networking team.
-
Configure
vapor.ymlAdd memory values, timeouts, subnets, security groups, queue names, and build/deploy steps.
-
Run your first deploy
Terminal window vendor/bin/vapor deploy develop
Vapor Limitations
Section titled “Vapor Limitations”Serverless constraints that may impact your application:
- Lambda max runtime: 15 minutes
- API Gateway timeout: 29 seconds
- File uploads must use S3 (direct-to-S3 strategy)
- Long-running queue jobs must be chunked
- Persistent disk requires optional EFS integration
- Limited customization of PHP extensions and binaries
Best Practices
Section titled “Best Practices”Encrypted Environment Variables
Section titled “Encrypted Environment Variables”Rather than scattering secrets across GitHub Secrets and .env.* files, store nearly all variables in:
.env.production.encrypted.env.develop.encryptedCommit these files to the repo and have Vapor decrypt them on deploy.
Example update flow:
php artisan env:decrypt --env=productionphp artisan env:encrypt --env=productiongit add .git commit -m "Update production env"Vapor reads these encrypted files at deploy time, so you can treat them as the single source of truth for environment configuration.
IaC for Supporting Resources
Section titled “IaC for Supporting Resources”Although Vapor can provision RDS, S3, and ElastiCache, teams often prefer OpenTofu for:
- Engine version management
- Bucket policy control
- VPC placement
- Parameter Store integration
Use OpenTofu/Terraform to provision long-lived infrastructure, and use Vapor primarily for application deployment and environment configuration.
Reference GitHub Actions Workflow (Vapor)
Section titled “Reference GitHub Actions Workflow (Vapor)”Below is a simplified GitHub Actions template that can be adapted for your application.
name: Deploy
on: push: branches: [ develop, qa, production ] workflow_dispatch:
concurrency: group: env-${{ github.ref }}
env: BRANCH_NAME: ${{ github.ref_name }}
jobs: deploy: runs-on: ubuntu-latest
steps: - name: 📂 Checkout Repository uses: actions/checkout@v5 with: ref: ${{ github.head_ref }}
- name: 🔑 Set Secrets Based on Branch run: | if [ "$BRANCH_NAME" = "production" ]; then echo "TERRAFORM_KEY_VAR_NAME=TF_KEY_PROD" >> $GITHUB_ENV echo "TERRAFORM_SECRET_VAR_NAME=TF_SECRET_PROD" >> $GITHUB_ENV echo "VAPOR_API_VAR_NAME=PROD_VAPOR_API_TOKEN" >> $GITHUB_ENV sed -i 's/id: 123/id: 456/' vapor.yml # Example Vapor project ID swap else echo "TERRAFORM_KEY_VAR_NAME=TF_KEY_NONPROD" >> $GITHUB_ENV echo "TERRAFORM_SECRET_VAR_NAME=TF_SECRET_NONPROD" >> $GITHUB_ENV echo "VAPOR_API_VAR_NAME=NONPROD_VAPOR_API_TOKEN" >> $GITHUB_ENV fi
- name: 🌐 Set Environment URL for Deployment id: environment-url run: | if [ "$BRANCH_NAME" = "production" ]; then echo "URL=https://your-app.northwestern.edu/" >> $GITHUB_OUTPUT elif [ "$BRANCH_NAME" = "qa" ]; then echo "URL=https://qa-your-app.northwestern.edu/" >> $GITHUB_OUTPUT else echo "URL=https://dev-your-app.northwestern.edu/" >> $GITHUB_OUTPUT fi
- name: 🐘 Setup PHP uses: shivammathur/setup-php@v2 with: php-version: 8.4 extensions: ctype, curl, dom, fileinfo, filter, hash, iconv, intl, json, libxml, mbstring, openssl, pcre, pdo, pdo_pgsql, phar, reflection, session, simplexml, sodium, tokenizer, xml, xmlreader, xmlwriter, zip
- name: 📦 Install pnpm uses: pnpm/action-setup@v4 with: version: 10.x.x
- name: 🟢 Setup NodeJS uses: actions/setup-node@v6 with: node-version: 25 cache: pnpm
- name: 🌱 Setup OpenTofu uses: opentofu/setup-opentofu@v1 with: tofu_version: 1.10.2
- name: 🔧 Install dependencies run: | composer install --no-interaction cp .env.ci .env php artisan key:generate pnpm install
- name: 🧱 OpenTofu Init env: AWS_ACCESS_KEY_ID: ${{ secrets[env.TERRAFORM_KEY_VAR_NAME] }} AWS_SECRET_ACCESS_KEY: ${{ secrets[env.TERRAFORM_SECRET_VAR_NAME] }} TF_VAR_local_state_passphrase: ${{ secrets.TF_LOCAL_STATE_ENCRYPTION_KEY_2025_07 }} TF_VAR_shared_state_passphrase: ${{ secrets.TF_SHARED_RESOURCES_STATE_ENCRYPTION_KEY_2025_07 }} run: | cd iac/${BRANCH_NAME} tofu init
- name: 🧱 OpenTofu Apply and Output env: AWS_ACCESS_KEY_ID: ${{ secrets[env.TERRAFORM_KEY_VAR_NAME] }} AWS_SECRET_ACCESS_KEY: ${{ secrets[env.TERRAFORM_SECRET_VAR_NAME] }} TF_VAR_local_state_passphrase: ${{ secrets.TF_LOCAL_STATE_ENCRYPTION_KEY_2025_07 }} TF_VAR_shared_state_passphrase: ${{ secrets.TF_SHARED_RESOURCES_STATE_ENCRYPTION_KEY_2025_07 }} run: | cd iac/${BRANCH_NAME} tofu apply -auto-approve -no-color -var="master_password=${{ secrets.DB_PASSWORD }}" db_name=$(tofu output -no-color -raw db_name) db_endpoint=$(tofu output -no-color -raw db_endpoint) db_username=$(tofu output -no-color -raw master_username) bucket_name=$(tofu output -no-color -raw file_uploads_bucket_name) echo "DB_HOST=$db_endpoint" >> $GITHUB_ENV echo "DB_USERNAME=$db_username" >> $GITHUB_ENV echo "DB_DATABASE=$db_name" >> $GITHUB_ENV echo "BUCKET_NAME=$bucket_name" >> $GITHUB_ENV
- name: 🚀 Deploy App env: VAPOR_API_TOKEN: ${{ secrets[env.VAPOR_API_VAR_NAME] }} SHA: ${{ github.sha }} run: | echo "${{ secrets.LARAVEL_ENV_ENCRYPTION_KEY }}" | vendor/bin/vapor secret --name "LARAVEL_ENV_ENCRYPTION_KEY" ${BRANCH_NAME} echo "${DB_HOST}" | vendor/bin/vapor secret --name "DB_HOST" ${BRANCH_NAME} echo "${DB_USERNAME}" | vendor/bin/vapor secret --name "DB_USERNAME" ${BRANCH_NAME} echo "${DB_DATABASE}" | vendor/bin/vapor secret --name "DB_DATABASE" ${BRANCH_NAME} echo "${{ secrets.DB_PASSWORD }}" | vendor/bin/vapor secret --name "DB_PASSWORD" ${BRANCH_NAME} sleep 5; echo "${BUCKET_NAME}" | vendor/bin/vapor secret --name "AWS_BUCKET" ${BRANCH_NAME} vendor/bin/vapor deploy --no-ansi ${BRANCH_NAME} --commit=${SHA}
- name: 🔗 Attach Scratch EFS env: AWS_ACCESS_KEY_ID: ${{ secrets[env.TERRAFORM_KEY_VAR_NAME] }} AWS_SECRET_ACCESS_KEY: ${{ secrets[env.TERRAFORM_SECRET_VAR_NAME] }} TF_VAR_local_state_passphrase: ${{ secrets.TF_LOCAL_STATE_ENCRYPTION_KEY_2025_07 }} TF_VAR_shared_state_passphrase: ${{ secrets.TF_SHARED_RESOURCES_STATE_ENCRYPTION_KEY_2025_07 }} run: | cd iac/${BRANCH_NAME} ARN=$(tofu output -no-color -raw scratch_efs_arn) aws lambda update-function-configuration --function-name vapor-your-app-${BRANCH_NAME} --file-system-configs Arn=$ARN,LocalMountPath=/mnt/scratch --region us-east-2 aws lambda update-function-configuration --function-name vapor-your-app-${BRANCH_NAME}-cli --file-system-configs Arn=$ARN,LocalMountPath=/mnt/scratch --region us-east-2 aws lambda update-function-configuration --function-name vapor-your-app-${BRANCH_NAME}-queue --file-system-configs Arn=$ARN,LocalMountPath=/mnt/scratch --region us-east-2Reference vapor.yml Configuration
Section titled “Reference vapor.yml Configuration”id: YOUR_PROJECT_IDname: your-appignore: - iac/ - docs/ - cypress/
environments: develop: memory: 4096 timeout: 28 cli-memory: 2048 queue-memory: 2048 queue-timeout: 840 cli-timeout: 840 queue-concurrency: 30 runtime: "php-8.4:al2" domain: - dev.your-app.example.edu subnets: - subnet-abc123 - subnet-def456 security-groups: - sg-xyz890 queue-database-session-persist: true build: - "COMPOSER_MIRROR_PATH_REPOS=1 composer install --no-dev" - "php artisan event:cache" - "pnpm install && pnpm build && rm -rf node_modules" deploy: - "php artisan db:wake" - "php artisan migrate --seed --force" - "php artisan db:seed --class=StakeholderSeeder"Optional: Attaching EFS for Persistent Scratch Storage
Section titled “Optional: Attaching EFS for Persistent Scratch Storage”If your application needs a shared scratch disk for exports, temporary reports, or other short-lived files, attach an EFS file system to the Vapor Lambda functions:
aws lambda update-function-configuration \ --function-name vapor-your-app-${BRANCH_NAME} \ --file-system-configs Arn=$EFS_ARN,LocalMountPath=/mnt/scratch \ --region us-east-2
aws lambda update-function-configuration \ --function-name vapor-your-app-${BRANCH_NAME}-cli \ --file-system-configs Arn=$EFS_ARN,LocalMountPath=/mnt/scratch \ --region us-east-2
aws lambda update-function-configuration \ --function-name vapor-your-app-${BRANCH_NAME}-queue \ --file-system-configs Arn=$EFS_ARN,LocalMountPath=/mnt/scratch \ --region us-east-2