Back to articles
Technology Insight

Cost Optimization: A Comprehensive Guide to Deploying Web Applications Directly to Ubuntu VPS Using Kamal 2

June 4, 2026

Introduction: The Rising Cost of the Cloud Tax

For years, engineering teams have default-migrated to fully managed Platform-as-a-Service (PaaS) providers and complex cloud environments. While these services offer convenience, they come with a hefty premium—often referred to as the "cloud tax." As a project scales, the cost of managed database instances, container orchestration tools, and load balancers can rapidly outpace actual compute utilization.

However, the modern infrastructure landscape is shifting. High-performance Virtual Private Servers (VPS) from providers like Hetzner, DigitalOcean, and Linode offer incredible raw hardware specs at a fraction of the cost. The missing link has always been ease of deployment. Enter Kamal 2. Developed by 37signals, Kamal 2 allows developers to deploy containerized applications directly to bare Ubuntu servers with the speed and zero-downtime smoothness of a PaaS, but without the cloud premium. In this guide, we will explore how to optimize your infrastructure costs by deploying a web application directly to an Ubuntu VPS using Kamal 2.

Why Kamal 2 is a Game-Changer for Cost Optimization

Before diving into the technical setup, it is crucial to understand why Kamal 2 is uniquely positioned for modern web infrastructure optimization:

  • Zero Cloud Lock-in: Kamal utilizes standard Docker containers. You can move your deployment targets from one VPS provider to another in minutes just by changing an IP address in a configuration file.
  • Zero-Downtime Deployments: Using an automated, lightweight reverse proxy (Kamal Proxy), it handles rolling updates seamlessly, ensuring your users experience no interruption.
  • No Server Agent Required: Unlike traditional PaaS alternatives, Kamal operates entirely over SSH from your local machine or CI/CD pipeline. It leaves no heavy agent running on your production VPS, maximizing available RAM and CPU for your application.

Prerequisites and Environment Setup

To successfully execute this deployment strategy, you will need a few foundational elements in place. Ensure you have the following ready before proceeding:

  1. A Fresh Ubuntu VPS: A clean installation of Ubuntu (22.04 LTS or 24.04 LTS is recommended) with a public IP address.
  2. SSH Key Access: Ensure your local machine can connect to the server via SSH as the root user or a user with sudo privileges without entering a password.
  3. A Dockerized Web Application: A project containing a valid Dockerfile that exposes a web server port (e.g., Node.js, Ruby on Rails, Python Django, Go, etc.).
  4. A Container Registry Account: A registry to host your private Docker images, such as Docker Hub, GitHub Packages (GHCR), or GitLab Registry.

Step 1: Preparing Your Local Machine and Remote VPS

First, ensure Docker is installed locally on your development machine, as Kamal relies on local builds or multi-platform buildx features. Next, you need to install the Kamal CLI tool. If you have Ruby installed on your local machine, you can install it via RubyGems:

gem install kamal

Alternatively, if you prefer not to manage a local Ruby runtime, you can run Kamal via a Docker alias or standalone binary packages available on the official GitHub repository.

On the remote Ubuntu VPS, Kamal will automatically attempt to install Docker if it is missing during the initial setup phase. However, ensuring your firewall permits essential web traffic is mandatory. Execute the following on your remote server to open ports 80 and 443:

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw reload

Step 2: Initializing Kamal in Your Project

Navigate to the root directory of your web application on your local machine. Initialize the Kamal configuration structure by running the initialization command:

kamal init

This command generates three critical infrastructure files within your project:

  • config/deploy.yml: The primary configuration file where your servers, registry info, and environment variables are defined.
  • .env: A local-only file designed to hold sensitive production secrets like registry passwords.
  • .kamal/hooks: A directory for lifecycle scripts to automate post- or pre-deployment behavior.

Step 3: Configuring the Deployment Manifest

Open the generated config/deploy.yml file. We will configure it to point to your Ubuntu VPS. Below is a production-ready template tailored for a standard web application setup:

Note: Ensure you replace placeholder values like IPs, registry names, and application names with your actual infrastructure details.


service: my-optimized-web-app
image: username/my-optimized-web-app

servers:
  web:
    - 192.0.2.1 # Replace with your Ubuntu VPS Public IP

registry:
  server: index.docker.io
  username: my-dockerhub-username
  password:
    - KAMAL_REGISTRY_PASSWORD

env:
  secret:
    - DATABASE_URL
    - APP_SECRET_KEY
  clear:
    PORT: 3000

proxy:
  ssl: true
  host: app.yourdomain.com
  app_port: 3000

Next, populate your local .env file with the concrete values for your secrets. This ensures your private credentials are never committed to your Git repository:


KAMAL_REGISTRY_PASSWORD=your_dockerhub_token_or_password
DATABASE_URL=postgresql://user:pass@host:port/db
APP_SECRET_KEY=super_secure_production_key_here

Step 4: Executing the First Deployment

With the configuration locked in, it is time to perform the initial setup and deployment. Kamal streamlines this into automated command sequences. From your local terminal, trigger the setup environment pipeline:

kamal setup

During a kamal setup execution, the following lifecycle actions occur completely autonomously:

  1. Connects to your Ubuntu VPS via SSH.
  2. Installs Docker on the remote system if it is not already present.
  3. Establishes a connection to your specified container registry.
  4. Builds your application image locally using your Dockerfile.
  5. Pushes the compiled image to your private container registry.
  6. Pulls the image down onto the remote Ubuntu server.
  7. Launches the Kamal Proxy and spins up your web container instance, seamlessly routing incoming port 80/443 traffic to your application port.

Step 5: Managing Post-Deployment Workflows

Once your initial deployment is live, subsequent changes require a shorter, optimized command sequence. When you push updates to your source code, you do not need to rerun the heavy setup routine. Simply run:

kamal deploy

Kamal 2 will perform a rolling update. It boots the new container version alongside the old one, waits for the new container to pass basic health checks, updates Kamal Proxy's routing configuration to shift inbound web traffic to the new container, and cleanly stops the legacy container instance. This guarantees a zero-downtime deployment flow without the complexity of Kubernetes.

To inspect logs or monitor your production system health, use these built-in management utilities:

  • kamal app logs: Streams the production standard output/error logs directly to your local terminal.
  • kamal details: Outlines active container versions, run statuses, and proxy configurations across all provisioned servers.
  • kamal rollback [VERSION]: Instantly switches traffic back to a previous container tag if an anomaly escapes your staging pipeline.

Conclusion: High-Performance Infrastructure on a Budget

Optimizing infrastructure costs does not mean sacrificing modern deployment paradigms. By pairing a low-cost, high-performance Ubuntu VPS with Kamal 2, you retain complete ownership of your underlying hardware while enjoying a clean, declarative, and robust deployment workflow. You can save hundreds or thousands of dollars a month in artificial cloud premiums while serving traffic from a bare-metal architecture tailored exactly to your resource demands. If you are looking to maximize efficiency and regain control of your operating costs, migrating your web projects to Kamal 2 is an exceptionally practical path forward.