Create your first Laravel Project with docker

Dan Trif
6 min readApr 3, 2021

This is a quick Laravel 8.x project setup tutorial using docker.

The goal is to set up a Laravel project without having to install external dependencies on your machine.

Why Laravel?

When designing a web application, you can use a range of software and frameworks. We assume, however, that Laravel is the best framework for creating modern, full-stack web applications.

Step 1 — Install composer

Get the latest composer release from here: https://getcomposer.org/download/

Step 2 — Create the Laravel project using composer

Open a terminal and write

$ composer create-project laravel/laravel laravel_docker

A project will be created in the laravel_docker directory.

Step 3 — Setup docker

Create a new file Dockerfile

$ nano Dockerfile

FROM ubuntu:xenial

RUN export TERM=linux

ENV HOME /root
ENV DEBIAN_FRONTEND noninteractive
ENV TERM linux

RUN apt-get update && \
apt-get install -y software-properties-common python-software-properties && \
LC_ALL=C.UTF-8 add-apt-repository -y -u ppa:ondrej/php && \
apt-get update -y && apt-get install -y \
apt-transport-https\
netcat\
nginx\
php7.3-bz2\
php7.3-curl\
php7.3-gd\
php7.3-intl\
php7.3-mbstring\
php7.3-mysqli\
php7.3-sqlite\
php7.3-xml\
php7.3-fpm\
php7.3\
--no-install-recommends

COPY --chown=www-data:www-data . /opt/xyz/webapp/

WORKDIR /opt/xyz/webapp


COPY .docker/nginx.conf /etc/nginx/nginx.conf

RUN chown -R www-data:www-data /opt/xyz/webapp/storage

RUN touch storage/logs/laravel.log
RUN chown -R www-data:www-data

RUN chown -R www-data:www-data /opt/xyz/webapp/public
RUN chmod 755 .docker/start.sh
RUN chmod 755 .docker/start_db.sh

RUN ln -s /opt/xyz/webapp/storage/app/public /opt/xyz/webapp/public/storage

RUN chown -R www-data:www-data /opt/xyz/webapp/storage/app/public
RUN crontab .docker/crontab

CMD [".docker/start.sh"]
EXPOSE 80

Create a new directory .docker in your project root

In the .docker directory create a new bash file start.sh

$ nano start.sh

#!/bin/bash
#set -x

pid=0

# SIGUSR1-handler
my_handler() {
echo "my_handler"
}

# SIGTERM-handler
term_handler() {
if [ $pid -ne 0 ]; then
echo "SIGTERM $pid"
kill -SIGTERM "$pid"
wait "$pid"
fi
exit 143; # 128 + 15 -- SIGTERM
}

# setup handlers
# on callback, kill the last background process, which is `tail -f /dev/null` and execute the specified handler
trap 'kill ${!}; my_handler' SIGUSR1
trap 'kill ${!}; term_handler' SIGTERM

# run application
echo configuring nginx server_name: ${NGINX_SRV_NAME}
sed -i "s|NGINX_SRV_NAME|${NGINX_SRV_NAME}|g" /etc/nginx/nginx.conf

echo configuring nginx root: ${NGINX_ROOT}
sed -i "s|NGINX_ROOT|${NGINX_ROOT}|g" /etc/nginx/nginx.conf

cp .env.example .env

grep -o '#[A-Z_]\{1,\}' .env | while read -r line ; do value="" ; eval "value=\$${line:1}" ; echo Found "$line"; sed -i "s|$line|${value}|g" .env ; done


.docker/start_db.sh ${DB_HOST}:${DB_PORT} -t 60 || exit

php
artisan cache:clear
php artisan key:generate
php artisan migrate --force

service php7.3-fpm start

nginx &
pid="$!"

# Create and start reading logging files laravel will append logs to these files
touch storage/logs/laravel.log
touch storage/logs/worker.log
tail -f storage/logs/* &

# wait forever
while true
do
tail -f /dev/null & wait ${!}
doneThis will start our application when starting the docker container and will be our entry point for the docker container.

Create a new bash file start-db.sh

$ nano start_db.sh

#!/usr/bin/env bash
# Use this script to test if a given TCP host/port are available

cmdname=$(basename $0)

echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }

usage()
{
cat << USAGE >&2
Usage:
$cmdname host:port [-s] [-t timeout] [-- command args]
-h HOST | --host=HOST Host or IP under test
-p PORT | --port=PORT TCP port under test
Alternatively, you specify the host and port as host:port
-s | --strict Only execute subcommand if the test succeeds
-q | --quiet Dont output any status messages
-t TIMEOUT | --timeout=TIMEOUT
Timeout in seconds, zero for no timeout
-- COMMAND ARGS Execute command with args after the test finishes
USAGE
exit 1
}

wait_for()
{
if [[ $TIMEOUT -gt 0 ]]; then
echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT"
else
echoerr "$cmdname: waiting for $HOST:$PORT without a timeout"
fi
start_ts=$(date +%s)
while :
do
(echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1
result=$?
if [[ $result -eq 0 ]]; then
end_ts=$(date +%s)
echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds"
break
fi
sleep 1
done
return $result
}

wait_for_wrapper()
{
# In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
if [[ $QUIET -eq 1 ]]; then
timeout $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT &
else
timeout $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT &
fi
PID=$!
trap "kill -INT -$PID" INT
wait $PID
RESULT=$?
if [[ $RESULT -ne 0 ]]; then
echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT"
fi
return $RESULT
}

# process arguments
while [[ $# -gt 0 ]]
do
case
"$1" in
*:* )
hostport=(${1//:/ })
HOST=${hostport[0]}
PORT=${hostport[1]}
shift 1
;;
--child)
CHILD=1
shift 1
;;
-q | --quiet)
QUIET=1
shift 1
;;
-s | --strict)
STRICT=1
shift 1
;;
-h)
HOST="$2"
if [[ $HOST == "" ]]; then break; fi
shift 2
;;
--host=*)
HOST="${1#*=}"
shift 1
;;
-p)
PORT="$2"
if [[ $PORT == "" ]]; then break; fi
shift 2
;;
--port=*)
PORT="${1#*=}"
shift 1
;;
-t)
TIMEOUT="$2"
if [[ $TIMEOUT == "" ]]; then break; fi
shift 2
;;
--timeout=*)
TIMEOUT="${1#*=}"
shift 1
;;
--)
shift
CLI="$@"
break
;;
--help)
usage
;;
*)
echoerr "Unknown argument: $1"
usage
;;
esac
done

if [[
"$HOST" == "" || "$PORT" == "" ]]; then
echoerr "Error: you need to provide a host and port to test."
usage
fi

TIMEOUT=${TIMEOUT:-15}
STRICT=${STRICT:-0}
CHILD=${CHILD:-0}
QUIET=${QUIET:-0}

if [[ $CHILD -gt 0 ]]; then
wait_for
RESULT=$?
exit $RESULT
else
if [[
$TIMEOUT -gt 0 ]]; then
wait_for_wrapper
RESULT=$?
else
wait_for
RESULT=$?
fi
fi

if [[
$CLI != "" ]]; then
if [[
$RESULT -ne 0 && $STRICT -eq 1 ]]; then
echoerr "$cmdname: strict mode, refusing to execute subprocess"
exit $RESULT
fi
exec $CLI
else
exit $RESULT
fi

This will start the database.

Next, in the .docker directory create a file nginx.conf

$nano nginx.conf

daemon off;

user www-data;
worker_processes auto;

pid /var/run/nginx.pid;

events {
worker_connections 80960;
use epoll;
multi_accept on;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format custom_format 'REQ:$request ST:$status RT:$request_time FWD:$http_x_forwarded_for';

server_tokens off;
open_file_cache max=200000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
tcp_nopush on;
tcp_nodelay on;
sendfile on;
keepalive_timeout 500s;
keepalive_requests 100000;
reset_timedout_connection on;
client_body_timeout 100;
client_max_body_size 20M;
send_timeout 500s;

gzip on;
gzip_types text/plain text/css text/xml image/svg+xml application/javascript text/javascript application/x-javascript application/xml;
gzip_disable "MSIE [1-6]\.";

map $server_name $http_x_forwarded_proto2 {
default $http_x_forwarded_proto;
localhost "https";
}

map $server_name $fastcgi_https {
default "on";
localhost "off";
}

server {
listen 8080;
server_name NGINX_SRV_NAME;
root NGINX_ROOT/public;
index index.php index.html;

# error_log /var/log/nginx/webapp-error.log warn;
# access_log /var/log/nginx/webapp-access.log custom_format buffer=16k flush=9s;
error_log stderr warn;
access_log /dev/stdout custom_format buffer=16k flush=9s;

#browser caching of static assets
location ~* \.(css|js|ico|gif|jpeg|jpg|webp|png|svg|eot|otf|woff|woff2|ttf|ogg)$ {
expires max;
}

location / {
try_files $uri $uri/ /index.php?$query_string;
#if ($http_x_forwarded_proto2 != "https"){
#rewrite ^(.*)$ https://$server_name$1 permanent;
#}
}

location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php/php7.3-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;

fastcgi_param HTTPS $fastcgi_https;
fastcgi_buffers 8 16k;
fastcgi_buffer_size 32k;
fastcgi_connect_timeout 600s;
fastcgi_send_timeout 600s;
fastcgi_read_timeout 600s;
}

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains;";

}
}

Finally, create docker-compose.yml in the project root.

$ cd .. && nano docker-compose.yml

version: '3'
services:
app:
ports:
- 8080:8080
image: local
container_name: app
restart: unless-stopped
build:
context: .
dockerfile: Dockerfile
links:
- "mysql"
environment:
APP_DEBUG: 0
APP_NAME: Laravel
APP_ENV: local
DB_CONNECTION: mysql
DB_DATABASE: db
DB_HOST: mysql
DB_PASSWORD: password
DB_PORT: 3306
DB_USERNAME: root

LOG_CHANNEL: stack
CACHE_DRIVER: file
QUEUE_CONNECTION: sync
SESSION_DRIVER: file
SESSION_LIFETIME: 120
NGINX_ROOT: /opt/xyz/webapp
NGINX_SRV_NAME: localhost
volumes:
- ./:/opt/xyz/webapp

mysql:
image: mysql:5.7.22
container_name: mysql
restart: unless-stopped
tty: true
ports:
- "3306:3306"
volumes:
- ./data:/var/lib/mysql
environment:
MYSQL_DATABASE: db
MYSQL_ROOT_PASSWORD: password

Finally you project structure should look like this:

This file starts a new container running on a Linux server using nginx with a dabase “db” running on localhost:3306. It will write the environment variables from the docker-compose.yml environment section.

Step 4 — Open the container

$ docker-compose up

This will install all dependencies and start the container and also the database and run on localhost:8080.

Step 5 — The result

If you want to see the docker containers type

$ docker ps

You should see:

To see the app open a browser and go to localhost:8080.

You should see this:

Enjoy coding!

--

--