Skip to content

Instantly share code, notes, and snippets.

@chris-cmsoft
Last active October 8, 2023 13:36
Show Gist options
  • Save chris-cmsoft/d398cb87907db6f39341df53345d6925 to your computer and use it in GitHub Desktop.
Save chris-cmsoft/d398cb87907db6f39341df53345d6925 to your computer and use it in GitHub Desktop.
TLDR - Laravel In Kubernetes Part 2
/vendor
/node_modules
version: '3'
services:
# We need to run the FPM container for our application
laravel.fpm:
build:
context: .
target: fpm_server
image: laravel-in-kubernetes/fpm_server
# We can override any env values here.
# By default the .env in the project root will be loaded as the environment for all containers
environment:
APP_DEBUG: "true"
# Mount the codebase, so any code changes we make will be propagated to the running application
volumes:
# Here we mount in our codebase so any changes are immediately reflected into the container
- '.:/opt/apps/laravel-in-kubernetes'
networks:
- laravel-in-kubernetes
# Run the web server container for static content, and proxying to our FPM container
laravel.web:
build:
context: .
target: web_server
image: laravel-in-kubernetes/web_server
# Expose our application port (80) through a port on our local machine (8080)
ports:
- '8080:80'
environment:
# We need to pass in the new FPM hst as the name of the fpm container on port 9000
FPM_HOST: "laravel.fpm:9000"
# Mount the public directory into the container so we can serve any static files directly when they change
volumes:
# Here we mount in our codebase so any changes are immediately reflected into the container
- './public:/opt/apps/laravel-in-kubernetes/public'
networks:
- laravel-in-kubernetes
# Run the Laravel Scheduler
laravel.cron:
build:
context: .
target: cron
image: laravel-in-kubernetes/cron
# Here we mount in our codebase so any changes are immediately reflected into the container
volumes:
# Here we mount in our codebase so any changes are immediately reflected into the container
- '.:/opt/apps/laravel-in-kubernetes'
networks:
- laravel-in-kubernetes
# Run the frontend, and file watcher in a container, so any changes are immediately compiled and servable
laravel.frontend:
build:
context: .
target: frontend
# Override the default CMD, so we can watch changes to frontend files, and re-transpile them.
command: ["npm", "run", "watch"]
image: laravel-in-kubernetes/frontend
volumes:
# Here we mount in our codebase so any changes are immediately reflected into the container
- '.:/opt/apps/laravel-in-kubernetes'
# Add node_modeules as singular volume.
# This prevents our local node_modules from being propagated into the container,
# So the node_modules can be compiled for each of the different architectures (Local, Image)
- '/opt/app/node_modules/'
networks:
- laravel-in-kubernetes
mysql:
image: 'mysql:8.0'
ports:
- '${FORWARD_DB_PORT:-3306}:3306'
environment:
MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
MYSQL_DATABASE: '${DB_DATABASE}'
MYSQL_USER: '${DB_USERNAME}'
MYSQL_PASSWORD: '${DB_PASSWORD}'
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
volumes:
- 'laravel-in-kubernetes-mysql:/var/lib/mysql'
networks:
- laravel-in-kubernetes
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-p${DB_PASSWORD}"]
retries: 3
timeout: 5s
networks:
laravel-in-kubernetes:
volumes:
laravel-in-kubernetes-mysql:
# Create args for PHP extensions and PECL packages we need to install.
# This makes it easier if we want to install packages,
# as we have to install them in multiple places.
# This helps keep ou Dockerfiles DRY -> https://bit.ly/dry-code
# You can see a list of required extensions for Laravel here: https://laravel.com/docs/8.x/deployment#server-requirements
ARG PHP_EXTS="bcmath ctype fileinfo mbstring pdo pdo_mysql tokenizer dom pcntl"
ARG PHP_PECL_EXTS="redis"
# We need to build the Composer base to reuse packages we've installed
FROM composer:2.1 as composer_base
# We need to declare that we want to use the args in this build step
ARG PHP_EXTS
ARG PHP_PECL_EXTS
# First, create the application directory, and some auxilary directories for scripts and such
RUN mkdir -p /opt/apps/laravel-in-kubernetes /opt/apps/laravel-in-kubernetes/bin
# Next, set our working directory
WORKDIR /opt/apps/laravel-in-kubernetes
# We need to create a composer group and user, and create a home directory for it, so we keep the rest of our image safe,
# And not accidentally run malicious scripts
RUN addgroup -S composer \
&& adduser -S composer -G composer \
&& chown -R composer /opt/apps/laravel-in-kubernetes \
&& apk add --virtual build-dependencies --no-cache ${PHPIZE_DEPS} openssl ca-certificates libxml2-dev oniguruma-dev \
&& docker-php-ext-install -j$(nproc) ${PHP_EXTS} \
&& pecl install ${PHP_PECL_EXTS} \
&& docker-php-ext-enable ${PHP_PECL_EXTS} \
&& apk del build-dependencies
# Next we want to switch over to the composer user before running installs.
# This is very important, so any extra scripts that composer wants to run,
# don't have access to the root filesystem.
# This especially important when installing packages from unverified sources.
USER composer
# Copy in our dependency files.
# We want to leave the rest of the code base out for now,
# so Docker can build a cache of this layer,
# and only rebuild when the dependencies of our application changes.
COPY --chown=composer composer.json composer.lock ./
# Install all the dependencies without running any installation scripts.
# We skip scripts as the code base hasn't been copied in yet and script will likely fail,
# as `php artisan` available yet.
# This also helps us to cache previous runs and layers.
# As long as comoser.json and composer.lock doesn't change the install will be cached.
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
# Copy in our actual source code so we can run the installation scripts we need
# At this point all the PHP packages have been installed,
# and all that is left to do, is to run any installation scripts which depends on the code base
COPY --chown=composer . .
# Now that the code base and packages are all available,
# we can run the install again, and let it run any install scripts.
RUN composer install --no-dev --prefer-dist
# For the frontend, we want to get all the Laravel files,
# and run a production compile
FROM node:14 as frontend
# We need to copy in the Laravel files to make everything is available to our frontend compilation
COPY --from=composer_base /opt/apps/laravel-in-kubernetes /opt/apps/laravel-in-kubernetes
WORKDIR /opt/apps/laravel-in-kubernetes
# We want to install all the NPM packages,
# and compile the MIX bundle for production
RUN npm install && \
npm run prod
# For running things like migrations, and queue jobs,
# we need a CLI container.
# It contains all the Composer packages,
# and just the basic CLI "stuff" in order for us to run commands,
# be that queues, migrations, tinker etc.
FROM php:8.0-alpine as cli
# We need to declare that we want to use the args in this build step
ARG PHP_EXTS
ARG PHP_PECL_EXTS
WORKDIR /opt/apps/laravel-in-kubernetes
# We need to install some requirements into our image,
# used to compile our PHP extensions, as well as install all the extensions themselves.
# You can see a list of required extensions for Laravel here: https://laravel.com/docs/8.x/deployment#server-requirements
RUN apk add --virtual build-dependencies --no-cache ${PHPIZE_DEPS} openssl ca-certificates libxml2-dev oniguruma-dev && \
docker-php-ext-install -j$(nproc) ${PHP_EXTS} && \
pecl install ${PHP_PECL_EXTS} && \
docker-php-ext-enable ${PHP_PECL_EXTS} && \
apk del build-dependencies
# Next we have to copy in our code base from our initial build which we installed in the previous stage
COPY --from=composer_base /opt/apps/laravel-in-kubernetes /opt/apps/laravel-in-kubernetes
COPY --from=frontend /opt/apps/laravel-in-kubernetes/public /opt/apps/laravel-in-kubernetes/public
# We need a stage which contains FPM to actually run and process requests to our PHP application.
FROM php:8.0-fpm-alpine as fpm_server
# We need to declare that we want to use the args in this build step
ARG PHP_EXTS
ARG PHP_PECL_EXTS
WORKDIR /opt/apps/laravel-in-kubernetes
RUN apk add --virtual build-dependencies --no-cache ${PHPIZE_DEPS} openssl ca-certificates libxml2-dev oniguruma-dev && \
docker-php-ext-install -j$(nproc) ${PHP_EXTS} && \
pecl install ${PHP_PECL_EXTS} && \
docker-php-ext-enable ${PHP_PECL_EXTS} && \
apk del build-dependencies
# We have to copy in our code base from our initial build which we installed in the previous stage
COPY --from=composer_base --chown=www-data /opt/apps/laravel-in-kubernetes /opt/apps/laravel-in-kubernetes
COPY --from=frontend --chown=www-data /opt/apps/laravel-in-kubernetes/public /opt/apps/laravel-in-kubernetes/public
# We want to cache the event, routes, and views so we don't try to write them when we are in Kubernetes.
# Docker builds should be as immutable as possible, and this removes a lot of the writing of the live application.
RUN php artisan event:cache && \
php artisan route:cache && \
php artisan view:cache
# As FPM uses the www-data user when running our application,
# we need to make sure that we also use that user when starting up,
# so our user "owns" the application when running
USER www-data
# We need an nginx container which can pass requests to our FPM container,
# as well as serve any static content.
FROM nginx:1.20-alpine as web_server
WORKDIR /opt/apps/laravel-in-kubernetes
# We need to add our NGINX template to the container for startup,
# and configuration.
COPY nginx.conf.template /etc/nginx/templates/default.conf.template
# Copy in ONLY the public directory of our project.
# This is where all the static assets will live, which nginx will serve for us.
COPY --from=frontend /opt/apps/laravel-in-kubernetes/public /opt/apps/laravel-in-kubernetes/public
# We need a CRON container to the Laravel Scheduler.
# We'll start with the CLI container as our base,
# as we only need to override the CMD which the container starts with to point at cron
FROM cli as cron
WORKDIR /opt/apps/laravel-in-kubernetes
# We want to create a laravel.cron file with Laravel cron settings, which we can import into crontab,
# and run crond as the primary command in the forground
RUN touch laravel.cron && \
echo "* * * * * cd /opt/apps/laravel-in-kubernetes && php artisan schedule:run" >> laravel.cron && \
crontab laravel.cron
CMD ["crond", "-l", "2", "-f"]
FROM cli
server {
listen 80 default_server;
listen [::]:80 default_server;
# We need to set the root for our sevrer,
# so any static file requests gets loaded from the correct path
root /opt/apps/laravel-in-kubernetes/public;
index index.php index.html index.htm index.nginx-debian.html;
# _ makes sure that nginx does not try to map requests to a specific hostname
# This allows us to specify the urls to our application as infrastructure changes,
# without needing to change the application
server_name _;
# At the root location,
# we first check if there are any static files at the location, and serve those,
# If not, we check whether there is an indexable folder which can be served,
# Otherwise we forward the request to the PHP server
location / {
# Using try_files here is quite important as a security concideration
# to prevent injecting PHP code as static assets,
# and then executing them via a URL.
# See https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/#passing-uncontrolled-requests-to-php
try_files $uri $uri/ /index.php?$query_string;
}
# Some static assets are loaded on every page load,
# and logging these turns into a lot of useless logs.
# If you would prefer to see these requests for catching 404's etc.
# Feel free to remove them
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
# When a 404 is returned, we want to display our applications 404 page,
# so we redirect it to index.php to load the correct page
error_page 404 /index.php;
# Whenever we receive a PHP url, or our root location block gets to serving through fpm,
# we want to pass the request to FPM for processing
location ~ \.php$ {
#NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
include fastcgi_params;
fastcgi_intercept_errors on;
fastcgi_pass ${FPM_HOST};
fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
}
location ~ /\.ht {
deny all;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
@chris-cmsoft
Copy link
Author

Hey Chris, thanks for this super awesome guide! ❤️

Please adjust:

All fixed. Thanks for letting me know Peter ! Glad you're enjoying it :)

Copy link

ghost commented Apr 13, 2022

Hi, this line according to your blog must be changed to:
COPY docker/nginx.conf.template /etc/nginx/templates/default.conf.template

@FFerrinho
Copy link

Hello!
Great tutorial first of all. It's really helping me solve a client's migration to Kubernetes.

I found that on the Dockerfile the PHP version is outdated and I was getting errors on the build. Can you change it from php:8.0-fpm-alpine to php:8.1-fpm-alpine. I also changed the php:8.0-alpine in the cli step locally to keep it the same version, although it wasn't returning any errors on that part.

Also, on the web_server step, on the blog page, you indicate that the nginx template file should be created in a new folder named docker. But in the Dockerfile you attempt to copy from the root. So please change COPY nginx.conf.template /etc/nginx/templates/default.conf.template to COPY docker/nginx.conf.template /etc/nginx/templates/default.conf.template.

Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment