Laravel is a free and open-source PHP framework offering extensive tools and features to build modern PHP applications. It has become a preferred choice among developers for its compatibility with numerous packages and extensions. Laravel includes powerful database tools, such as the ORM (Object Relational Mapper) called Eloquent and mechanisms for creating database migrations. It also comes with a command-line tool called Artisan, which allows developers to quickly generate new models, controllers, and other application components, streamlining the development process.
Containerizing an application involves modifying an application and its components to run efficiently in lightweight environments called containers. This guide demonstrates how to use Docker Compose to containerize a Laravel application for development purposes.
We will create the following three Docker containers for our Laravel application:
- An
app
service running PHP 8.2-FPM - A
db
service running MySQL 8.0 - An
nginx
service to process PHP code using theapp
service and serving the Laravel application to users
Additionally, we will generate an SSL certificate for our Laravel website using Let’s Encrypt.
Prerequisites
-
- A server running Ubuntu 22.04.
- A non-root user with sudo privileges.
- A fully qualified domain name (FQDN) pointing to your server. For the purposes of this guide, we will use
example.com
as the domain name. - Ensure your system is updated:
$ sudo apt update
-
- Install essential utility packages. Note that some may already be installed:
$ sudo apt install wget curl nano software-properties-common dirmngr apt-transport-https gnupg gnupg2 ca-certificates lsb-release ubuntu-keyring unzip -y
Step 1 – Configure Firewall
The first step is to configure the firewall. Ubuntu includes ufw (Uncomplicated Firewall) by default.
To check the firewall status:
$ sudo ufw status
You should see the following output:
Status: inactive
Allow SSH port to prevent the firewall from disrupting the current connection upon enabling:
$ sudo ufw allow OpenSSH
Allow HTTP and HTTPS ports:
$ sudo ufw allow http $ sudo ufw allow https
Enable the firewall:
$ sudo ufw enable Command may disrupt existing ssh connections. Proceed with operation (y|n)? y Firewall is active and enabled on system startup
Verify the firewall status again:
$ sudo ufw status
You should see an output like this:
Status: active To Action From -- ------ ---- OpenSSH ALLOW Anywhere 80/tcp ALLOW Anywhere 443 ALLOW Anywhere OpenSSH (v6) ALLOW Anywhere (v6) 80/tcp (v6) ALLOW Anywhere (v6) 443 (v6) ALLOW Anywhere (v6)
Step 2 – Install SSL
We’ll first create an SSL certificate for our domain outside Docker as it simplifies future maintenance. Later, we’ll sync up the certificates to the container for renewal and refresh.
Install Certbot to generate the SSL certificate. You can use either Ubuntu’s repository or Grab the latest version using Snapd. We’ll be utilizing Snapd.
Ubuntu 22.04 typically includes Snapd by default. Run these commands to ensure Snapd is up to date:
$ sudo snap install core $ sudo snap refresh core
Install Certbot:
$ sudo snap install --classic certbot
Ensure the Certbot command is functioning by creating a symbolic link:
$ sudo ln -s /snap/bin/certbot /usr/bin/certbot
Generate an SSL Certificate:
$ sudo certbot certonly --standalone --agree-tos --no-eff-email --staple-ocsp --preferred-challenges http -m name@example.com -d example.com
This command will download a certificate to the /etc/letsencrypt/live/example.com
directory on your server.
Generate a Diffie-Hellman group certificate:
$ sudo openssl dhparam -dsaparam -out /etc/ssl/certs/dhparam.pem 4096
Validate SSL renewal with a dry run:
$ sudo certbot renew --dry-run
If no errors occur, the certificate will renew automatically.
Step 3 – Install Docker and Docker Compose
To install the latest version of Docker on Ubuntu 22.04, which ships with an older version, first import the Docker GPG key:
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
Create a Docker repository file:
$ echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Update the system repository list and install Docker:
$ sudo apt update $ sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin
Verify service status:
$ sudo systemctl status docker
If you want to manage Docker without repeatedly using sudo
, add your user to the docker
group:
$ sudo usermod -aG docker $(whoami)
Re-login or use this command to apply changes:
$ su - ${USER}
Confirm user’s addition to Docker group:
$ groups navjot wheel docker
Step 4 – Download Laravel and Installing Dependencies
Begin by downloading Laravel and installing Composer, the PHP package manager.
Create a directory for Laravel:
$ mkdir ~/laravel
Navigate to the directory:
$ cd ~/laravel
Clone the latest Laravel release:
$ git clone https://github.com/laravel/laravel.git .
Mount necessary directories for Laravel using Docker’s Compose image:
$ docker run --rm -v $(pwd):/app composer install
This command facilitates dependency management across local and container environments.
Modify Laravel directory ownership to the current user:
$ sudo chown -R $USER:$USER ~/laravel
Step 5 – Create the Docker Compose File
Create and open the Docker Compose file:
$ nano docker-compose.yml
Insert the following configuration:
services: app: build: context: . dockerfile: Dockerfile image: howtoforge/app container_name: app restart: unless-stopped tty: true environment: SERVICE_NAME: app SERVICE_TAGS: dev working_dir: /var/www volumes: - ./:/var/www - ./php/local.ini:/usr/local/etc/php/conf.d/local.ini networks: - app-network webserver: container_name: webserver image: nginx:alpine restart: unless-stopped tty: true ports: - 80:80 - 443:443 volumes: - ./:/var/www - ./nginx/conf.d:/etc/nginx/conf.d - ./nginx/logs:/var/log/nginx - /etc/ssl/certs/dhparam.pem:/etc/ssl/certs/dhparam.pem - /etc/letsencrypt:/etc/letsencrypt logging: options: max-size: "10m" max-file: "3" networks: - app-network db: image: mysql:latest container_name: db restart: unless-stopped tty: true ports: - "3306:3306" environment: MYSQL_DATABASE: laravel MYSQL_ROOT_PASSWORD: MYSQL_ROOT_PASSWORD MYSQL_USER: laraveluser MYSQL_PASSWORD: password SERVICE_TAGS: dev SERVICE_NAME: mysql volumes: - dbdata:/var/lib/mysql - ./mysql/my.cnf:/etc/mysql/my.cnf networks: - app-network volumes: dbdata: driver: local networks: app-network: driver: bridge
Save the file.
The docker-compose.yml
defines three main services:
- App: Specifies Laravel and uses a custom Docker image. The working directory is located at
/var/www
within the container, mapped to the host’s current directory. A PHP configuration file mount enhances flexibility and isolation for PHP settings. - Webserver: Utilizes the Nginx image, binding host ports 80 and 443. Nginx logs, custom configurations, application directory, and SSL certificates are combined in defined volumes.
- Db: This service uses the MySQL image with environment variables for database creation and authentication details. A local MySQL data volume ensures data retention across reboots with customizable storage.
A Docker network named app-network
, set as a bridge network, enables container communication while isolating services on different networks.
Step 6 – Create the Dockerfile
A Dockerfile serves to create custom images for applications. Given Laravel’s lack of a standard image, this Dockerfile will configure dependencies and environments. For deeper insights, refer to our Dockerfile tutorial.
Create and open the Dockerfile:
$ nano Dockerfile
Add the following content:
FROM php:8.2-fpm # Copy composer.lock and composer.json COPY composer.lock composer.json /var/www/ # Set working directory WORKDIR /var/www # Install dependencies RUN apt-get update && apt-get install -y \ build-essential \ libpng-dev \ libjpeg62-turbo-dev \ libfreetype6-dev \ locales \ zip \ jpegoptim optipng pngquant gifsicle \ vim \ libzip-dev \ unzip \ git \ curl \ libonig-dev # Clear cache RUN apt-get clean && rm -rf /var/lib/apt/lists/* # Install extensions RUN docker-php-ext-install pdo_mysql mbstring zip exif pcntl RUN docker-php-ext-configure gd --enable-gd --with-freetype --with-jpeg RUN docker-php-ext-install gd # Install composer RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer # Copy existing application directory contents to the working directory COPY . /var/www # Assign permissions of the working directory to the www-data user RUN chown -R www-data:www-data \ /var/www/storage \ /var/www/bootstrap/cache # Assign writing permissions to logs and framework directories RUN chmod 775 storage/logs \ /var/www/storage/framework/sessions \ /var/www/storage/framework/views # Expose port 9000 and start php-fpm server EXPOSE 9000 CMD ["php-fpm"]
Save the file.
This Dockerfile builds Laravel on the php:8.2-fpm Docker image, setting /var/www
as the working directory and installs essential packages & PHP extensions, copies application files, and adjust permissions for a seamless Laravel experience. It exposes port 9000 for Nginx and runs the PHP-FPM server.
Step 7 – Configure PHP
Create a PHP configuration directory:
$ mkdir ~/laravel/php
Create and edit local.ini
:
$ nano local.ini
Add these lines:
upload_max_filesize=40M post_max_size=40M
Save the file. Adjust values as per your requirement. This file customizes PHP settings, overriding defaults.
Step 8 – Configure Nginx
Create Nginx config directory:
$ mkdir -p ~/laravel/nginx/conf.d
Create and edit app.conf
to use PHP-FPM:
$ nano ~/laravel/nginx/conf.d/app.conf
Add this configuration:
server { # Redirect any http requests to https listen 80; listen [::]:80; server_name example.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; index index.php index.html; error_log /var/log/nginx/error.log; access_log /var/log/nginx/access.log; root /var/www/public; client_max_body_size 40m; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem; ssl_session_timeout 1d; ssl_session_cache shared:SSL:10m; ssl_session_tickets off; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; ssl_ecdh_curve secp384r1; ssl_dhparam /etc/ssl/certs/dhparam.pem; # OCSP stapling ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 5s; location ~ \.php$ { try_files $uri =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass app:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; } location / { try_files $uri $uri/ /index.php?$query_string; gzip_static on; } }
Save the file.
This setup directs Nginx to serve HTTP & HTTPS versions, redirecting HTTP to HTTPS. The fastcgi_pass
directive points to the app
container’s PHP-FPM service, optimized for container separation.
Step 9 – Configure MySQL
Enable MySQL general query logging.
Create MySQL directory:
$ mkdir ~/laravel/mysql
Create and edit my.cnf
:
$ nano ~/laravel/mysql/my.cnf
Paste this code:
[mysqld] general_log = 1 general_log_file = /var/lib/mysql/general.log
Save the file. This enables logging for debugging query issues.
Step 10 – Setting up the Environment File
Configure Laravel’s environment variables in the .env
file.
Create a copy of the example environment file:
$ cp .env.example .env
Edit the .env
file:
$ nano .env
Update the database block:
DB_CONNECTION=mysql DB_HOST=db DB_PORT=3306 DB_DATABASE=laravel DB_USERNAME=laraveluser DB_PASSWORD=your_laravel_db_password
Align these values with those set in your Docker Compose file. Save the file.
Step 11 – Start the Containers and Complete Laravel Installation
Run the following to start containers:
$ docker compose up -d
This pulls and sets up images & containers. Check status with:
$ docker ps
Results should show active containers, similar to:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a57be976c0fa mysql:latest "docker-entrypoint.s…" 6 hours ago Up 6 hours 0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp db 85e515c4a404 howtoforge/app "docker-php-entrypoi…" 6 hours ago Up 6 hours 9000/tcp app 8418bbc83bd3 nginx:alpine "/docker-entrypoint.…" 6 hours ago Up 6 hours 0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp webserver
Complete Laravel installation within the container:
Generate an application key:
$ docker compose exec app php artisan key:generate
Create application cache:
$ docker compose exec app php artisan config:cache
This generates a config cache file to bolster performance.
Visit https://example.com
in your browser, where you should see a Laravel welcome page, confirming successful installation.
Step 12 – Configure SSL Renewal
As the Laravel site is operational, configure SSL for consistency and security enhancements. Create scripts to stop, renew, and restart the webserver
service using Certbot’s pre_hook
and post_hook
.
Create SSL directory:
$ mkdir ~/laravel/ssl
Generate a script to stop the webserver:
$ sh -c 'printf "#!/bin/sh\ndocker stop webserver\n" > ~/laravel/ssl/server-stop.sh'
Generate a script to start the webserver:
$ sh -c 'printf "#!/bin/sh\ndocker start webserver\n" > ~/laravel/ssl/server-start.sh'
Make the scripts executable:
$ chmod +x ~/laravel/ssl/server-*.sh
Open Certbot configuration for your domain:
$ sudo nano /etc/letsencrypt/renewal/example.com.conf
Append these lines at the file’s end:
pre_hook = /home/<username>/laravel/ssl/server-stop.sh post_hook = /home/<username>/laravel/ssl/server-start.sh
Save the file and confirm the renewal process via a dry run:
$ sudo certbot renew --dry-run
This output confirms success:
Saving debug log to /var/log/letsencrypt/letsencrypt.log - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Processing /etc/letsencrypt/renewal/example.com.conf - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Account registered. Hook 'pre-hook' ran with output: webserver Simulating renewal of an existing certificate for example.com - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Congratulations, all simulated renewals succeeded: /etc/letsencrypt/live/example.com/fullchain.pem (success) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Hook 'post-hook' ran with output: webserver
Your SSL will be automatically renewed, maintaining secure connections for your Dockerized Laravel site.
Step 13 – Data Migration and Tinker Console
Post-installation, you can migrate data and experiment using tinker
. The Tinker command line tool engages a PsySH console for intertwined interactions with the Laravel ecosystem.
Migrate initial data into your configured database container:
$ docker compose exec app php artisan migrate
Output:
INFO Preparing database. Creating migration table .............................................................................................. 32ms DONE INFO Running migrations. 2014_10_12_000000_create_users_table .................................................................................. 184ms DONE 2014_10_12_100000_create_password_resets_table ......................................................................... 259ms DONE 2019_08_19_000000_create_failed_jobs_table ............................................................................ 102ms DONE 2019_12_14_000001_create_personal_access_tokens_table .................................................................. 46ms DONE
Launch the Tinker interactive console:
$ docker compose exec app php artisan tinker
Shell session displayed as:
Psy Shell v0.11.10 (PHP 8.2.1 — cli) by Justin Hileman >
Retrieve migration data to test MySQL connectivity:
> \DB::table('migrations')->get();
View the resulting data:
= Illuminate\Support\Collection {#3670 all: [ {#3679 +"id": 1, +"migration": "2014_10_12_000000_create_users_table", +"batch": 1, }, {#3681 +"id": 2, +"migration": "2014_10_12_100000_create_password_resets_table", +"batch": 1, }, {#3682 +"id": 3, +"migration": "2019_08_19_000000_create_failed_jobs_table", +"batch": 1, }, {#3683 +"id": 4, +"migration": "2019_12_14_000001_create_personal_access_tokens_table", +"batch": 1, }, ], }
Type exit
to leave the console:
> exit INFO Goodbye.
Utilize the tinker
console to explore data interactions, with seamless service integration and model deployment, invigorating Laravel developments.
Conclusion
In this tutorial, you successfully containerized and installed Laravel using Docker, MySQL, and PHP, setting the application to function over a secured domain name. Should you have any questions or feedback, please share them in the comments below.
Frequently Asked Questions
- What is the advantage of using Docker for Laravel?Docker offers a consistent development environment and isolates dependencies, decreasing potential configuration conflicts and speeding up deployment across different platforms.
- Why do we need an SSL certificate?An SSL certificate encrypts data between your application and users, enhancing security and ensuring data integrity. It builds trust and can improve search engine rankings.
- Can I customize the services defined in this tutorial?Absolutely! Docker’s flexibility allows developers to adjust service configurations, environment variables, and volumes to better fit specific project needs.
- How can I handle database backups if I use Docker?Scheduled cron jobs can automate MySQL dumps in the container to ensure continuous backups, archived for redundancy.
- What are the benefits of using Tinker in Laravel?Tinker offers a real-time feedback mechanism for quick testing and manipulation of Laravel applications, streamlining development and debugging processes.