Dockerizing a Laravel 10 API and Nuxt 3 Frontend

Using Laravel Sail and Docker Compose

Table of contents

Laravel comes with a pretty handy way to use docker during development, it is called Laravel Sail and is a great implementation managed by the Laravel team itself. In fact, there's a LABEL option in the Dockerfile that names the maintainer as none other than Taylor Otwell himself.

This implementation works great if you're using Laravel by itself, and I would perhaps only advise that you add an alias for ./vendor/bin/sail, to make all those sail commands easier to type for yourself.

Where I ran into headwinds was when I decided that for a better dev experience on a project I was working on with a Laravel API and a Nuxt frontend, I was going to run them both from one docker-compose.yml file, in a common parent folder. My folder structure looks something like this:

  • Parent App Folder

    • Client App

    • Server App

    • docker-compose.yml

Here's a walkthrough of how I got this working and how you could too.

Client App

To get this setup working, we first have to dockerize the Client App. This is the Dockerfile setup we end up with for the client:

FROM node:18 as dev

WORKDIR /client
ENV PATH ./node_modules/.bin/:$PATH

COPY package*.json ./
RUN npm ci
COPY . ./

EXPOSE 3000
EXPOSE 24678

CMD ["npm", "run", "dev"]

There were a few gotchas here though. Firstly, using the regular WORKDIR of /app breaks Nuxt, as there are some issues around that name being used by a few of the internal workings of Nuxt. We need to choose a different value. This is discussed in further detail here: Github Gist;

Secondly, Nuxt polls on port 24678 for updates in dev mode, so we must expose this port. With these, the client is now able to run in a container. Next, we update our docker-compose.yml with a client service as shown below:

build: 
      context: <path to client app>
      target: dev
    container_name: <client app container name>
    volumes:
      - <path to client app>:/client/
    ports:
      - 3000:3000
      - 24678:24678
    tty: true
    network_mode: host
    command: npm run dev

Another gotcha here, if we have any server-side rendering that requires the forwarding of headers from the client side, this is broken without using host networking. We thus need to set our network_mode to host.

With the client out of the way, let us set up the server next.

Server App

For the server side of this application, we have a Laravel API and a PostgreSQL database. Since we're using Laravel Sail, we have a docker-compose.yml file at the root of the Server App directory.

Laravel Sail references quite a few .env variables, and so requires the .env file to be on the same folder level as the docker-compose.yml, or it would get ignored. To eat our cake and still have it, we can create a symlink from the original Server App/.env to the Parent App Folder/.env to have our .env live in two places at once. I believe this is better than moving the file, as it saves us the hassle of having to reconfigure all our configs to point to an .env file at a different location.

The next step would be to create two (2) new variables in the .env, called WWWUSER and WWWGROUP.These should point to your user id and your user group id respectively. For me, these were both equal to 1000.

Finally, we can carefully go through the default Laravel Sail docker-compose, and copy it over into our new docker-compose.yml, being careful to change the relative paths as required. For my use case, we have two server-side services, the server and the pgsql (database). This is what the docker-compose.yml looks like afterward:

server:
    build:
      context: <path to Server App>/vendor/laravel/sail/runtimes/8.2
      dockerfile: Dockerfile
      args:
        WWWGROUP: '${WWWGROUP}'
    image: sail-8.2/app
    extra_hosts:
      - 'host.docker.internal:host-gateway'
    ports:
      - '${APP_PORT}:80'
      - '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
    env_file: .env
    environment:
      WWWUSER: '${WWWUSER}'
      LARAVEL_SAIL: 1
      XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
      XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
      IGNITION_LOCAL_SITES_PATH: '${PWD}'
    volumes:
      - '<path to Server App>:/var/www/html'
    depends_on:
      - pgsql

  pgsql:
    image: 'postgres:15'
    ports:
      - 5432:5432
    env_file: .env
    environment:
      PGPASSWORD: '${DB_PASSWORD}'
      POSTGRES_DB: '${DB_DATABASE}'
      POSTGRES_USER: '${DB_USERNAME}'
      POSTGRES_PASSWORD: '${DB_PASSWORD}'
    volumes:
      - 'sail-pgsql:/var/lib/postgresql/data'
      - '<path to Server App>/vendor/laravel/sail/database/pgsql/create-testing-database.sql:/docker-entrypoint-initdb.d/10-create-testing-database.sql'
    healthcheck:
      test:
        - CMD
        - pg_isready
        - '-q'
        - '-d'
        - '${DB_DATABASE}'
        - '-U'
        - '${DB_USERNAME}'
      retries: 3
      timeout: 5s

Not any gotchas here particularly, as it should just work. To recap, the full docker-compose.yml should now look something like this:

version: '3.7'

services:
    server:
        build:
          context: <path to Server App>/vendor/laravel/sail/runtimes/8.2
          dockerfile: Dockerfile
          args:
            WWWGROUP: '${WWWGROUP}'
        image: sail-8.2/app
        extra_hosts:
          - 'host.docker.internal:host-gateway'
        ports:
          - '${APP_PORT}:80'
          - '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
        env_file: .env
        environment:
          WWWUSER: '${WWWUSER}'
          LARAVEL_SAIL: 1
          XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
          XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
          IGNITION_LOCAL_SITES_PATH: '${PWD}'
        volumes:
          - '<path to Server App>:/var/www/html'
        depends_on:
          - pgsql

  pgsql:
    image: 'postgres:15'
    ports:
      - 5432:5432
    env_file: .env
    environment:
      PGPASSWORD: '${DB_PASSWORD}'
      POSTGRES_DB: '${DB_DATABASE}'
      POSTGRES_USER: '${DB_USERNAME}'
      POSTGRES_PASSWORD: '${DB_PASSWORD}'
    volumes:
      - 'sail-pgsql:/var/lib/postgresql/data'
      - '<path to Server App>/vendor/laravel/sail/database/pgsql/create-testing-database.sql:/docker-entrypoint-initdb.d/10-create-testing-database.sql'
    healthcheck:
      test:
        - CMD
        - pg_isready
        - '-q'
        - '-d'
        - '${DB_DATABASE}'
        - '-U'
        - '${DB_USERNAME}'
      retries: 3
      timeout: 5s

    client:
         build: 
              context: <path to client app>
              target: dev
        container_name: <client app container name>
        volumes:
          - <path to client app>:/client/
        ports:
          - 3000:3000
          - 24678:24678
        tty: true
        network_mode: host
        command: npm run dev

At this point, you should be able to start your entire application and have your server and client running in the same terminal using the following commands:

$: docker compose build
$: docker compose up