Platform-as-a-Service (PaaS) providers make launching a web application simple. You push code, and they handle the infrastructure. However, as your application grows, the cost scales aggressively. What starts as a $20/month hobby server can scale to hundreds or thousands of dollars for simple database upgrades, extra RAM, or custom background workers.
In the past, moving off a PaaS meant adopting complex container orchestrators like Kubernetes or spending hours writing custom server setup scripts.
Kamal 2 (created by 37signals) offers a middle ground. It is an infrastructure-agnostic deployment tool that allows you to deploy containerized applications to any virtual private server (like Hetzner, DigitalOcean, or bare metal) with zero downtime, using a clean SSH-based workflow.
Here is a practical engineering guide to deploying a production Laravel application using Kamal 2.
1. Prerequisites and Docker Setup
To deploy with Kamal, your application must be containerized. We need a production-ready Dockerfile that packages PHP-FPM, Nginx, and the necessary PHP extensions.
Create a Dockerfile in the root of your Laravel project:
FROM php:8.4-fpm-alpine
# Install system dependencies and PHP extensions
RUN apk add --no-cache \
nginx \
supervisor \
postgresql-dev \
libxml2-dev \
oniguruma-dev \
libpng-dev \
zip \
unzip \
git \
curl \
bash
RUN docker-php-ext-install pdo_pgsql mbstring xml gd bcmath opcache
# Copy Nginx and Supervisor configs
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/supervisord.conf /etc/supervisord.conf
# Copy application files
WORKDIR /var/www/html
COPY . .
# Install composer dependencies
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Set directory permissions
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache
# Expose port 80 and boot supervisor
EXPOSE 80
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
2. Installation and Initializing Kamal
Ensure you have Docker running locally. Then, install Kamal on your local development machine:
gem install kamal
Once installed, run the initialization command in your Laravel project root:
kamal init
This command generates three key files:
config/deploy.yml: The primary deployment configuration file..env: Locally contains secret keys and registry passwords (ignored by git)..kamal/secrets: Scripts for managing secrets securely.
3. Configuring config/deploy.yml
The deploy.yml file defines your servers, your container image registry, and your service configurations. Here is a production configuration setting up a Laravel app and a Redis server on a single VPS:
service: my-laravel-app
image: klytron/my-laravel-app
servers:
web:
hosts:
- 192.0.2.1 # Your VPS IP address
labels:
traefik.http.routers.my-laravel-app.rule: "Host(`app.example.com`)"
registry:
username: klytron
password:
- KAMAL_REGISTRY_PASSWORD
env:
clear:
DB_CONNECTION: pgsql
DB_HOST: 192.0.2.1
DB_PORT: 5432
DB_DATABASE: my_database
REDIS_HOST: my-laravel-app-redis
secret:
- APP_KEY
- DB_PASSWORD
accessories:
redis:
image: redis:7.2-alpine
roles:
- web
port: "6379:6379"
4. Managing Environment Secrets
To prevent sensitive API keys or database passwords from leaking, Kamal retrieves secrets from your local .env file on deployment and injects them into the running container securely.
Define your local variables in your local .env:
KAMAL_REGISTRY_PASSWORD=dckr_pat_your_docker_hub_token
APP_KEY=base64:your_production_laravel_key
DB_PASSWORD=your_production_database_password
When you deploy, Kamal reads these values and writes them to an encrypted environment file inside the target server.
5. Executing the First Deployment
With your servers, registry, and environment variables configured, run the setup routine:
kamal setup
This command will:
- SSH into your target server.
- Install Docker (if it is not already present).
- Install Traefik (the default reverse proxy used by Kamal to route traffic).
- Boot configured accessories (like Redis).
- Build your Docker image locally, push it to your registry, and pull it to your servers.
- Start your web containers and hand off traffic via Traefik.
For all subsequent code updates, simply run:
kamal deploy
Kamal will perform a zero-downtime, rolling update by booting the new container version, verifying its health check, routing Traefik requests to it, and cleanly stopping the old container.
Conclusion
Kamal 2 offers a deployment experience that rivals standard PaaS providers while allowing you to maintain full control over your server cost and layout. By running standard Docker containers under a light SSH orchestrator, you avoid vendor lock-in and can run production environments on cost-effective VPS servers.
If you are looking to move off an expensive PaaS, set up highly reproducible Dockerized environments, or optimize your DevOps pipeline, let’s schedule a call.