Running Laravel Vapor with local development setup
In one of my previous articles, we explored how we can use Localstack to replicate AWS services in a local environment. Now, let's take it a step further and see how we can set up Laravel Vapor in a local environment, creating a development setup that closely mirrors our production environment.
Table of Contents
- Why Run Vapor Locally?
- Setting Up the Environment
- Stitching it all together
- Running Vapor Locally
- Conclusion
Why Run Vapor Locally?
Laravel Vapor is a first-party serverless deployment platform created by the Laravel team for Laravel apps. All the services run on AWS, and it greatly simplifies our infrastructure and production deployments. By running Vapor locally, we can:
- Test Vapor-specific features without deploying
- Debug issues in an environment closer to production
- Develop faster with a setup that doesn't require constant deployments
Setting Up the Environment
Let's continue to build upon our previous Localstack setup to create a Vapor-like local environment.
Localstack Setup
From the previous article, we already have a docker-compose.yml
with Localstack set up. The service is configured to set up and run S3, SQS, SES, and DynamoDB automatically. You can find the file docker/localstack/init-aws.sh
and the required changes to the Laravel config files in the other article.
services:
localstack:
image: localstack/localstack
ports:
- '4566:4566' # Localstack dashboard
# - "127.0.0.1:4510-4559:4510-4559" # Uncomment to expose all services
volumes:
- './docker/localstack/init-aws.sh:/etc/localstack/init/ready.d/init-aws.sh'
- 'localstack:/var/lib/localstack'
- '/var/run/docker.sock:/var/run/docker.sock'
environment:
DEBUG: '${APP_DEBUG:-1}'
DYNAMODB_TABLE: '${DYNAMODB_CACHE_TABLE:-default}'
HOSTNAME_EXTERNAL: localstack
HOST_TMP_FOLDER: ${TMPDIR}
LOCALSTACK_HOST: localstack
PERSISTENCE: '${PERSISTENCE:-1}'
S3_BUCKET: '${AWS_BUCKET:-default}'
SERVICES: dynamodb,ses,sqs,s3
SES_EMAIL: '${MAIL_FROM_ADDRESS:-hello@example.com}'
SQS_ENDPOINT_STRATEGY: path
SQS_QUEUE: '${SQS_QUEUE:-default}'
networks:
- default-network
networks:
default-network:
driver: bridge
volumes:
localstack:
driver: local
PHP-FPM Service
Adding the PHP runtime that Vapor uses is straightforward. The PHP runtime is publicly available, and you can read more about it in the Laravel Vapor documentation. We're going to use the laravelphp/vapor:php83-arm
image, which is the PHP 8.3 runtime for ARM architecture.
services:
php-fpm:
image: laravelphp/vapor:php83-arm
command: ['php-fpm']
working_dir: '/var/task'
extra_hosts:
- 'host.docker.internal:host-gateway'
volumes:
- .:/var/task
restart: unless-stopped
networks:
- default-network
depends_on:
# - pgsql # Uncomment if you have a Postgres service
- localstack
This configuration:
- Uses the official Vapor PHP runtime
- Runs PHP-FPM, mirroring Vapor's production setup
- Mounts the current directory to
/var/task
, where Vapor expects the application code - Allows communication with the host machine, useful for multi-container setups
- Depends on the Localstack service to ensure AWS services are available
Nginx Service
Next, we need to add a Nginx service to proxy requests to the PHP-FPM container:
services:
nginx:
image: nginx:latest
volumes:
- ./docker/nginx/nginx.conf:/etc/nginx/conf.d/vapor.conf
- .:/var/task
ports:
- '${FORWARD_APP_PORT:-8000}:8000'
restart: unless-stopped
networks:
- default-network
depends_on:
# - node # Uncomment if you have a Node.js service
- php-fpm
This service:
- Uses the official Nginx image
- Mounts a custom Nginx configuration (
nginx.conf
) - Exposes port 8000, configurable via the
FORWARD_APP_PORT
environment variable in.env
. This port that you can access your application on, e.g.,http://localhost:8000
- Depends on the Node.js and PHP-FPM services
The docker/nginx/nginx.conf
file should look like this:
upstream php-fpm {
server php-fpm:9000;
}
server {
listen 8000 default_server;
server_name _;
charset utf-8;
index index.html index.htm index.php;
root /var/task/public;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
location = /favicon.ico { log_not_found off; access_log off; }
location = /robots.txt { log_not_found off; access_log off; }
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php-fpm;
include fastcgi_params;
fastcgi_index index.php;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
error_page 404 /index.php;
location ~ /\.(?!well-known).* {
deny all;
}
}
Node.js Service
While not strictly necessary for a basic Vapor setup, both the node
and postgres
services are often useful in Laravel projects and can enhance your local development environment.
For frontend asset compilation:
services:
node:
image: node:22-alpine
command: ['npm', 'run', 'dev']
working_dir: '/var/task'
volumes:
- '.:/var/task'
ports:
- '${VITE_FORWARD_PORT:-5174}:${VITE_FORWARD_PORT:-5174}'
restart: unless-stopped
environment:
NODE_ENV: development
networks:
- default-network
This service runs the Vite watcher to compile and update your JavaScript and CSS files in real-time.
PostgreSQL Service
For the database services we will use PostgreSQL, Laravel Vapor uses PostgreSQL 13.11 for its fixed size databases:
services:
pgsql:
image: postgres:13.11-alpine
platform: linux/x86_64
ports:
- '${FORWARD_DB_PORT:-5432}:5432'
volumes:
- 'postgres:/var/lib/postgresql/data'
- './docker/pgsql/init-test-db.sql:/docker-entrypoint-initdb.d/init-test-db.sql'
environment:
PGPASSWORD: '${DB_PASSWORD:-secret}'
POSTGRES_DB: '${DB_DATABASE:-laravel}'
POSTGRES_USER: '${DB_USERNAME:-postgres}'
POSTGRES_PASSWORD: '${DB_PASSWORD:-secret}'
TZ: 'Europe/Oslo'
restart: unless-stopped
networks:
- default-network
healthcheck:
test:
[
'CMD',
'pg_isready',
'-q',
'-d',
'${DB_DATABASE}',
'-U',
'${DB_USERNAME}',
]
retries: 3
timeout: 5s
This is completely optional, by I like to initialize the test database as well. We can do this by creating a docker/pgsql/init-test-db.sql
file with the following content:
CREATE DATABASE "phpunit";
\c phpunit;
GRANT ALL PRIVILEGES ON DATABASE "phpunit" TO "$POSTGRES_USER";
SET TIMEZONE = 'Europe/Oslo';
Stitching it all together
Here's the complete docker-compose.yml
file that combines all the services:
name: laravel
services:
nginx:
image: nginx:latest
volumes:
- ./docker/nginx/nginx.conf:/etc/nginx/conf.d/vapor.conf
- .:/var/task
ports:
- '${FORWARD_APP_PORT:-8000}:8000'
restart: unless-stopped
networks:
- laravel-network
depends_on:
- node
- php-fpm
php-fpm:
image: laravelphp/vapor:php83-arm
command: ['php-fpm']
working_dir: '/var/task'
extra_hosts:
- 'host.docker.internal:host-gateway'
volumes:
- .:/var/task
restart: unless-stopped
networks:
- laravel-network
depends_on:
- localstack
- pgsql
node:
image: node:22-alpine
command: ['npm', 'run', 'dev']
working_dir: '/var/task'
volumes:
- '.:/var/task'
- './docker/node/entrypoint.sh:/var/task/entrypoint.sh'
ports:
- '${VITE_FORWARD_PORT:-5174}:${VITE_FORWARD_PORT:-5174}'
restart: unless-stopped
environment:
NODE_ENV: development
networks:
- laravel-network
pgsql:
image: postgres:13.11-alpine
platform: linux/x86_64
ports:
- '${FORWARD_DB_PORT:-5432}:5432'
volumes:
- 'postgres:/var/lib/postgresql/data'
- './docker/pgsql/init-test-db.sql:/docker-entrypoint-initdb.d/init-test-db.sql'
environment:
PGPASSWORD: '${DB_PASSWORD:-secret}'
POSTGRES_DB: '${DB_DATABASE:-laravel}'
POSTGRES_USER: '${DB_USERNAME:-postgres}'
POSTGRES_PASSWORD: '${DB_PASSWORD:-secret}'
TZ: 'Europe/Oslo'
restart: unless-stopped
networks:
- laravel-network
healthcheck:
test:
[
'CMD',
'pg_isready',
'-q',
'-d',
'${DB_DATABASE}',
'-U',
'${DB_USERNAME}',
]
retries: 3
timeout: 5s
localstack:
image: localstack/localstack
ports:
- '127.0.0.1:4566:4566'
volumes:
- './docker/localstack/init-aws.sh:/etc/localstack/init/ready.d/init-aws.sh'
- 'localstack:/var/lib/localstack'
- '/var/run/docker.sock:/var/run/docker.sock'
environment:
DEBUG: '${APP_DEBUG:-1}'
DYNAMODB_TABLE: '${DYNAMODB_CACHE_TABLE:-default}'
HOSTNAME_EXTERNAL: localstack
HOST_TMP_FOLDER: ${TMPDIR}
LOCALSTACK_HOST: localstack
PERSISTENCE: '${PERSISTENCE:-1}'
S3_BUCKET: '${AWS_BUCKET:-default}'
SERVICES: dynamodb,ses,sqs,s3
SES_EMAIL: '${MAIL_FROM_ADDRESS:-hello@example.com}'
SQS_ENDPOINT_STRATEGY: path
SQS_QUEUE: '${SQS_QUEUE:-default}'
restart: unless-stopped
networks:
- laravel-network
networks:
laravel-network:
driver: bridge
volumes:
localstack:
driver: local
postgres:
driver: local
Running Vapor Locally
With our Docker setup in place, we can now run our Vapor application locally:
Start the Docker environment, run the migrations and seed the database. Finally, access your application at http://localhost:8000
:
docker-compose up -d
docker-compose exec php-fpm php artisan migrate --seed
open http://localhost:8000
Conclusion
Running Laravel Vapor locally with Docker and Localstack provides a development environment that closely resembles production. This setup allows you to confidently develop and test Vapor-specific features without the need for constant deployments.
The benefits of this local Vapor setup include:
- Faster development cycles with instant feedback
- Easier debugging of Vapor-specific issues
- Consistent environment across your development team
Remember, while this local setup is powerful, it's not a perfect replication of the Vapor environment. Always perform final testing in a staging environment before deploying to production.
By combining the power of Docker, Localstack, and Laravel Vapor, we've created a robust local development environment that improves the process of building serverless Laravel applications. This setup empowers developers to harness the full potential of Vapor while maintaining the flexibility and speed of local development.
Here are some resources that you might find useful:
- Github - My Laravel Localstack Project.
- Github - Implementation Pull Request.
- Localstack Documentation.
Happy coding!