Step-by-Step Guide: Installing Laravel with Docker on Ubuntu 22.04

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 the app 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.

Laravel Homepage

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.