Part 1: Setup a dockerized Symfony backend app with ReactJs for frontend with hotloading enabled
As part of my work, I need to build backend services and attach it to frontend, and for this I use docker containers docker-compose
to make life easier during development.
But I have always faced some problems with that approach, specially with UI frameworks like ReactJs which uses hotloading during development phase.
Previously, I have not been doing it correctly, I was just building the API in a container, then I run npm start
locally to run the development server and connect to the backend api and have a headacheless development time.
But, finally I have had come to have the time and effort to research and come to the most optimized way to have a better development dockerized environment.
Why should you use docker?
Docker is the most efficient tool to help you develop your apps in the most predictable way. You can use to avoid unexpected behavior of running your app in different environments setup.
Also, it solve the most biggest problem
Requirements for the development environment
When setting up your day-to-day development environment, you are installing plugins, take caring of a lot of noise around your real work. But in my opinion, you shouldn’t waste time on all the noise around the task at hand, instead your development environment should be:
- Easy to setup and run.
- Environment agnostic.
- Agile, meaning: you don’t need to spend a lot of time to make slightest changes or adjustments.
Also, this comes with a high business value, because:
- It saves on development time, also, it is easier to on-board new comers to your team.
- Your team doesn’t spend too much time fixing cross environment problems or operating system dependencies.
Getting started
As mentioned, during this blog series, I will describe the process on how to setup a php
Symfony application with ReactJs
acting as the frontend with hot-loading (live-loading) enabled for it.
And for this, I will create a backend RESTful api that displays the application healthcheck on the frontend.
So this tutorial will be split into two parts:
- Part 1: Setup a dockerized Symfony backend app with ReactJs for frontend with hotloading enabled.
- Part 2: Setup a SPA reactjs frontend with hot reloading for development
Setting up the project directory
For the ease of use during our tutorial, let’s make a directory which will hold both our backend api and frontend app.
$ mkdir monitoring-app
Setting up the backend API
I’m using Symfony as my php
framework for the backend api service. So, let’s first create a symfony skeleton app.
Creating a Symfony application
To create a skeleton of a symfony app run the below command
$ composer create-project symfony/skeleton api
So the folder monitoring-app
contents would be as below
monitoring-app/
├── api
└── docker-compose.yml
Setting up a basic docker-compose
Second, let’s create docker-compose.yml
, and add the below contents in the file for the API
version: "3.7"
services:
nginx:
build: docker/nginx
restart: on-failure
volumes:
# this will mount the api folder which contains the code to the docker container
- ${PWD}/api:/var/www/api:delegated
# this will mount the nginx logs so that you can check the logs for errors,
# without logging into the container
- ${PWD}/var/logs/nginx/:/var/log/nginx:delegated
# this will create the server config to serve the api
- ${PWD}/docker/nginx/api.conf:/etc/nginx/conf.d/api.conf:delegated
ports:
- "90:80"
depends_on:
- api
- ui
networks:
- monitoring
api:
build: docker/php
restart: on-failure
volumes:
# this will mount the api folder which contains the code to the docker container
- ${PWD}/api:/var/www/api:delegated
# this will mount the custom `.ini` to the container
- ${PWD}/docker/php/php.ini:/usr/local/etc/php/php.ini:ro
networks:
- monitoring
networks:
monitoring:
driver: bridge
The above will create two docker container services:
- nginx: the webserver to serve your api.
- api: which is running
php-fpm
Your Dockerfiles
In the docker-compose.yaml
, we used custom docker images to tweak it to our liking.
Below are the contents of the Dockerfile
and configs for nginx
and php
.
Nginx Dockerfile and config
This would be placed in docker/nginx/Dockerfile
FROM nginx:alpine
MAINTAINER Mina Sami <mina.nsami@gmail.com>
RUN apk update \
&& apk add git curl vim wget bash acl
COPY nginx.conf /etc/nginx/
# remove the default domain conf
RUN rm /etc/nginx/conf.d/default.conf
# set right permissions for symfony cache
RUN HTTPDUSER=$(ps axo user,comm | grep -E '[a]pache|[h]ttpd|[_]www|[w]ww-data|[n]ginx' | grep -v root | head -1 | cut -d\ -f1) \
&& setfacl -dR -m u:"$HTTPDUSER":rwX -m u:$(whoami):rwX var \
&& setfacl -R -m u:"$HTTPDUSER":rwX -m u:$(whoami):rwX var
And in docker/nginx/nginx.conf
user nginx;
worker_processes 4;
pid /run/nginx.pid;
events {
worker_connections 2048;
multi_accept on;
use epoll;
}
http {
server_tokens off;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 15;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log off;
error_log on;
gzip on;
gzip_disable "msie6";
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
open_file_cache max=100;
}
And to define the api
config to server the symfony project you need to add docker/nginx/api.conf
The below config, is copied from the Symfony’s documentation.
server {
# you can use those domain, if and only if you added them to
# your local /etc/hosts
server_name api.local.internal www.api.local.internal;
root /var/www/api/public;
location / {
# try to serve file directly, fallback to index.php
try_files $uri /index.php$is_args$args;
}
# optionally disable falling back to PHP script for the asset directories;
# nginx will return a 404 error when files are not found instead of passing the
# request to Symfony (improves performance but Symfony 404 page is not displayed)
# location /bundles {
# try_files $uri =404;
# }
location ~ ^/index\.php(/|$) {
# Here I used the docker service name and port for `php-fpm`
fastcgi_pass api:9000;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
# optionally set the value of the environment variables used in the application
# When you are using symlinks to link the document root to the
# current version of your application, you should pass the real
# application path instead of the path to the symlink to PHP
# FPM.
# Otherwise, PHP's OPcache may not properly detect changes to
# your PHP files (see https://github.com/zendtech/ZendOptimizerPlus/issues/126
# for more information).
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
# Prevents URIs that include the front controller. This will 404:
# http://domain.tld/index.php/some-path
# Remove the internal directive to allow URIs like this
internal;
}
# return 404 for all other php files not matching the front controller
# this prevents access to other php files you don't want to be accessible.
location ~ \.php$ {
return 404;
}
error_log /var/log/nginx/api_error.log;
access_log /var/log/nginx/api_access.log;
}
php-fpm Dockerfile and config
The php-fpm
Dockerfile
would be placed in docker/php/Dockerfile
FROM php:7-fpm-alpine
MAINTAINER Mina Sami <mina.nsami@gmail.com>
RUN apk update \
&& apk add git \
curl \
vim \
wget \
bash \
zlib \
zlib-dev \
patch \
icu-dev
# install php dependencies
RUN apk add --no-cache $PHPIZE_DEPS \
&& pecl install -f xdebug \
&& docker-php-ext-install intl opcache bcmath sockets \
&& docker-php-ext-enable xdebug intl opcache bcmath sockets \
&& rm -rf /var/lib/apt/lists/*
# install composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
RUN composer selfupdate
# Set timezone
ENV TIMEZONE=Etc/UCT
RUN ln -snf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && echo ${TIMEZONE} > /etc/timezone \
&& printf '[PHP]\ndate.timezone = "%s"\n', ${TIMEZONE} > /usr/local/etc/php/conf.d/tzone.ini \
&& "date"
CMD ["php-fpm", "-F"]
COPY . /var/www/api
WORKDIR /var/www/api
EXPOSE 9000
And optimize php
with the following php.ini
which would be placed in docker/php/php.ini
date.timezone = ${TIMEZONE}
short_open_tag = Off
log_errors = On
error_reporting = E_ALL
display_errors = Off
error_log = /proc/self/fd/2
memory_limit = -1
; Optimizations for Symfony, as documented on http://symfony.com/doc/current/performance.html
opcache.max_accelerated_files = 20000
realpath_cache_size = 4096K
realpath_cache_ttl = 600
Now, all the configs and Dockerfile
s need are created and put in the right place.
Starting up the containers
Now, that you have everything setup, you can now start the containers. you can do that by:
$ docker-compose up --build
Installing Symfony’s dependencies
To make the symfony app behave like a RESTful API, we need to install some packages as a dependencies
$ docker-compose exec api composer require friendsofsymfony/rest-bundle jms/serializer-bundle symfony/validator
Configuring FOSRestbundle
After installing the fosrestbundle
you will see that it will place a config file in api/config/packages/fos_rest.yaml
, but it is an empty file, in order to make work as expected, you can add the following to it
# Read the documentation: https://symfony.com/doc/master/bundles/FOSRestBundle/index.html
fos_rest:
disable_csrf_role: ROLE_API
param_fetcher_listener: true
body_listener: true
view:
view_response_listener: true
formats:
json: true
html: false
mime_types:
json: [ 'application/json', 'application/json;version=1.0', 'application/json;version=1.1', 'application/json;version=1.2' ]
exception:
enabled: true
format_listener:
enabled: true
rules:
- { path: ^/api/v1, prefer_extension: true, fallback_format: json, priorities: [ json ] }
The above configuration will:
- Put your restful api behind
https://domain.com/api/v1
- Allow
json
format only as default. - Disable csrf for API calls.
For further explanation you can check the FOSRest bundle docs
Creating the health check controller
In your src/Controller
folder, create a new php
file HealthCheckController.php
which this will be the RESTful controller.
<?php
declare(strict_types=1);
namespace App\Controller;
use FOS\RestBundle\Controller\AbstractFOSRestController;
use FOS\RestBundle\Controller\Annotations\Get;
use Symfony\Component\HttpFoundation\JsonResponse;
class HealthCheckController extends AbstractFOSRestController
{
/**
* A controller action function, defined with the
* `http` method `GET` to retrieve the server
* health status.
*
* @Get("/healthCheck", name="get_health_check")
*/
public function getHealthCheck()
{
return new JsonResponse([
'status' => "I'm alive",
'code' => 'ok'
]);
}
}
To make your controllers Restful you need them to extend AbstractFOSRestController
.
Then, the next thing is to create an action function to show the get and respond the health check of the server, using the FOSRestBundle
annotations to define the route for it.
Now, we are one step closer to finishing our first RESTful api
call to retrieve the server status.
Tunning the routes definition
In the fos_rest.yaml
we configured the api routes to be prefixed with /api/v1
url path, that means that all the api
routes will lay under this url path, but in order for this config to complete and take effect, we need one more step to be done.
If you go and look into api/config/routes/annotations.yaml
file, its content will be
controllers:
resource: ../../src/Controller/
type: annotation
kernel:
resource: ../../src/Kernel.php
type: annotation
In order to make all the controller routes prefixed with the path configured in the fos_rest.yaml
file, all you need to do is add the line to file so that it looks like ths
controllers:
prefix: /api/v1
resource: ../../src/Controller/
type: annotation
kernel:
resource: ../../src/Kernel.php
type: annotation
Now, if you run
$ docker-compose exec api php bin/console debug:router
The output will be like this
------------------ -------- -------- ------ --------------------------
Name Method Scheme Host Path
------------------ -------- -------- ------ --------------------------
get_health_check GET ANY ANY /api/v1/healthCheck
_preview_error ANY ANY ANY /_error/{code}.{_format}
------------------ -------- -------- ------ --------------------------
Testing your api call
So, after we have setup everything for our api to work, let’s test it.
If you go to your browser the type http://localhost:90/api/v1/healthCheck
you will get the following response.
Wrapping up
So, we have come to an end of this tutorial, where we so far accomplished the following:
- Created a RESTful Symfony api, using the skeleton of Symfony and FOSRestBundle.
- Created a development using
docker
. - Created our first API endpoint to retrieve the server health check.
- Achieved faster on-boarding for new developers in product teams.
- Help developers focus more on the user stories, rather than worry about a stable development environment.
What is next ?
In the next blog of this series, I will create simple reactjs frontend to consume and API, and make it work with hot-loading.