How to deploy Laravel with Kamal 2 and SSL certificate on any VPS
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.
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:
- How to deploy Rails with Kamal and SSL certificate on any VPS
- How to deploy Rails with Kamal, PostgreSQL, Sidekiq and Backups on a single host
- How to Deploy and Scale your Rails application with Kamal.
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 . /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
- Easy, Perfect for side/small projects: How to deploy Rails with Kamal and SSL certificate on any VPS
- Medium: Perfect for most projects: How to deploy Rails with Kamal, PostgreSQL, Sidekiq and Backups on a single host
- Expert: Perfect for big projects: How to Deploy and Scale your Rails application with Kamal