laraveldockernginxdocker-composenginx-reverse-proxy

How do I connect Nginx to a Docker container?


I have a Laravel project folder (as part of a Git repo) on a Linux server that I've been running Nginx on and just directly serving, but now I want to containerize that project with Docker. I also have a React project folder in the root directory of the repo. The Laravel project is in the folder titled API and the React project is in the folder titled speedcart-react.

I created all of the following files in my API folder for the Laravel project in discussion:

Dockerfile:

# Use the official PHP image as a base image
FROM php:8.2-fpm

# Set working directory (all subsequent commands will be run in this directory)
WORKDIR /var/www/SpeedCart/API

# Install system dependencies
RUN apt-get update && apt-get install -y \
    git \
    curl \
    libpng-dev \
    libonig-dev \
    libxml2-dev \
    zip \
    unzip \
    sqlite3 \
    libsqlite3-dev

# Clear cache (This cleans up the package cache to reduce the size of the Docker image)
RUN apt-get clean && rm -rf /var/lib/apt/lists/*

# Install PHP extensions required by Laravel
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd pdo_sqlite

# Install Composer (copies the composer binary from the latest Composer image 
# into your Docker image, making Composer available for dependency management)
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Copy existing application directory contents from working directory
# on host to /var/www/SpeedCart/API in the container
COPY . /var/www/SpeedCart/API

# Copy existing application directory permissions
COPY --chown=www-data:www-data . /var/www/SpeedCart

# Change current user to www-data (used by web servers to improve security)
USER www-data

# Expose port 9000 (the default port for PHP-FPM to listen on) and start php-fpm server
# (Even if you are using Nginx as a reverse proxy, PHP-FPM will still need to be exposed 
# internally within the Docker network, which is why we need EXPOSE 9000)
EXPOSE 9000
CMD ["php-fpm"]

docker-compose.yml:

version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    image: laravel-app
    container_name: laravel-app
    restart: unless-stopped
    working_dir: /var/www/SpeedCart/API
    volumes:
      - .:/var/www/SpeedCart/API
      - ./php.ini:/usr/local/etc/php/conf.d/local.ini

  webserver:
    image: nginx:alpine
    container_name: nginx
    restart: unless-stopped
    # Map host ports to ports in container
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - .:/var/www/SpeedCart/API
      - ./default.conf:/etc/nginx/conf.d/default.conf

networks:
  laravel:
    driver: bridge

default.conf:

server {
    listen 80;
    server_name api.speedcartapp.com;

    location / {
        proxy_pass http://172.18.0.2:9000;  # Replace with your container name or service name
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # SSL configuration
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/api.speedcartapp.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/api.speedcartapp.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

I have tried running docker-compose up -d --build after stopping Nginx with systemctl, then got the IP of the docker container that was running after that via running sudo docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' laravel-app and I put that IP address in the configuration file.

I tried adding the Nginx configuration file to /etc/nginx/sites-available (instead of the /var/www/SpeedCart/API/default.conf file location I was using before) and linked to that in /etc/nginx/sites-enabled since I thought that was the problem, but that didn't work. At the moment, my configuration file looks like this:

server {
    listen 80;
    server_name api.speedcartapp.com;

    location / {
        proxy_pass http://172.18.0.3:9000;  # Replace with your container name or service name
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # SSL configuration
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/api.speedcartapp.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/api.speedcartapp.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

I have run sudo nginx -t in the sites-available directory and it was all successful information. The only other configuration files I have are shopfast-react (old name but it's what's running for the speedcart-react folder in my repo), which looks like this:

server {
    listen 80;
    server_name speedcartapp.com www.speedcartapp.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    # server_name 139.144.239.217; # your server IP
    server_name speedcartapp.com www.speedcartapp.com;
    ssl_certificate /etc/letsencrypt/live/speedcartapp.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/speedcartapp.com/privkey.pem;
    
    root /var/www/SpeedCart/speedcart-react/build;
    index index.html;

    location / {
        try_files $uri /index.html;
    }

    # Redirect HTTP to HTTPS
    if ($scheme != "https") {
        return 301 https://$host$request_uri;
    }

    location ~ /\.well-known/acme-challenge/ {
        allow all;
        root /var/www/SpeedCart/speedcart-react/build;
    }


    location /api {
        # Assuming your PHP file is named api.php
        try_files $uri $uri/ /api.php?$args;
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Adjust the version based on your PHP-FPM configuration
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
        add_header Access-Control-Allow-Origin "http://localhost:3000";
        add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE";
        add_header Access-Control-Allow-Headers "Content-Type, Authorization";
    }

    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
        expires max;
        log_not_found off;
    }

    location ~ /\. {
        deny all;
    }

    location ~* /speedcart-react/(.*) {
        alias /var/www/speedcart-react/build/$1;
        try_files $uri $uri/ /speedcart-react/index.html;
    }

    # ... other configurations if any ...
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Adjust the version based on your PHP-FPM configuration
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PROJECT_ROOT /var/www/SpeedCart; # Add this line
        include fastcgi_params;
    }

}

and default which looks like this:

server {
        listen 80 default_server;
        listen [::]:80 default_server;

        root /var/www/html;

        index index.html index.php;

        server_name _;

        location / {
                try_files $uri $uri/ =404;
        }

        location ~ \.php$ {
                include snippets/fastcgi-php.conf;
                fastcgi_pass unix:/run/php/php8.1-fpm.sock; # Adjust the socket path
                # Add the following line to set the environment variable
                fastcgi_param PROJECT_ROOT /var/www/html;

                include fastcgi_params;
        }

        # ... other configuration ...
}

When I run sudo docker ps to check that the container is running, I get this:

CONTAINER ID   IMAGE         COMMAND                  CREATED          STATUS          PORTS      NAMES
3ee6e572d623   laravel-app   "docker-php-entrypoi…"   22 minutes ago   Up 21 minutes   9000/tcp   laravel-app

This is what my routes.php in /var/www/SpeedCart/API/public looks like for my actual routes:

<?php

// Note: We need to move almost all of this content to api.php for clarity ASAP

use App\Libraries\Database\Database;
use App\Libraries\Logging\Loggable;

use Illuminate\Support\Facades\Log;

use Illuminate\Support\Facades\Route;

use App\Http\Middleware\GoogleAuthentication; // This brings in our middleware to ensure authentication prior to user actions actually being done

// This is primarily for testing; since it's middleware, it doesn't usually get directly contacted
Route::post('/auth/google', function () {
    // No code necessary here; we just want to test the middleware
    Log::error("Finished executing GoogleAuthentication middleware"); // Why isn't this logging?
    return response()->json([
        'status' => 'success',
        'message' => 'Authentication successful',
    ], 200);
})->middleware(GoogleAuthentication::class);


Route::get('/phpinfo', function () {
    phpinfo();
});

//use App\Http\Controllers\Api\UserController;
use App\Http\Controllers\Api\RouteController;
use App\Http\Controllers\Api\ShoppingListController;
use App\Http\Controllers\Api\GroceryItemController;

//Route::apiResource('users', UserController::class);
Route::apiResource('routes', RouteController::class);

//Route::apiResource('shopping-lists', ShoppingListController::class);
// Middleware for authentication endpoint
Route::post('/shopping-lists', [ShoppingListController::class, 'store'])
->middleware(GoogleAuthentication::class);

// Route for retrieving all shopping list titles (used for Dashboard page)
Route::get('/shopping-lists', [ShoppingListController::class, 'getUserShoppingLists'])
->middleware(GoogleAuthentication::class);

// Route for retrieving shopping list title for a given ID
Route::get('/shopping-lists/{id}', [ShoppingListController::class, 'show'])
->middleware(GoogleAuthentication::class);

// Route for retrieving all items for a given shopping list ID
Route::get('/grocery-items/{id}', [GroceryItemController::class, 'show'])
->middleware(GoogleAuthentication::class);

// Route for deleting a shopping list
Route::delete('/shopping-lists/{id}', [ShoppingListController::class, 'destroy'])
    ->middleware(GoogleAuthentication::class);

Route::post('grocery-items', [GroceryItemController::class, 'store'])
    ->middleware(GoogleAuthentication::class);

// Route for updating shopping list title
Route::put('/shopping-lists/{id}', [ShoppingListController::class, 'update'])
    ->middleware(GoogleAuthentication::class);

// Route for updating grocery items
Route::put('/grocery-items/{id}', [GroceryItemController::class, 'update'])
    ->middleware(GoogleAuthentication::class);

// This is also needed for "updating" a grocery list because some items could be deleted
Route::delete('/grocery-items/{id}', [GroceryItemController::class, 'destroy'])
    ->middleware(GoogleAuthentication::class);

I can't figure out why api.speedcartapp.com won't work; I tried /phpinfo as the path because that one is an actual page (whereas my other endpoints are just REST APIs), but I keep getting 502 Bad Gateway and the Nginx error.log file looks like this every time you try that:

2024/06/15 16:38:27 [error] 3629981#3629981: *25 recv() failed (104: Unknown error) while reading response header from upstream, client: 167.94.138.40, server: api.speedcartapp.com, request: "GET / HTTP/1.1", upstream: "http://172.18.0.3:9000/", host: "139.144.239.217"

I've tried looking everywhere and I just want to use api.speedcartapp.com for my Laravel endpoints with Docker containers.

Edit: I made a container for php-fpm and I'm working on a container for nginx, but I keep getting this error in the logs whenever the container tries starting up: 2024/06/16 22:18:13 [emerg] 1#1: open() "/etc/nginx/snippets/fastcgi-php.conf" failed (2: No such file or directory) in /etc/nginx/conf.d/default.conf:24 nginx: [emerg] open() "/etc/nginx/snippets/fastcgi-php.conf" failed (2: No such file or directory) in /etc/nginx/conf.d/default.conf:24

I looked at another StackOverflow post on this error which said that the volumes list caused the container to actually ignore the file within the container (because it should consult the host machine instead for all volumes), but I even got rid of that and retried building using this docker-compose.yml file:

version: '3.8'
services:
  nginx:
    build:
      context: .
      dockerfile: docker/nginx/Dockerfile
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./docker/nginx/ssl:/etc/nginx/ssl  # Ensure this directory exists in your project
    depends_on:
      - app
  app:
    build:
      context: .
      dockerfile: docker/php/Dockerfile
    volumes:
      - .:/var/www/SpeedCart/API
    expose:
      - "9000"

I even included a RUN command in the Dockerfile that runs ls -la on /etc/nginx/snippets after all the copy commands and got this output, showing the file considered missing indeed does exist within the container:

Step 6/7 : RUN ls -la /etc/nginx/snippets
 ---> Running in bc1be3860516
total 12
drwxr-xr-x    2 root     root          4096 Jun 17 20:58 .
drwxr-xr-x    1 root     root          4096 Jun 17 20:58 ..
-rw-rw-r--    1 root     root           249 Jun 16 18:26 fastcgi-php.conf

Solution

  • I figured this out; I needed to create two container images to run two instances, one for the web server reverse proxy to get HTTPS to work and one for the actual Laravel code to run based on a PHP FPM image. I also had to make sure environment variables were included in the image creation process and I set the database as a volume (along with some other volumes like the log file for logging output).