Hardened NGINX config, seperated concerns

I’m using podman-compose (alternative to docker-compose) with some written Dockerfiles to build from source. The fun quirk is that I have the nginx side holding the static content while php-fpm has the dynamic content (/application/*/*.php including index.php & ipconfig.php) since this was the tidier solution without duplicating content between two containers.

So a request hits nginx and unless it’s location includes /assets or /uploads it should defer to php-fpm which has index.php and execute it. Or that’s the goal. I did get close to that result but I don’t think my nginx knowledge is strong enough to figure out what’s going on.

server {
  listen 0.0.0.0:80;

  root /var/www/html;

  location / {
    include fastcgi_params;
    fastcgi_pass invoice-php-fpm:9000;
    fastcgi_param SCRIPT_FILENAME $document_root/index.php;
  }

  location /assets/ {
    expires 30d;
  }

  location /uploads/ {
    expires 30m;
  }
}

This almost works, but there’s a http 307 redirect loop to the first time setup (/welcome aka /index.php/welcome) as if the script isn’t receiving the location; it sees I need to go to first time setup, doesn’t know where I actually navigated, redirects regardless, repeat.

I’d prefer this solution since as far as I’m aware InvoicePlane executes index.php at the root, so there is no need for a client to request any other php file and puts a seal on any Badness™ when it comes to somehow placing a php file where it shouldn’t be.

For the moment, I ended up picking a default.conf and modified it slightly (because nginx doesn’t see the php files, only php-fpm, so ask regardless).

server {
  listen 0.0.0.0:80;

  root /var/www/html;

  index index.php index.html;

  location / {
    try_files $uri $uri/ /index.php?$query_string;
  }

  location ~ \.php$ {
    include fastcgi_params;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass invoice-php-fpm:9000;
  }
}
1 Like

Hey,
Great job on what you’ve figured out above.
If you want, you can maintain our Docker setup(s)

That’s actually what i wanted to refer to.

I don’t know the solution to that infinite redirect (yet), but i know that i have 2 working Docker setups:

  • In the root of the InvoicePlane repository I have a docker-compose.yml file
  • in resources i have a couple of Dockerfile files for nginx and things like that

The second setup is just on the main github page of InvoicePlane. It’s in a separate repository.

What i did was grab something called laradock and stripped it of all nonsense, leaving just nginx, php, mariadb, redis and a container with the name of workspace. Works like a charm.

The infinite redirect?
Might have to do with 2 things:

  • difference between /index.php/setup and /setup
  • something in the code having to do with sessions
    I wouldn’t want to look into that one, unless you’re very interested in that.

Your ultimate personal solution might be to place index.php and assets in the public directory and then include vendor from ../vendor, like Laravel does.
Then all you need to do is deal wit ipconfig.php somehow.
AND the uploaded files from /upload need to be able to be downloaded somehow.

You can then serve all .php files from /public

The offer is flattering and I’ll give it some thought even if my knowledge of containerization has some holes.

I’m currently darting around Thailand right now so doing any extra debugging is out of the question for the next few days; I just happened to get inspiration after returning to the hotel from a night market with energy to spare, and after some trail and error reached this point.

But I can at least spare the Dockerfiles and the docker-compose.yml I created in the meantime since they might be nice to look at—and probably should’ve offered on the get-go but I was pretty tired at the end of that session—and pick apart until I’m much more stationary to do some back and forth. Mind I don’t build containers and assemble services with them often so my work might be amateur, along with some good old StackOverflow copy-pasting. Additionally not a big fan of me baking ipconfig.php into the container instead of using a bind-mount since it needs to be mutable (though temporarily).


Note: The repo for InvoicePlane must be in the same directory to build, and named InvoicePlane. git cloneing will suffice. Additionally copy ipconfig.example.php and place it next to docker-compose.yml with its Dockerfile friends, naming it ipconfig.php and editing IP_URL appropriately with http://localhost:8085.

Same redirect loop regardless of REMOVE_INDEXPHP being true or false


docker-compose.yml

version: 3.8

services:
  invoice-nginx:
    image: invoice-plane-nginx
    build:
      dockerfile: nginx.Dockerfile
    depends_on:
      - invoice-php-fpm
    networks:
      - invoice-frontend
      - invoice-fastcgi
    volumes:
      - invoice-uploads:/var/www/html/uploads
    ports:
      - 8085:80

  invoice-php-fpm:
    image: invoice-plane-php-fpm
    build:
      dockerfile: php-fpm.Dockerfile
    depends_on:
      - invoice-db
    networks:
      - invoice-backend
      - invoice-fastcgi

  invoice-db:
    image: mariadb:11.3.2
    networks:
      - invoice-backend
    volumes:
      - invoice-db-data:/var/lib/mysql
    environment:
      MARIADB_ROOT_PASSWORD: funfridaynight

volumes:
  invoice-db-data:
  invoice-uploads:

networks:
  invoice-frontend:
  invoice-fastcgi:
  invoice-backend:
  site-frontend:
  site-backend:

nginx.Dockerfile

# Builds static assets to be served directly by nginx

# Builds stylesheets, minimizes scripts, and collects fonts
FROM node:lts-bookworm as style-scripts
WORKDIR /app
COPY ./InvoicePlane/ ./
RUN npm install -g grunt-cli
RUN npm install
RUN grunt build

FROM nginx:latest
COPY ./default.conf /etc/nginx/conf.d
WORKDIR /var/www/html

# Because --parent is not available in podman-compose so a bunch of COPY commands. It's as painful as it looks.

COPY  --from=style-scripts \
      /app/assets/core/css \
      ./assets/core/css

COPY  --from=style-scripts \
      /app/assets/core/fonts \
      ./assets/core/fonts

COPY  --from=style-scripts \
      /app/assets/core/js \
      ./assets/core/js

COPY  --from=style-scripts \
      /app/assets/invoiceplane/invoiceplane.theme \
      ./assets/invoiceplane/

COPY  --from=style-scripts \
      /app/assets/invoiceplane/css \
      ./assets/invoiceplane/css

COPY  --from=style-scripts \
      /app/assets/invoiceplane_blue/invoiceplane_blue.theme \
      ./assets/invoiceplane_blue/

COPY  --from=style-scripts \
      /app/assets/invoiceplane_blue/css \
      ./assets/invoiceplane_blue/css

php-fpm.Dockerfile

# This gets php-fpm with the required packages and extensions along with all the php related assets

# Collects php dependencies using Composer
# Even though we *don't* need FastCGI right here, might as well to avoid pulling more images than we need
FROM php:8.1-fpm-bookworm as php-deps
WORKDIR /app
# https://stackoverflow.com/a/61228827/5620797
RUN apt-get update
RUN apt-get install -y \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
        libpng-dev \
        unzip

RUN docker-php-ext-configure gd --with-freetype --with-jpeg
RUN docker-php-ext-install -j$(nproc) bcmath gd

# Composer container is lean and needs to be copied from it to be executed here
COPY --from=composer:lts /usr/bin/composer /usr/bin/composer
RUN --mount=type=bind,source=InvoicePlane/composer.json,target=composer.json \
    --mount=type=bind,source=InvoicePlane/composer.lock,target=composer.lock \
    --mount=type=cache,target=/tmp/cache \
    composer install --no-dev --no-interaction

FROM php:8.1-fpm-bookworm as php
WORKDIR /var/www/html
RUN apt-get update
RUN apt-get install -y \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
        libpng-dev

RUN docker-php-ext-configure gd --with-freetype --with-jpeg
RUN docker-php-ext-install -j$(nproc) pdo pdo_mysql bcmath gd
RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
#RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"

# Collect .php files
COPY  ./ipconfig.php \
      ./InvoicePlane/index.php \
      ./

COPY  ./InvoicePlane/application \
      ./application

COPY  --from=php-deps \
      /app/vendor \
      ./vendor

default.conf, towards the intended direction

This is the config that I want to build from, but causes the aforementioned redirect loop.

server {
  listen 0.0.0.0:80;

  root /var/www/html;

  location / {
    include fastcgi_params;
    fastcgi_pass invoice-php-fpm:9000;
    fastcgi_param SCRIPT_FILENAME $document_root/index.php;
  }

  location /assets/ {
    expires 30d;
  }

  location /uploads/ {
    expires 30m;
  }
}

default.conf, made to work

This isn’t the direction I wanted to go, but does at least make the site accessible. Cannot complete welcome presently since a bootstrapping script has to be crafted to create the /upload folder structure (within a volume or otherwise) and use a bind-mount for ipconfig.php.

server {
  listen 0.0.0.0:80;

  root /var/www/html;

  index index.php index.html;

  location / {
    try_files $uri $uri/ /index.php?$query_string;
  }

  location ~ \.php$ {
    include fastcgi_params;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass invoice-php-fpm:9000;
  }
}
1 Like