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.

Running Laravel Vapor with local development setup

Table of Contents

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:

  1. Test Vapor-specific features without deploying
  2. Debug issues in an environment closer to production
  3. 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.

docker-compose.yml
yaml
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.

docker-compose.yml
yaml
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:

docker-compose.yml
yaml
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:

docker/nginx/nginx.conf
nginx
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:

docker-compose.yml
yaml
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:

docker-compose.yml
yaml
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:

docker/pgsql/init-test-db.sql
sql
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:

docker-compose.yml
yaml
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:

bash
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:

Happy coding!

Stay up to date

Get notified when I publish something new, and unsubscribe at any time.