DevOps

How to deploy Laravel with Kamal 2 and SSL certificate on any VPS

Profil Picture

Guillaume Briday

6 minutes

I recently purchased a new dedicated server and decided to deploy my most-starred GitHub project, laravel-blog. Since this project has never been in production, it can be difficult for developers to get a clear sense of what it looks like without running it locally, which poses a significant challenge.

kamal

I've already discussed a lot about Kamal with Rails, and many of the concepts we'll explore here with Laravel are similar to those in my previous posts. I highly recommend checking them out for insights that you can adapt to your own needs:

In simple terms, Kamal provides a set of well-crafted scripts that execute Docker commands via SSH, making the deployment process easier. Although it was initially designed for Rails applications, Kamal can be used with any web app that can be containerized – including, in our case, a Laravel app.

In this blog post, we'll cover how to:

  • Install Kamal 2 in a PHP project.
  • Set up your DNS.
  • Persist a SQLite database in production.
  • Create a Dockerfile with PHP-FPM, Node, and Nginx.
  • Set up Kamal-proxy and Nginx to automatically generate SSL certificates.
  • Configure Laravel to run behind a reverse proxy.
  • Deploy the application.

1. Install Kamal 2

Kamal comes as a Ruby Gem:

$ gem install kamal

After installation, verify that it's correctly installed by checking the version:

$ kamal version

For more info, see: https://kamal-deploy.org/docs/installation/.

2. Prepare the Configuration Files

To generate the necessary configuration files for Kamal, run the following command:

$ kamal init

This command will create two essential files: .kamal/secrets and config/deploy.yml.

  • .kamal/secrets: This file is where you define your secrets environment variables that will be pushed to your servers.
  • config/deploy.yml: This file contains your Kamal configuration settings.

If you use the default Docker Hub, the KAMAL_REGISTRY_PASSWORD in your .env must be a token, not your actual password.

See: https://docs.docker.com/security/for-developers/access-tokens/ for more information.

The most important file, config/deploy.yml:

# Load variables from `.env` without exposing it
<% require "dotenv"; Dotenv.load(".env") %>

# Name of your application. Used to uniquely configure containers.
service: laravel-blog

# Name of the container image.
image: guillaumebriday/laravel-blog

# Deploy to these servers.
servers:
  web: # We can have as many servers as you want
    - 192.168.0.1

proxy:
  app_port: 8080 # This is the default port for HTTP to Nginx with the `serversideup/php:8.3-fpm-nginx` image we are going to use.
  ssl: true # Handle SSL at the proxy level
  host: laravel-blog.guillaumebriday.fr # Adapt this to your own domain or hostname.

# Credentials for your image host.
registry:
  username: guillaumebriday

  # Always use an access token rather than real password (pulled from .kamal/secrets).
  password:
    - KAMAL_REGISTRY_PASSWORD

builder:
  arch: amd64

env:
  clear:
    # These are the variables I use to configure the files that are in `config/*.php`. 
    APP_NAME: "Laravel blog"
    APP_ENV: "production"
    APP_DEBUG: false
    APP_URL: "https://laravel-blog.guillaumebriday.fr"
    ASSET_URL: "https://laravel-blog.guillaumebriday.fr"
    DB_CONNECTION: "sqlite"
    DB_DATABASE: "/var/www/html/data/production_laravel_blog.sqlite3"
    MAIL_MAILER: "log"
    SESSION_DRIVER: "file"
    CACHE_STORE: "file"
  secret:
    - APP_KEY # It's going to be pulled up from `.env` automatically

volumes:
  - "data:/var/www/html/data" # Use Docker volume to keep SQLite database on disk between deploys.

# Really handy aliases specific for Laravel
aliases:
  console: app exec --reuse -i "bash"
  tinker: app exec --reuse -i "php artisan tinker"

I recommend creating a data/.gitkeep file in your app to prevent permission issues during deployment. Ensure this folder matches the one specified in your volumes configuration.

3. Prepare your DNS

It's important to configure your DNS early, as it may take some time to propagate. Kamal-proxy will also need the DNS configuration to generate the SSL certificate.

Here’s an example of how to configure the DNS:

laravel-blog 86400 IN A 192.168.0.1 ; Configured as a subdomain.

; OR

@ 86400 IN A 192.168.0.1 ; Configured as a top-level domain.

Make sure to replace 192.168.0.1 with the actual IP address of your server.

4. Prepare your Laravel application

To check if your app is healthy, Kamal will attempt to access the /up route within a new container before directing traffic to it.

Starting from Laravel 11.x, this route is built-in, so there's no need to add it manually. For more details, refer to: Laravel Health Route Documentation.

The key change you need to make is configuring trustProxies on the $middleware instance.

Here's why this is crucial: SSL termination occurs on the Kamal-proxy side, meaning that everything beyond that point is transmitted over plain HTTP. As a result, Nginx and Laravel aren’t aware they are behind an SSL proxy. This configuration tells them to trust the proxy, ensuring everything works as expected.

Now we can update the file bootstrap/app.php:

// bootstrap/app.php
<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__ . '/../routes/web.php',
        commands: __DIR__ . '/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->trustProxies(at: '*'); // This is the key configuration change. Adjust as needed for your setup.
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

See Laravel Documentation: https://laravel.com/docs/11.x/requests#configuring-trusted-proxies.

5. Prepare your Dockerfile

Here’s an example of a Dockerfile you can use for your Laravel project. Be sure to modify it based on your specific requirements.

For instance, you might not need node or sqlite, but you might prefer to install mysql instead.

FROM serversideup/php:8.3-fpm-nginx AS base

# Switch to root so we can do root things
USER root

# Install the exif extension with root permissions
RUN install-php-extensions exif

# Install JavaScript dependencies
ARG NODE_VERSION=20.18.0
ENV PATH=/usr/local/node/bin:$PATH
RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \
    /tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \
    # Corepack will install yarn automatically according to my package.json
    corepack enable && \
    rm -rf /tmp/node-build-master

# Drop back to our unprivileged user
USER www-data

FROM base

# These are environments variables from https://serversideup.net/open-source/docker-php/docs/reference/environment-variable-specification
# Disable SSL on NGINX level, Kamal-proxy will handle that for us.
ENV SSL_MODE="off" 
# See: https://serversideup.net/open-source/docker-php/docs/laravel/laravel-automations
ENV AUTORUN_ENABLED="true"
ENV PHP_OPCACHE_ENABLE="1"
ENV HEALTHCHECK_PATH="/up"

# Copy the app files...
COPY --chown=www-data:www-data . /var/www/html

# Re-run install, but now with scripts and optimizing the autoloader (should be faster)...
RUN composer install --no-interaction --prefer-dist --optimize-autoloader

# Precompiling assets for production
RUN yarn install --immutable && \
    yarn build && \
    rm -rf node_modules

In this case, there’s no need to override the CMD or ENTRYPOINT, as it's already included in the image.

Kamal will handle the build, push, and pull of the Docker image automatically, which is why it requires your credentials to connect to the registry server.

⚠️ You don't need to build and publish the image manually.

6. Prepare your server

With Kamal, everything happen in Docker, so theoretically you don't anything more installed on your server. Also, Kamal will automatically install Docker for you.

But if you have just created a new server, you probably want to configure basic security rules and better default config.

To automate these actions, I created an Ansible playbook so you don't have to run every command manually, it's especially handy if you deploy your app on multiple servers with load balancing.

You can follow the instructions here if you are interested: 👉 Kamal Ansible Manager

Basically, it will upgrade already installed packages and install the UFW Firewall, Fail2ban and NTP.

7. First deploy

Now we're ready to deploy our app for the first time, but before that, we need to set up Kamal:

$ kamal setup

This command will perform the following tasks:

  • Install Docker if it's not already installed.
  • Start and configure the kamal-proxy container.
  • Load all the environment variables from your .kamal/secrets file.
  • Build and push your Docker image to the registry.
  • Launch a new container with the latest version of your app.

8. Workflow

Now that everything should be up and running, here are some essential commands you'll use regularly:

To deploy a new version of your app:

$ kamal deploy

Thanks to aliases, you can also easily manage your app with these handy commands:

To start a bash session in a new container using the latest app image:

$ kamal console

To open a Laravel tinker console in a new container using the latest app image:

$ kamal tinker

To view your application logs:

$ kamal app logs

9. Conclusion

It's easy as that, enjoy your reliable and cheap deploy! 🚀

A big thanks to Tony Messias for the inspiration behind this blog post. You should definitely follow him on Twitter!

📢 This post is part of a series on Kamal

  1. Easy, Perfect for side/small projects: How to deploy Rails with Kamal and SSL certificate on any VPS
  2. Medium: Perfect for most projects: How to deploy Rails with Kamal, PostgreSQL, Sidekiq and Backups on a single host
  3. Expert: Perfect for big projects: How to Deploy and Scale your Rails application with Kamal

Simplify your time tracking with Timecop

Timecop is a time tracking app that brings simplicity in your day to day life.

Timecop projects