One VM is a boundary problem
Running two Rails applications on one virtual machine is not hard because Capistrano is exotic. It is hard because the shared machine makes boundaries easy to blur: users, directories, services, environment variables, logs, sockets, ports, and rollback assumptions all live close together.
A good setup starts by treating each application as a tenant. The apps may share an operating system and reverse proxy, but they should not share a release directory, service name, runtime user, database credentials, or secret files unless there is a deliberate reason.
Capistrano helps by making deployment repeatable. It does not decide the isolation model. That decision belongs to the server layout.
Users, directories, and releases
The cleanest shape is usually one Unix user per app. Each user owns a separate tree such as /var/www/app_one and /var/www/app_two, with the standard Capistrano layout underneath: releases, shared, repo, and current.
That separation keeps file permissions simple. A bad deploy in one app cannot rewrite the other app's shared files. Uploads, persistent storage, logs, credentials, and temporary files remain local to the application that owns them.
Release directories are also a rollback boundary. If app one rolls back, app two should not notice. Shared machine resources may still be affected by CPU, RAM, or disk pressure, but the deployment state should not be entangled.
Services, ports, and process names
Each app needs distinct process names. That includes systemd units, Puma service names, sockets, ports, log paths, and any background-worker services. Generic names such as puma.service are fine on a single-app VM and confusing on a shared one.
Use names that include the application slug, for example app-one-web.service and app-two-worker.service. The reverse proxy should route to different sockets or ports, and those targets should be visible in configuration rather than hidden inside deploy scripts.
The goal is operational clarity during incidents. When memory climbs or a deploy fails, the service names should already answer which app is involved.
Environment and secrets
Environment files deserve the same separation as code. Each app should have its own database URL, Rails master key or credentials path, cache settings, hostnames, and third-party tokens. Sharing a .env file between apps saves a few minutes and creates a long-lived risk.
Rails configuration also needs to match the public host. Asset hosts, allowed hosts, mailer URLs, cookie domains, and CORS settings can quietly leak assumptions from one app into another if they are copied without review.
Capistrano can symlink shared configuration files, but it should not become the only place where secrets are understood. The server should be inspectable without reading a deployment recipe line by line.
Deploy flow and rollback
The deploy flow should be independent per application: fetch code, install dependencies, run migrations when appropriate, compile assets, publish the new release, and restart only the services for that app. Cross-app restarts are a smell unless the apps intentionally share a runtime component.
Database migrations need the most caution. A rollback of code does not automatically roll back data. On a shared VM, the pressure to recover quickly can lead to broad restarts or manual fixes. Keeping each app's deployment path boring makes that pressure easier to manage.
The practical test is simple: can app one deploy, rollback, and restart while app two continues serving traffic? If the answer is not yes, the VM is shared more deeply than expected.
One-VM layout
Running multiple Rails apps on one VM is mostly a naming and isolation problem. Each app needs its own deploy path, service name, environment file, socket or port, and log location.
# config/deploy/app_one.rb
set :application, "app_one"
set :deploy_to, "/var/www/app_one"
set :puma_bind, "unix:///var/www/app_one/shared/tmp/sockets/puma.sock"
# config/deploy/app_two.rb
set :application, "app_two"
set :deploy_to, "/var/www/app_two"
set :puma_bind, "unix:///var/www/app_two/shared/tmp/sockets/puma.sock"
The reverse proxy then routes hostnames to the correct socket. The point is to avoid hidden shared state: do not let two apps share a working directory, PID file, credentials file, or systemd unit by accident.
server {
server_name one.example.com;
location / {
proxy_pass http://unix:/var/www/app_one/shared/tmp/sockets/puma.sock;
}
}
One VM can be perfectly fine for small apps, but the deployment should still make each app independently restartable and independently observable.
Deployment checklist
Each app should have a distinct Unix user or at least a distinct service identity, a distinct deploy path, and a distinct environment file. Sharing the same VM is not the same as sharing runtime state. The common failure is that two apps accidentally point at the same socket, credentials path, log path, or systemd unit. That works until the first rollback or restart affects the wrong process.
The checklist should be boring: hostnames route to the expected socket, each app can be restarted independently, logs identify the app, backups cover each database, and deploy keys cannot write outside their intended directory. If one of those answers is unclear, the VM layout is already too clever.
Sources
- Capistrano documentation, installation. https://capistranorb.com/documentation/getting-started/installation/
- Capistrano documentation, deploy flow. https://capistranorb.com/documentation/getting-started/flow/
- Rails Guides, configuring Rails applications. https://guides.rubyonrails.org/configuring.html