37signals Deploys ONCE to Bypass Multi-Tenant SaaS Complexity

Distributing Rails applications with ONCE - Kevin McConnell — Ruby on Rails

Ruby on Rails

Distributing software directly to customer hardware eliminates cloud overhead and aggregate scaling bottlenecks because single installations run comfortably on standard Rails and SQLite.

AI-assisted summary of Distributing Rails applications with ONCE - Kevin McConnell — original source. Check the original for full context.

Source published:

Key ideas

  • 37signals introduced ONCE to streamline self-hosted application delivery, enabling single-command deployments for commercial products like Campfire and Fizzy without SaaS infrastructure overhead. (0:11)
  • Modern 'rails new' applications natively satisfy all ONCE deployment requirements out of the box, leveraging default Dockerfiles, port 80 Thruster proxies, and SQLite-backed Solid primitives. (15:10)
  • Self-hosted distribution eliminates aggregate scaling bottlenecks, requiring an application instance to withstand only the peak load of an individual customer rather than an entire multi-tenant user base. (5:18)
  • ONCE orchestrates automated upgrades via Rails' container boot migrations and enables zero-downtime daily backups by coupling lifecycle hooks with SQLite's online backup API. (25:50)

Detailed notes

## Background Published on October 1, 2026, on the Ruby on Rails YouTube channel, this talk features Kevin McConnell of 37signals discussing how to distribute Rails applications outside the traditional SaaS model. Drawing on lessons from self-hosted products like Campfire and Fizzy, McConnell introduces ONCE, a tool designed to install web applications via a single command, and explains how standard Rails conventions support self-hosting.

## TL;DR McConnell contrasts the operational overhead and telemetry advantages of SaaS with the privacy, simplicity, and architectural benefits of self-hosted software, which avoids aggregate scaling traps by serving individual organizations. Drawing from 37signals' work on Campfire and Fizzy, he introduces ONCE, an automated deployment tool that packages applications with a console-cartridge simplicity using single-line shell commands. The talk outlines how modern Rails defaults—including SQLite, Thruster, built-in health checks, and Solid primitives—render vanilla Rails apps immediately ready for ONCE deployment. Finally, McConnell details how ONCE automates zero-downtime schema migrations, injects vital runtime environment variables, and achieves atomic, non-disruptive daily backups using container hooks and SQLite's online backup API.

## Atmosphere & style The presentation is pragmatic, calm, and grounded in real-world product delivery rather than abstract theory. McConnell speaks with the measured confidence of an engineer demystifying server management, methodically breaking down self-hosting myths with clear analogies like vintage video game cartridges. The tone feels refreshing and empowering, inviting developers to step away from ballooning cloud complexity in favor of straightforward, cohesive software craftsmanship.

## Bulletin - [00:11](https://www.youtube.com/watch?v=eE4aVg15tbs&t=11s) 37signals introduced ONCE to streamline self-hosted application delivery, enabling single-command deployments for commercial products like Campfire and Fizzy without SaaS infrastructure overhead. - [15:10](https://www.youtube.com/watch?v=eE4aVg15tbs&t=910s) Modern 'rails new' applications natively satisfy all ONCE deployment requirements out of the box, leveraging default Dockerfiles, port 80 Thruster proxies, and SQLite-backed Solid primitives. - [05:18](https://www.youtube.com/watch?v=eE4aVg15tbs&t=318s) Self-hosted distribution eliminates aggregate scaling bottlenecks, requiring an application instance to withstand only the peak load of an individual customer rather than an entire multi-tenant user base. - [25:50](https://www.youtube.com/watch?v=eE4aVg15tbs&t=1550s) ONCE orchestrates automated upgrades via Rails' container boot migrations and enables zero-downtime daily backups by coupling lifecycle hooks with SQLite's online backup API.

## Key takeaways - [01:07](https://www.youtube.com/watch?v=eE4aVg15tbs&t=67s) SaaS and self-hosting represent inverted infrastructure topologies: SaaS centralizes all tenants on shared vendor hardware, while self-hosting distributes independent instances across customer-owned nodes. - [02:22](https://www.youtube.com/watch?v=eE4aVg15tbs&t=142s) Self-hosting shifts hosting expenses and data security liability away from the vendor, offering customers data sovereignty and reduced latency at the cost of vendor operational control. - [04:19](https://www.youtube.com/watch?v=eE4aVg15tbs&t=259s) The primary operational loss in self-hosting is application observability, as vendors cannot monitor remote instances without intrusive telemetry that compromises customer trust. - [05:18](https://www.youtube.com/watch?v=eE4aVg15tbs&t=318s) Deploying standalone instances avoids SaaS scaling anti-patterns like microservices and sharded databases; Campfire proved a modest single server running SQLite can handle peak enterprise chat volume. - [08:35](https://www.youtube.com/watch?v=eE4aVg15tbs&t=515s) Real-world support logs from thousands of Campfire sales revealed that self-hosting friction is largely perceptual, with virtually no users contacting support due to failed installations. - [09:36](https://www.youtube.com/watch?v=eE4aVg15tbs&t=576s) Modeled after plug-and-play vintage video game cartridges, ONCE was designed as a one-shot installer that only pauses to ask for a domain name to configure TLS routing. - [11:54](https://www.youtube.com/watch?v=eE4aVg15tbs&t=714s) The ONCE architecture coordinates Docker containers, persistent data volumes, and a reverse proxy, while surfacing an interactive terminal dashboard for resource and traffic monitoring. - [17:12](https://www.youtube.com/watch?v=eE4aVg15tbs&t=1032s) ONCE integrates cleanly with Rails storage conventions by dual-mounting the host's persistent volume to both '/storage' and '/rails/storage' inside the container simultaneously. - [18:09](https://www.youtube.com/watch?v=eE4aVg15tbs&t=1089s) Rails features like Solid Queue, Solid Cache, and local disk Active Storage allow an entire production-grade stack to run entirely within a container using SQLite, eliminating Redis and S3 dependencies. - [18:09](https://www.youtube.com/watch?v=eE4aVg15tbs&t=1089s) Application updates require zero customer intervention because the Rails Docker entrypoint runs 'db:prepare' on container boot, executing pending migrations before routing live traffic. - [20:29](https://www.youtube.com/watch?v=eE4aVg15tbs&t=1229s) ONCE injects runtime environment variables into managed containers, including per-install SECRET_KEY_BASE values, push keys, user-configured SMTP settings, and NUM_CPUS constraints. - [25:50](https://www.youtube.com/watch?v=eE4aVg15tbs&t=1550s) To prevent SQLite corruption, ONCE defaults to pausing containers during daily backup file copies, but provides pre-backup hooks using SQLite's online backup API to enable live, zero-downtime snapshots.

## Detailed walkthrough ### [00:11](https://www.youtube.com/watch?v=eE4aVg15tbs&t=11s) The Three Pillars: Distribution, Rails, and ONCE

Kevin McConnell introduces the session by outlining three core themes at the intersection of modern web development and software delivery: self-hosted distribution, the architectural strengths of Ruby on Rails, and ONCE—a dedicated deployment tool developed at 37signals. While the prevailing modern default is Software as a Service (SaaS), this talk explores the alternative path of packaging applications so customers can host and run them on their own machines. Rails plays a pivotal role here, as many of its newest built-in conventions and primitives naturally complement the operational requirements of self-hosted software.

The insights shared throughout the presentation stem directly from 37signals' recent experience developing and launching commercial, self-hosted products such as Campfire and Fizzy. Shipping these products exposed the team to new constraints and real-world feedback from end users, highlighting the friction points of self-hosting and ultimately motivating them to design ONCE to make single-command deployments possible.

[01:07](https://www.youtube.com/watch?v=eE4aVg15tbs&t=67s) Flipping the Distribution Model: SaaS vs. Self-Hosted

When analyzing distribution strategies, SaaS and self-hosting represent two fundamentally inverted mental models. SaaS begins with centralized infrastructure—whether that is a dedicated rack of bare-metal servers or managed cloud resources—and invites customers into that shared environment. The provider deploys software to a central place, and every user accesses the exact same multi-tenant or managed infrastructure over the web.

```mermaid flowchart TD accTitle: SaaS vs Self-Hosted Architecture accDescr: A comparison diagram contrasting the centralized infrastructure of SaaS with the decentralized customer-run nodes of self-hosting.

subgraph SaaS["Centralized SaaS Model"] direction TB Cloud["Central Infrastructure<br/>(Cloud / Managed Hardware)"] UserA["Customer A"] --> Cloud UserB["Customer B"] --> Cloud UserC["Customer C"] --> Cloud end

subgraph SelfHosted["Decentralized Self-Hosted Model"] direction TB NodeA["Customer A's Hardware<br/>(Local App + DB)"] NodeB["Customer B's Hardware<br/>(Local App + DB)"] NodeC["Customer C's Hardware<br/>(Local App + DB)"] end ```

The self-hosted paradigm completely flips this topology on its head. Instead of starting with centralized hardware, the model begins with the customer. The vendor delivers a discrete copy of the software directly to the customer, who runs it on their own hardware to serve their specific organization, team, conference, or family. As more customers adopt the application, the result is not a larger, monolithic cluster managed by the vendor, but rather a constellation of hundreds or thousands of tiny, independent, isolated installations running globally on diverse customer hardware.

[02:22](https://www.youtube.com/watch?v=eE4aVg15tbs&t=142s) Operational Costs and Data Ownership

Comparing these models side-by-side reveals that the strengths of one are almost invariably the weaknesses of the other. The most immediate divergence centers on infrastructure ownership and operations:

* **Infrastructure Management & Expense:** In SaaS, the vendor runs the servers, maintaining granular deployment control and the ability to roll out instantaneous bug fixes to all users simultaneously. However, this comes at tremendous financial and operational cost: cloud fees, bandwidth charges, hardware depreciation, and dedicated engineering time to keep systems online. In contrast, self-hosted software requires customers to bring their own hardware, entirely relieving the vendor of hosting costs, bandwidth overhead, and constant infrastructure maintenance. * **Data Custodianship & Privacy:** SaaS vendors assume the immense liability of safeguarding everyone's data in one location, demanding rigorous security controls, compliance regimens, and robust backup pipelines. Self-hosting decentralizes this liability back to the user. For the vendor, this is liberating; for the customer, it guarantees complete privacy, data sovereignty, and minimal latency by keeping data stored physically close to the people using it.

[04:19](https://www.youtube.com/watch?v=eE4aVg15tbs&t=259s) The Visibility Trade-off and Telemetry

The greatest operational superpower of SaaS is complete visibility into how an application behaves. Because all incoming traffic lands on infrastructure owned by the provider, teams can instrument request lifecycles, monitor centralized application logs, capture exception backtraces, and run detailed analytics. This constant telemetry makes it obvious where bugs occur, which features are slow, and where engineers should focus their efforts to maximize impact.

In a self-hosted world, this visibility drops to near zero. Applications run on machines that the developer cannot access and often does not even know the physical location or network address of. Achieving comparable insight would require embedding intrusive telemetry into the application binary or image. Kevin notes that this is neither something developers feel comfortable writing nor something self-hosting customers would tolerate running. Rather than compromising customer trust, 37signals chose to avoid telemetry entirely and accept the inherent trade-off of operating without SaaS-style observability.

[05:18](https://www.youtube.com/watch?v=eE4aVg15tbs&t=318s) Avoiding the SaaS Scaling Trap: Lessons from Campfire

One of the hidden costs of running a large SaaS platform is the architectural complexity introduced strictly to handle aggregate scale. As a SaaS system grows, engineers are frequently forced to dismantle simple designs to prevent bottlenecks:

- Breaking monolithic applications into microservices. - Sharding massive single databases across distributed clusters. - Rewriting straightforward algorithms to handle extreme concurrency. - Leaking infrastructure workarounds directly into core product business logic.

Self-hosting completely circumvents aggregate scaling dilemmas. An individual application instance never needs to absorb the combined traffic of your entire customer base; it only needs to withstand the peak load of the *single largest customer* using that specific instance. While individual organizations vary in size, the workload of even a large company remains orders of magnitude smaller than the collective volume of a shared SaaS platform.

Kevin illustrates this dynamic with the development of Campfire. Early benchmarking showed that a largely vanilla Rails application powered by SQLite running on a single, modest server could comfortably handle the highest traffic volumes the team could reasonably imagine for an enterprise chat installation. By abandoning the multi-tenant SaaS architecture, they avoided immense distributed-systems complexity and preserved a remarkably clean, performant codebase.

[07:05](https://www.youtube.com/watch?v=eE4aVg15tbs&t=425s) Access Control and the Customer Effort Gap

Monetization and customer friction highlight another stark contrast between the two distribution models:

* **Access Control and Billing:** In SaaS, gating access is trivial because every web request passes through an edge proxy or authentication barrier owned by the vendor. Paywalls, user seats, and subscription expirations can be strictly enforced. With self-hosted software, vendors must surrender considerable control. Even if the software is sold for a one-time fee or bundled with a subscription check, "the ship has sailed a little bit" once customers possess a running copy on their own hardware. Attempting to lock down distributed software aggressively often proves counterproductive. * **The Customer Effort Gap:** SaaS established itself as the dominant model because it requires zero technical overhead from the end user: customers simply visit a URL, fill out a sign-up form, and begin working. Self-hosting, by contrast, is a substantially larger ask. Customers must provision machines, handle container or binary installations, configure networking, and maintain the environment over time—a prospect that remains daunting for non-technical users and busy system administrators alike.

[08:35](https://www.youtube.com/watch?v=eE4aVg15tbs&t=515s) Perception vs. Reality: Support Lessons from Campfire

The difficulty of running self-hosted software is largely a problem of perception rather than practical reality. If you clearly communicate the necessary steps in advance, the vast majority of users never encounter friction. 37signals saw this firsthand with Campfire: despite selling thousands of copies to a wide variety of customers and offering dedicated support, "almost nobody" ever reached out because they could not get the application to run.

The inbound support requests they did receive fell into entirely different buckets: routine product functionality questions or users attempting to run Campfire in unsupported, custom environments who wanted to talk through their setups. This track record proved that running software locally is not inherently difficult; it simply requires the right tooling to make it easy. That realization set the stage for ONCE, the tool built to streamline the entire self-hosting lifecycle.

[09:36](https://www.youtube.com/watch?v=eE4aVg15tbs&t=576s) The Game Cartridge Metaphor and the One-Shot Installer

When 37signals began designing their first wave of ONCE applications, they recognized that the entire premise of self-hosted software depended on an effortless setup; if the process felt daunting, people would simply refuse to do it. The mental model the team kept returning to was vintage video game consoles:

> "The metaphor and model that we kept coming back to whenever we thought about software installation was old video game consoles, particularly the kinds where if you bought a new game, it came on a physical cartridge and all you had to do to use that game was just stick that cartridge in the console and it would work super simple."

Web applications inevitably introduce challenges that cartridges never had to solve—such as persistent state, data storage over time, and networking. Even so, the design goal remained constant: achieve a true "one-shot" installation where a user executes a single command and ends up with a fully functional app, rather than wading through a manual list of instructions.

The team ran into only one point of friction preventing a pure single-line execution: web applications exposed to the internet require a hostname or domain name. The installer must know the host name to issue TLS certificates and configure routing if multiple applications share the same server. Rather than running completely unattended and leaving the user with a misconfigured app, ONCE pauses at the very beginning to ask that single question before automating every remaining step.

[11:54](https://www.youtube.com/watch?v=eE4aVg15tbs&t=714s) Installing and Managing Apps: The ONCE Dashboard

The entry point for ONCE relies on the classic shell pattern of curling a script directly into a shell (`curl | sh`). This approach asks virtually nothing of the host server beyond basic terminal access, whether through an SSH session or a cloud provider's web console (like DigitalOcean). Running the base script installs the ONCE binary, resolves any host-level dependencies, and boots the environment. To preserve the one-shot goal, ONCE allows passing an application identifier directly to the command:

```bash curl -fsSL https://once.com/fizzy | sh ```

ONCE recognizes short aliases for first-party products like Campfire and Fizzy, but it is not restricted to them. Supplying any public Docker image repository path triggers the same automated flow. In a live walkthrough installing Fizzy, pasting the command prompts the user for a hostname (using a local domain for development), downloads the container image, provisions the environment, and completes the setup in seconds. The user can immediately open the app in a browser and create their first account.

Once installation finishes, ONCE drops the user into an interactive dashboard that serves two functions:

- **Host and App Monitoring:** Acts as a centralized dashboard displaying all installed applications, traffic stats (such as visitor counts over the past 24 hours and 7 days), and container resource metrics like memory consumption. - **System Management Hub:** Provides navigable menus and keyboard shortcuts to inspect, configure, and maintain running applications, removing the intimidating blank-slate question of "where do I start?" for users unfamiliar with server administration.

[15:10](https://www.youtube.com/watch?v=eE4aVg15tbs&t=910s) Under the Hood and the Four Requirements for ONCE Apps

```mermaid flowchart TD accTitle: ONCE Application Architecture accDescr: Flow showing incoming traffic hitting a reverse proxy before routing to isolated Docker app containers backed by persistent storage.

subgraph Host["Host Server"] Proxy["Web Proxy (TLS Termination & Routing)"]

subgraph Net["Docker Network"] App["Rails Application Container\n(Port 80 + Health Check)"] Vol[("Persistent Volume\n(Application Data & Backups)")] end end

Traffic["Incoming Web Traffic"] --> Proxy Proxy -->|"Routes by Hostname"| App App <--->|"Mounts data path"| Vol ```

Under the hood, ONCE manages applications through standard Docker primitives. For every installed application, ONCE provisions an isolated Docker network, boots the container, and attaches a persistent volume so that database files and uploaded data survive container restarts and upgrades. That persistent volume also serves as the single source of truth when ONCE executes automated application backups. In front of the containers sits a reverse web proxy responsible for TLS termination and routing incoming internet traffic to the appropriate container based on the requested domain name.

Because of this architectural footprint, running any Rails application under ONCE requires meeting four straightforward contract requirements:

1. **Packaged as a Docker image:** The distribution artifact must be an OCI/Docker container image. 2. **Exposed on Port 80:** The container must serve HTTP traffic over default port 80 where the internal proxy routes incoming requests. 3. **Dedicated data directory:** All persistent files, databases, and assets must live under a designated storage path mounted to the persistent Docker volume. 4. **Health check endpoint:** The application must expose an HTTP health check so ONCE can verify readiness before cutting traffic over.

For modern Rails developers, meeting these requirements requires zero specialized tooling. The "short, pithy answer" to making an application compatible with ONCE is simply running `rails new`, because fresh Rails applications ship with Dockerfiles, port 80 defaults, and built-in health checks right out of the box.

[17:12](https://www.youtube.com/watch?v=eE4aVg15tbs&t=1032s) Default Rails Conventions and Storage Trickery

A default Rails Dockerfile is preconfigured to serve web traffic over port 80 via Thruster, giving applications built-in advantages such as accelerated static asset serving and HTTP response caching right out of the box. While Rails natively expects locally persisted data—such as SQLite databases and uploaded files—to live under the path `/rails/storage`, ONCE deliberately avoids being strictly Rails-specific, instead choosing `/storage` as its general standard.

Rather than forcing developers to fight Rails defaults or manually rewrite config files, ONCE resolves the path mismatch through a clever mounting trick: it simply mounts the persistent storage volume at both `/storage` and `/rails/storage` simultaneously. Whether the application writes to the generic path or the Rails default, data lands in the exact same physical host location. Combined with Rails' built-in `/up` health check endpoints, a freshly generated "vanilla Rails app already sort of fits in this environment" without requiring custom deployment scaffolding.

```mermaid flowchart LR accTitle: ONCE Dual-Mount Storage Strategy accDescr: Shows how the persistent volume is mapped to both /storage and /rails/storage inside the container.

subgraph Host["Host Machine"] VOL["Persistent Volume"] end

subgraph Container["Rails Application Container"] S1["/storage<br/>(ONCE default)"] S2["/rails/storage<br/>(Rails default)"] end

VOL -->|"Mount"| S1 VOL -->|"Mount"| S2 ```

[18:09](https://www.youtube.com/watch?v=eE4aVg15tbs&t=1089s) Zero-Downtime Migrations and Built-in Rails Primitives

The standard entry point generated in a Rails Dockerfile runs `db:prepare` every time an application container boots. This design choice proves exceptionally powerful in a distributed self-hosting model:

> "If you imagine you've shipped version one of your app, you might have thousands of people who have it installed who have existing data and their databases are in a particular state. If you then add some more features that require some migrations to run and you ship a newer version of your Docker image, the ONCE tooling will find the newer version and automatically pull that down and upgrade everybody to the latest version."

When ONCE launches the updated container, pending migrations execute automatically before traffic routes to the new code, leaving the database schema fully synchronized without requiring manual intervention from the customer. 37signals has relied on this exact strategy across Campfire and Fizzy, running it "flawlessly" in production.

Modern Rails primitives align effortlessly with this container model: - **Solid Queue**: Background job workers run directly inside the main application container and store execution state in SQLite. - **Solid Cache**: Stores cache data directly in SQLite on the local volume rather than requiring a dedicated Redis instance. - **Active Storage**: Writes attachments directly to disk inside the mounted volume without needing external S3 buckets.

Distributing an app via ONCE reduces the workflow to four basic steps: create the app, build features, push the Docker image to a public or accessible registry, and publish a one-line install command referencing that image. Pasting that command onto any server yields a fully functioning instance within seconds.

[20:29](https://www.youtube.com/watch?v=eE4aVg15tbs&t=1229s) Injected Environment Variables: Secrets, Web Push, SMTP, and CPU Limits

To simplify running self-hosted apps, ONCE injects specific configuration variables into each application container at boot time:

| Environment Variable | Source / Purpose | Application Handling | |:--- |:--- |:--- | | `SECRET_KEY_BASE` | Auto-generated randomly per installation | Rails reads this natively; prevents signed cookies or tokens from one install being accepted by another. | | Web Push Keys | Generated proactively by ONCE for push notifications | Apps using standard web push can read the generated key pair immediately; safe to ignore if unused. | | SMTP Settings | Entered by user via ONCE "Email settings" UI | Feeds third-party credentials (Postmark, SendGrid, Mailgun) directly into Rails Action Mailer configuration. | | `NUM_CPUS` | Configured via ONCE resource constraint UI | Exposes allowed core count so apps can scale background workers accurately to available hardware. |

Email delivery on bare servers often suffers from deliverability and spam filtering problems, making third-party SMTP providers a practical necessity. The ONCE admin dashboard provides a simple form where users plug in credentials from providers such as Postmark, Mailgun, or SendGrid. Those values are injected into the container environment variables. Developers looking for a reference implementation can check the open-source Fizzy repository, specifically `config/environments/production.rb`, which includes a copy-pasteable block for ingesting these values into Action Mailer.

Similarly, if an administrator uses ONCE's resource limiting settings to throttle container memory or assign specific CPU limits via Docker, ONCE sets `NUM_CPUS`. This prevents the container from blindly over-allocating concurrency threads or background job processes beyond the actual hardware allotment.

[24:13](https://www.youtube.com/watch?v=eE4aVg15tbs&t=1453s) Automated Daily Backups and Atomic Snapshot Strategies

ONCE comes with built-in daily backups, generating an archive every 24 hours that contains application data, operational metadata, and installation settings, retaining a rolling 30-day history. However, taking safe backups introduces a core technical challenge: actively written files cannot simply be copied naively from the host filesystem.

In an active SQLite database, copying a file while transactions land on disk risks capturing mismatched page writes, producing a corrupted backup. To prevent this, ONCE's default baseline strategy is simple: it briefly pauses the container during the filesystem copy step. Pausing guarantees a strictly atomic snapshot that can be reliably restored later. Because self-hosted installations are typically single-tenant and modest in size, this momentary pause once per day generally "flies under the radar" without users noticing any service degradation. However, for applications requiring uninterrupted real-time operation, ONCE also supports dedicated hook scripts to safely orchestrate backups without pausing execution.

[25:50](https://www.youtube.com/watch?v=eE4aVg15tbs&t=1550s) Seamless Backups with Pre-Backup and Post-Restore Hooks

Pausing application containers to take a raw file-system snapshot works well for small installations, but it fails to scale when an application generates substantial amounts of data. In high-volume scenarios, the file copying phase can take a considerable amount of time, resulting in noticeable downtime if the app remains paused throughout. To eliminate this bottleneck, ONCE provides application lifecycle hooks that coordinate directly with the backup pipeline, allowing backups to complete smoothly without taking the application offline.

Right before initiating the file copy, ONCE checks for the presence of a pre-backup script inside the container. If that script exists and returns a successful exit status, ONCE interprets this as confirmation that the application has made its own transactional preparations. Because the application guarantees a consistent file-system state, ONCE bypasses the pause step entirely, leaving the container actively running while it copies the data directly into the archive.

```mermaid flowchart TD accTitle: ONCE Backup and Restore Hook Lifecycle accDescr: Flow diagram showing pre-backup execution without container pausing and post-restore preparation before app boot.

subgraph BackupFlow ["Backup Phase"] direction TB A["ONCE initiates backup"] --> B{"Pre-backup script exists?"} B -- Yes & Success --> C["Execute Ruby hook:<br/>SQLite Online Backup API"] C --> D["Safe database snapshot created"] D --> E["Archive created live<br/>(No container pause)"] B -- No / Fails --> F["Pause container & copy"] end

subgraph RestoreFlow ["Restore Phase"] direction TB G["Unpack archive to new volume"] --> H["Invoke post-restore hook"] H --> I["Move snapshot file to default DB location"] I --> J["Boot application container"] end

BackupFlow --> RestoreFlow ```

In 37signals applications like Campfire, this hook is implemented as a lightweight Ruby script leveraging SQLite’s built-in "online backup API". This API safely generates a transactionally consistent copy of the database into a separate file even while concurrent writes and active database updates are taking place. On restore, ONCE reverses this process: it unpacks the backup archive into a brand-new volume, sets up the runtime environment, and fires a post-restore hook immediately before booting the application. In Campfire's setup, the post-restore script moves the safe snapshot back into the primary database location, ensuring the application boots immediately against an intact, transactionally clean data set.

- **Zero-downtime backups**: Invoking a successful pre-backup hook signals ONCE that live file copying is safe, avoiding container pauses. - **SQLite Online Backup API**: Connects directly to SQLite to write out an atomic snapshot to disk concurrently while transactions continue. - **Post-restore hook**: Executes after the archive unpacks into a new volume but before container launch, moving the generated snapshot file into place.

---

[27:34](https://www.youtube.com/watch?v=eE4aVg15tbs&t=1654s) Embracing Self-Hosting with Rails and ONCE

Kevin McConnell wraps up the presentation by inviting Rails developers to reconsider self-hosting as a modern, viable deployment model. Rather than defaulting to multi-tenant Software-as-a-Service (SaaS) architecture for every product, self-hosting provides an appealing alternative for developers seeking greater control, simpler operations, and software ownership.

He emphasizes that developers do not need to rewrite their applications or learn an entirely foreign deployment paradigm. Because a standard `rails new` application using modern defaults—such as SQLite—is practically distribution-ready out of the box, ONCE can package and deploy most Rails apps with minimal adjustments. McConnell encourages the audience to take an existing project or fresh idea, test it with ONCE, and explore the possibilities of distributing standalone Rails software.

## Notable quotes > [07:05](https://www.youtube.com/watch?v=eE4aVg15tbs&t=425s) the ship has sailed a little bit — Kevin McConnell

> [08:35](https://www.youtube.com/watch?v=eE4aVg15tbs&t=515s) almost nobody — Kevin McConnell

> [09:36](https://www.youtube.com/watch?v=eE4aVg15tbs&t=576s) The metaphor and model that we kept coming back to whenever we thought about software installation was old video game consoles, particularly the kinds where if you bought a new game, it came on a physical cartridge and all you had to do to use that game was just stick that cartridge in the console and it would work super simple. — Kevin McConnell

> [11:54](https://www.youtube.com/watch?v=eE4aVg15tbs&t=714s) where do I start? — Kevin McConnell

> [15:10](https://www.youtube.com/watch?v=eE4aVg15tbs&t=910s) rails new — Kevin McConnell

> [17:12](https://www.youtube.com/watch?v=eE4aVg15tbs&t=1032s) vanilla Rails app already sort of fits in this environment — Kevin McConnell

> [18:09](https://www.youtube.com/watch?v=eE4aVg15tbs&t=1089s) If you imagine you've shipped version one of your app, you might have thousands of people who have it installed who have existing data and their databases are in a particular state. If you then add some more features that require some migrations to run and you ship a newer version of your Docker image, the ONCE tooling will find the newer version and automatically pull that down and upgrade everybody to the latest version. — Kevin McConnell

> [18:09](https://www.youtube.com/watch?v=eE4aVg15tbs&t=1089s) flawlessly — Kevin McConnell

> [24:13](https://www.youtube.com/watch?v=eE4aVg15tbs&t=1453s) flies under the radar — Kevin McConnell

> [25:50](https://www.youtube.com/watch?v=eE4aVg15tbs&t=1550s) online backup API — Kevin McConnell

## Mentioned Kevin McConnell, 37signals, ONCE, Ruby on Rails, Campfire, Fizzy, Shopify, Docker, SQLite, Thruster, Solid Queue, Solid Cache, Active Storage, Action Mailer, DigitalOcean, Postmark, SendGrid, Mailgun

## Video description SaaS isn't the only way to ship a Rails app. Drawing on lessons from self-hosted apps like Campfire and Fizzy, Kevin McConnell (37signals) compares SaaS with self-hosting and introduces ONCE, a tool that installs a Rails app with a single command. It shows why a fresh rails new app is already ready to distribute this way and covers automatic migrations, backup hooks, and the settings ONCE handles for you.

*** Thank you Shopify for sponsoring the editing and post-production of these videos. Check out insights from the Engineering team at: https://shopify.engineering/

#RailsWorld #RubyOnRails #ONCE #SelfHosting #37signals #SQLite

[Source](https://www.youtube.com/watch?v=eE4aVg15tbs)

中文简报

37signals推出ONCE以规避多租户SaaS的复杂性 将软件直接分发至客户硬件消除了云端运维开销和总体扩展瓶颈,因为单次安装可在标准Rails和SQLite上顺畅运行。 McConnell将SaaS的运维开销和遥测优势,与自托管软件在隐私、简易性及架构方面的益处进行了对比,后者通过仅服务单一组织避免了总体扩展陷阱。结合37signals在Campfire和Fizzy上的实践,他介绍了自动化部署工具ONCE,该工具能够通过单行Shell命令以类似主机卡带般的极简方式打包应用程序。演讲概述了现代Rails的默认配置(包括SQLite、Thruster、内置健康检查和Solid基语)如何让原生Rails应用能够直接支持ONCE部署。最后,McConnell详细阐述了ONCE如何通过容器钩子与SQLite的在线备份API,自动化实现零停机数据库迁移、注入关键运行时环境变量,并完成原子化且无干扰的每日备份。 37signals推出ONCE以简化自托管软件的交付,使Campfire和Fizzy等商业产品无需承担SaaS基础设施开销即可实现单命令部署。 (0:11) 现代“rails new”生成的应用开箱即完全满足ONCE的所有部署要求,充分利用了默认Dockerfile、80端口Thruster代理以及基于SQLite的Solid基语。 (15:10) 自托管分发模式消除了总体扩展瓶颈,应用实例只需承受单个客户的峰值负载,而无需应对整个多租户用户群体的压力。 (5:18) ONCE通过Rails的容器启动迁移编排自动化升级,并将生命周期钩子与SQLite在线备份API相结合,实现了零停机日常备份。 (25:50)

Reading summary

Kevin McConnell of 37signals argues that self-hosting provides an operationally simpler, cost-effective alternative to multi-tenant SaaS. He outlines how modern Ruby on Rails conventions combine with 37signals' ONCE deployment tool to turn complex web applications into standalone, single-command installations. Inverting the SaaS Operational Model (1:07) McConnell contrasts the centralized topology of SaaS with the decentralized architecture of self-hosting. In SaaS, a single multi-tenant infrastructure serves all users, creating substantial hosting bills, continuous infrastructure maintenance, and immense data custody liabilities. Scaling SaaS often forces engineering teams to dismantle simple designs in favor of microservices, distributed database sharding, and concurrency workarounds. By contrast, self-hosted software runs on customer hardware, eliminating aggregate scaling dilemmas because each instance only needs to handle the traffic of a single organization. Testing on Campfire showed that a vanilla Rails app using SQLite on a modest server easily handled large enterprise chat workloads. However, self-hosting requires clear trade-offs. While SaaS allows vendors instant deployment control, straightforward subscription enforcement, and granular telemetry to diagnose bugs, self-hosted environments run on private customer hardware where external visibility drops to near zero. Rather than compromising user trust by bundling intrusive telemetry into container images, 37signals chose to operate without SaaS-style observability, relying instead on direct user support. The Single-Command Installation Experience (9:36) To lower adoption barriers, ONCE uses the mental model of vintage video game cartridges, where software runs immediately upon insertion without complex configuration. The distribution flow uses a standard shell command (such as `curl -fsSL https://once.com/fizzy | sh`) that provisions the host, installs the ONCE binary, resolves dependencies, and boots the container environment in seconds. To preserve full automation while handling public internet routing, the installer pauses for a single prompt: requesting the target hostname. This input allows the built-in reverse proxy to terminate TLS certificates and route incoming traffic properly. Once installed, ONCE provides an interactive terminal dashboard displaying container memory consumption, visitor traffic across 24-hour and 7-day windows, and navigation menus that simplify ongoing server administration for non-technical users. Mapping Rails Primitives to Container Contracts (17:12) ONCE requires applications to meet four basic architectural constraints: packaging as a Docker image, exposing HTTP traffic on port 80, maintaining a dedicated persistent storage path, and providing an HTTP health check. McConnell highlights that running `rails new` satisfies these prerequisites immediately because modern Rails ships with preconfigured Dockerfiles, port 80 defaults via Thruster, and a built-in `/up` health endpoint. To bridge path conventions between ONCE's standard `/storage` directory and Rails' default `/rails/storage`, ONCE dual-mounts the persistent host volume to both locations simultaneously. Modern Rails components—including Solid Queue for background jobs, Solid Cache for caching, and Active Storage for uploads—operate entirely against local SQLite files and disk volumes, eliminating external dependencies like Redis or S3. Furthermore, because default Rails containers invoke `db:prepare` on startup, ONCE executes database migrations automatically whenever it pulls updated images, enabling zero-downtime upgrades without customer intervention. Atomic Backups and SQLite Hook Pipelines (24:13) ONCE executes automated daily backups with a rolling 30-day retention schedule, archiving application data, metadata, and configuration settings. Because copying an active SQLite database during concurrent writes risks capturing mismatched pages and corrupting the archive, ONCE's default mechanism briefly pauses the container to take an atomic filesystem snapshot. While effective for small datasets, this pause causes noticeable downtime as storage volumes grow. To enable zero-downtime backups, ONCE supports pre-backup and post-restore lifecycle hooks. If a pre-backup script is detected inside the container, ONCE leaves the application running while copying files. In Campfire, this hook runs a Ruby script utilizing SQLite's Online Backup API, writing a safe, transactionally consistent snapshot to disk alongside live writes. When restoring, ONCE unpacks the archive onto a fresh volume and invokes a post-restore hook that moves the safe database snapshot into place before booting the container. McConnell concludes that modern Rails conventions have removed the historical complexity of self-hosting, allowing developers to ship maintainable, standalone software without specialized deployment machinery. While the model surrenders centralized metrics and strict access control, it provides developers and customers with independent software ownership and data sovereignty.

Source quotes

[7:05] Kevin McConnell: the ship has sailed a little bit [8:35] Kevin McConnell: almost nobody [9:36] Kevin McConnell: The metaphor and model that we kept coming back to whenever we thought about software installation was old video game consoles, particularly the kinds where if you bought a new game, it came on a physical cartridge and all you had to do to use that game was just stick that cartridge in the console and it would work super simple. [11:54] Kevin McConnell: where do I start? [15:10] Kevin McConnell: rails new [17:12] Kevin McConnell: vanilla Rails app already sort of fits in this environment [18:09] Kevin McConnell: If you imagine you've shipped version one of your app, you might have thousands of people who have it installed who have existing data and their databases are in a particular state. If you then add some more features that require some migrations to run and you ship a newer version of your Docker image, the ONCE tooling will find the newer version and automatically pull that down and upgrade everybody to the latest version. [18:09] Kevin McConnell: flawlessly [24:13] Kevin McConnell: flies under the radar [25:50] Kevin McConnell: online backup API

Original transcript

[0:06] [music] [0:11] Hi there. So I hope everyone's having a [0:13] nice conference so far. Um this talk is [0:16] distributing rails applications with [0:18] once and so it's really a talk about [0:19] three things. It's about distributing [0:22] applications or specifically [0:24] distributing applications to be run in a [0:26] self-hosted style as opposed to uh [0:29] running them as SAS that you might [0:30] normally do. It's also a talk about [0:32] Rails because we'll look at some of the [0:34] specific features in Rails and see how [0:35] they fit into this self-hosted style. [0:37] And it's a talk about once once is a [0:40] tool that we built to try and make this [0:41] whole process as easy as possible. [0:45] The context for this talk really came [0:46] from time that we spent at 37 signals uh [0:49] building and shipping a series of apps [0:50] that we designed to be self-hosted in [0:53] this way. Um we learned quite a lot from [0:55] going through this process both from [0:56] building and shipping them and talking [0:58] to customers who were using them. Um and [1:00] that's what went on to inform the [1:01] tooling that we built and the context to [1:03] them that I'll be talking through in [1:04] this talk. [1:07] So to start with I just wanted to look [1:08] at a couple of different distribution [1:10] models so we can kind of compare the [1:12] details of them. And starting with SAS [1:14] because I think that's the thing that [1:15] most of us are kind of most familiar [1:16] with. [1:18] The way I think about SAS is it really [1:20] needs two things in order to work. You [1:22] need some kind of infrastructure um [1:24] which could be a rack of servers um [1:27] bunch of cloud resources or something [1:28] like that and then you need a bunch of [1:31] customers. And so SAS works by putting [1:33] your software on the on that [1:34] infrastructure and letting your [1:35] customers access it. The self-hosted [1:38] approach kind of spins everything around [1:40] a bit. And so the way I think of this is [1:42] that rather than starting with [1:43] infrastructure, you start with a [1:46] customer. You can give that customer a [1:48] copy of your software which they run on [1:49] their own hardware and then they can use [1:51] that along with their kind of group of [1:53] people, whoever they are. So that might [1:54] be their company, their conference, um [1:57] their [snorts] family or whatever it is [1:58] they need. When the next customer comes [2:01] along, um they do the same thing and now [2:03] you have two installations out in the [2:04] wild. more customers come along and you [2:06] get more installations and eventually [2:08] you end up with a picture that looks a [2:10] bit more like this where you have tons [2:11] and tons of these tiny little individual [2:13] installations all over the place. Each [2:15] one running on a different bit of [2:16] hardware and each one serving a [2:18] different set of uh customers. [2:22] When you put these models side by side [2:24] and look at some of the characteristics [2:25] of them, um I think you often find that [2:28] something that's a strength for one [2:29] turns out to be a weakness for the other [2:31] or vice versa. There's not necessarily a [2:32] better way to do this. It's just two [2:34] different models that serve different [2:35] purposes. [2:37] And the most obvious place to start, I [2:38] think, is the question of who's running [2:40] the infrastructure. So in SAS, you run [2:43] the infrastructure yourself. That can [2:45] have some advantages, like you have a [2:47] lot of control over it. So if you do [2:48] something like find a bug in your app [2:50] and ship a fix for it, you know that all [2:52] of your customers are getting that bug [2:53] fixed right away, which can be nice. Um, [2:55] the less good part about running your [2:58] infrastructure is the fact that you're [2:59] the one who has to pay for it. So now [3:00] you have to buy all the machines or rent [3:02] the cloud resources. You have to pay the [3:04] bandwidth bills. Um you probably have to [3:06] devote somebody's time, your own time or [3:08] someone else to look after this stuff [3:10] and that can get expensive, right? So [3:12] for a a big SAS application, the the [3:14] running costs can be fairly [3:15] considerable. [3:17] Uh whereas in the self-hosted model, [3:18] every customer brings their own [3:20] hardware. You don't have to set that up. [3:22] You don't have to pay for it or look [3:23] after it. So there's a lot less that you [3:24] have to do and kind of a lot [snorts] [3:26] less you have to pay for. [3:29] There's also a big difference in kind of [3:32] the split of responsibility for data [3:34] between these two models. So in the SAS [3:37] setup, all the data that gets gathered [3:39] in your application when people are [3:40] using it is going to land on your [3:41] servers and now you're responsible for [3:43] kind of being um like taking care of [3:46] that data properly. So making sure that [3:48] it's kept securely, it's not going to be [3:50] hacked and stolen. Uh making sure you [3:52] have backups so that if something goes [3:53] wrong, you can recover people's data. [3:56] But in the self-hosted style, that's [3:58] another thing that um the customer does [4:00] for themselves. So now you don't have [4:02] the responsibility that goes with that [4:03] which can be quite freeing. It can also [4:05] be advantageous for the customer because [4:07] they can like from a privacy point of [4:09] view, they can keep their data somewhere [4:10] that no one else can get to. Um they can [4:12] also keep their data physically near [4:14] themselves so they'll have faster access [4:16] to it. [4:19] One thing that SAS does really well [4:21] though I think is you get really good [4:23] visibility into how your application's [4:25] working. Since all the requests into [4:27] your app are coming into your [4:28] infrastructure, you can instrument them. [4:30] You can see your app logs. You can see [4:32] exception reports and so on. And so you [4:34] can see in great detail what's working [4:36] well in your app and what isn't working [4:37] well in your app. Um which can be really [4:40] valuable and it can help you kind of um [4:42] decide where you should be spending your [4:43] time if you want to improve your [4:45] product. On the self-hosted side, you [4:47] don't really have that at all. Um, [4:50] people are going to be installing your [4:51] software on machines that you not only [4:53] do you not have access to them, you [4:54] won't even know where they are. And so, [4:56] the only way you could know how your [4:58] software works there would be to have [5:00] some kind of telemetry. That's probably [5:02] something you're not going to be that [5:03] comfortable doing. It's probably also [5:04] something that your customers would not [5:06] be very comfortable with you doing. Um, [5:08] and so we found it's kind of better just [5:10] to sort of stay away with that, away [5:11] from that and just accept the fact that [5:13] you don't have the visibility that you [5:14] would have with SAS. [5:18] Um, scaling is something that can be [5:20] quite complicated at times with SAS as [5:22] I'm sure a lot of you have run into in [5:24] your jobs. Um, as your application kind [5:27] of gets bigger and more popular, you may [5:28] run into situations where you have to [5:30] engineer your way around some kind of [5:31] scaling problem. Um, so you might have [5:34] to do things like split things up into [5:36] different services or shard big data [5:38] sets into smaller portions that are more [5:40] manageable. Uh, rethink some algorithms [5:42] you're using or something like that. Um, [5:44] and as a programmer, that kind of stuff [5:46] can be fun, but I think from a business [5:47] point of view and a product point of [5:49] view, um, it's not really ideal that you [5:51] have to do that. One of the downsides is [5:53] that it can end up with a lot of [5:55] complexity leaking into your application [5:57] and it's complexity that only exists to [5:59] get around these scaling problems. [6:01] Something that's nice about the [6:02] self-hosted environment on the other [6:04] hand is that scaling problems kind of [6:06] just go away because your application [6:08] only needs to be able to work um well [6:10] enough to serve the biggest individual [6:12] customer. And for sure you'll have some [6:15] customers that are bigger than others, [6:16] but it's still likely that even your [6:18] largest customer is still significantly [6:20] smaller or orders of magnitude smaller [6:22] than that sort of total um kind of [6:24] aggregate workload that you would have [6:26] had if you were running it in SAS. [6:28] This is something that we definitely saw [6:30] this with Campfire. For example, when we [6:32] were working on Campfire early on, we [6:34] did some tests and measurements to see [6:36] what would it take to make sure it [6:38] performed well in kind of the biggest [6:40] installation that we thought someone [6:41] would reasonably want to use that [6:43] product for. And what we found was [6:46] pretty much vanilla Rails application [6:48] running SQLite on a modest single server [6:51] can serve a surprising amount of traffic [6:52] and was easily enough to serve that kind [6:54] of what we imagine to be a very big [6:56] installation. And so we got to avoid any [6:59] of the complexity that might have gone [7:00] on if we were doing it SAS. [7:05] Um, another thing that I think SAS does [7:07] much better or is much easier in the SAS [7:09] world is that it gives you much more [7:11] control over who's accessing your [7:12] software. So if you want to do something [7:14] like charge a subscription or require [7:17] some kind of um conditions about sign up [7:19] before people can use it, it's easy to [7:21] do because again all those requests are [7:23] coming into your infrastructure and you [7:24] can gate that traffic on anything you [7:26] want to gate that traffic on. In the [7:28] self-hosted model, you have to give up [7:31] quite a lot of control there. So you can [7:33] still charge money for software. You can [7:34] still wire things up to um work in a [7:37] subscription model and so on if you want [7:39] to. But really once you've given someone [7:42] um like a working copy of your software, [7:44] the ship has sailed a little bit in [7:46] terms of how much you could then [7:47] restrict what they went would go on to [7:49] do with it. Um you can try and fight a [7:51] bit but I think really you have to just [7:52] accept that there's less control in that [7:54] model. [7:56] And then the last point I wanted to [7:57] mention here, but I think this for the [7:59] purposes of this talk, it's the more [8:00] interesting one is just the difference [8:02] in customer effort that there is between [8:04] these two models. So in SAS, this is [8:07] always super easy for customers, right? [8:08] They don't have to think about software [8:09] and hardware. They just go at your [8:11] product and sign up and start using it. [8:13] This is pretty much what SAS was [8:14] invented for and it works really well. [8:17] On the self-hosted side, it's kind of a [8:19] bigger ask now because now you can give [8:22] software to someone, but you you're also [8:24] telling them that they have to install [8:25] it. They have to look after it and make [8:27] sure it keeps working. [8:29] It can be a bit daunting for people who [8:30] are not used to it. But I will say that [8:32] what we found in our experience is that [8:35] this is really more of a problem of [8:37] perception than a practical problem. um [8:40] you can make it easy for them to do this [8:42] and as long as you sort of communicate [8:43] to people what the steps are going to be [8:46] involved then most people won't run into [8:47] problems and so again using Campfire as [8:50] an example for this you know we sold [8:52] thousands of copies of Campfire to lots [8:54] of different people and we offered [8:56] support with that so if anyone had [8:57] trouble getting it running they could [8:59] contact us and almost nobody did like [9:01] the the support requests that we saw [9:03] come in were much more often just about [9:06] product questions or maybe people who [9:08] wanted to run it in an environment that [9:10] it wasn't really designed to run or [9:11] something like that and they wanted to [9:12] talk through that. Um, but almost nobody [9:15] contacted us to just say they were [9:16] having a hard time running it, which I [9:18] think is pretty encouraging. [9:20] And that's not to say that I think it [9:22] means that running software yourself is [9:24] like inherently easy. I think it shows [9:26] that you can make it easy if you build [9:28] the right tooling. And so that is a [9:30] handy segue to the second part of this [9:32] talk, which is to take a look at once. [9:36] So once is the tooling that we built to [9:39] try and simplify this process. Um we [9:42] started looking into this way back when [9:43] we were working on that first set of [9:45] applications that I mentioned because it [9:48] seemed to us like we knew we were going [9:50] to need to make this a really easy thing [9:52] for customers to do. Otherwise the whole [9:54] premise of making self-hosted software [9:56] it just wasn't going to be viable. [9:58] Right? If it was daunting for people [9:59] they weren't going to do it. [10:01] And the metaphor and model that we kept [10:03] coming back to whenever we thought about [10:05] um software installation was old video [10:08] game consoles, particularly the kinds [10:10] where if you bought a new game, it came [10:11] on a physical cartridge and all you had [10:13] to do to use that game was just stick [10:14] that cartridge in the console and it [10:16] would work super simple. And so we [10:19] thought the closer we could get to that [10:20] kind of feeling or like the spirit of [10:22] that, the better. Of course, running web [10:25] applications is probably never going to [10:26] be quite as simple as that just because [10:28] we have concerns to deal with a [10:29] cartridge-based game didn't have. So [10:32] things like network connectivity or [10:34] managing the data that occurs over the [10:35] time you're using it or something like [10:37] that. But still, it was like a goal to [10:39] aim for to see how close to that we [10:40] could get. [10:42] And I think the main thing we took from [10:43] it was what you really want is to give [10:47] someone a way to install your software [10:48] that's going to oneshot it. You want [10:50] just like a single command. They can run [10:52] that command and they're done. and and [10:54] everything's working. You don't want to [10:55] give people like a series of steps to [10:57] follow um or things that they have to [10:59] learn. [11:01] We could almost get to that. Um but the [11:03] one part that that came up when we [11:05] looked at it was when you're installing [11:07] web applications, it turns out you you [11:10] pretty much always want to know what [11:12] host name or domain name someone's [11:13] trying to run that software on. Because [11:15] usually they're they're going to run [11:16] these applications so they're accessible [11:18] over the internet. So we'll need to know [11:20] things like what name to put on their [11:21] TLS certificate. um if someone's running [11:24] multiple applications on the same [11:25] server, then requests are going to come [11:27] in for all those applications, we need [11:29] to know how to route them again based on [11:30] their names. [11:32] So we sort of had two choices. You can [11:34] either have the thing that nearly [11:35] oneshots the installation but then leave [11:37] something that's not properly configured [11:39] because we don't know the name or we can [11:40] have that process just ask them the [11:42] question at the start. And we found that [11:44] the second one is much easier for people [11:45] because then you kind of you get [11:48] prompted for the one bit of information [11:49] it needs and everything else just gets [11:51] taken care of automatically. [11:54] So the format we landed on for these [11:57] command you can one shot is just the [11:58] good old like curl something into a [12:00] shell construction. I think these are a [12:02] nice way to install software on servers [12:05] particularly because it asks so little [12:07] of the of the server you're installing [12:09] onto, right? You just need to have some [12:10] kind of terminal access. You can do it [12:12] over SSH. You can use the web console in [12:14] Digital Ocean or whatever. But if you [12:16] can get to a terminal, you can paste one [12:18] of these in and that and and you can [12:20] kind of start there. And so this is the [12:22] format we started with. This version [12:24] will be enough to install the once tool. [12:27] So if you paste this into a terminal [12:28] now, it'll install the once tool, sort [12:30] out any dependencies it needs to, and [12:32] run the tool so that you could then go [12:34] on to install some apps. [12:37] But that in itself isn't really this [12:38] whole like oneshotting the process we [12:40] wanted to get to because it doesn't [12:41] leave you yet with the app running. And [12:43] to get around that, we have this form of [12:46] the install where you can specify the [12:47] app that you want. So the once knows a [12:50] few apps by their kind of like short [12:51] name or alias like it knows what [12:53] campfire is, it knows what fizzy is and [12:55] so on. Um but it's not just for those [12:57] apps. It work with any suitable web [12:59] application. So for other applications [13:01] you can just put in the path to a docker [13:04] image and it will install the [13:06] application that lives in that docker [13:07] image. Um and this is the thing that as [13:09] I say other than asking the question [13:11] about the host name it can kind of [13:12] oneshot everything from there. [13:15] This I'll make a bit clearer if I could [13:16] show you an example. So here's a video [13:18] of me doing a like a complete [13:20] installation of fizzy from start to [13:22] finish just by pasting in the the fizzy [13:24] version of this link. [13:26] So see in a moment I'll paste in the [13:28] link that's going to go when I run that [13:30] it's going to go and fetch the ones tool [13:32] and boot that up. Um here's where I get [13:34] to enter the host name. I'm just going [13:35] to use a local host domain in this [13:37] example because I was just trying to run [13:39] it locally on this machine. But then [13:40] when you hit install it will do all the [13:42] remaining steps. It'll go fetch the [13:44] container be the container configure it [13:46] and you're left with something that's [13:47] working. And that's the whole install [13:49] complete. So at this point um fizzy is [13:51] running on this machine can open up in [13:53] the browser. Um, if I was doing this for [13:55] real, I could go into that sign up um, [13:57] section and create my first Dizzy [13:58] account and so on. So, that's what the [14:00] process looks like to use it. [14:03] When the installation is finished, you [14:05] end up back on this screen. And so, this [14:07] is like on these main view whenever you [14:09] have any apps installed. And this has [14:12] two jobs to do really. One is that it [14:14] acts as a dashboard for what's going on [14:16] on the machine. So, you can see all the [14:19] apps you have installed. Here we just [14:20] have one. um you can see some stats [14:23] about how they're behaving. So like how [14:24] much traffic they're serving, how many [14:26] visitors you've had in the last 24 hours [14:28] and seven days, a little bit of what [14:30] their resource usage looks like, so how [14:32] much memory they're using. And so that [14:34] sort of dashboard overview is one of the [14:36] purposes of this. [14:39] Its other purpose is it's kind of the [14:41] jumping off point to do anything you [14:42] need to do to manage these applications [14:44] going forward. So, there's a bunch of [14:46] different menus and screens that you can [14:48] get to either with the mouse or keyboard [14:50] shortcuts. And it's designed to be kind [14:52] of something you can explore. And the [14:55] hope here is it makes it a lot less [14:56] daunting for people who are new to [14:57] running their own apps because instead [14:59] of having that question of like where do [15:01] I start, it's like well here here's [15:02] where you start. Just open this and you [15:04] can probably find the thing that you're [15:06] looking for and do it from inside this. [15:10] Under the hood, all that's really [15:12] happening with this is that we're [15:14] running the apps as Docker containers. [15:16] So, once will make a Docker network for [15:18] each app that you install, it'll boot a [15:20] container for that app. Um, it'll attach [15:22] a persistent volume so the application [15:24] can keep its data in that volume and we [15:26] can keep it safe between um like between [15:28] launches of containers with that's the [15:30] source of um if once takes a backup of [15:32] an application, it's backing up the [15:34] contents of that volume. [15:36] We run the apps behind a little web [15:38] proxy so that way it can um handle the [15:40] TLS termination and it can also route [15:42] requests to the correct app if you have [15:44] multiple apps running on the same [15:45] machine. [15:48] So knowing how these applications run [15:51] and what environment they're in. That [15:52] also helps us start to think about what [15:55] is required of your application to work [15:57] in this environment and what you'd have [15:58] to do if you're building a Rails app [15:59] that needs to fit into it. So that's all [16:02] our second handy segue that takes us [16:03] into the third part of the talk. [16:08] So if you want to build an application [16:10] in Rails that you can distribute this [16:12] way and install with Once, there's [16:15] really four requirements that once makes [16:17] on the application. So it needs to be [16:19] packaged as a Docker image because [16:20] that's what we're using to launch the [16:22] app. It needs to serve web traffic on [16:24] the default port 80 because that's where [16:26] the proxy was going to sell that traffic [16:28] to. [16:29] It needs to keep all of its data inside [16:31] a particular path because that's the [16:33] path that will have mounted that [16:34] persistent volume onto. So anything that [16:37] you application wants to keep needs to [16:38] go in there. Um and it needs to have a [16:41] health check endpoint so that the [16:42] tooling knows when your application's [16:44] kind of ready and healthy and can start [16:46] sending traffic to it. [16:49] If you're starting a new Rails app that [16:50] you might want to distribute this way, [16:52] then the sort of short piffy answer for [16:54] what you need to do to make it adhere to [16:56] these principles is to run Rails new [16:59] because these are all just defaults that [17:01] Rails already does out of the box [17:03] anyway. So, a new Rails app um already [17:06] comes kind of packaged for Docker [17:07] because there'll be a Docker file [17:08] generated when you created the [17:10] application. the configuration in that [17:12] docker file will serve web traffic over [17:14] port 80 because it'll have thruster [17:16] installed which will handle the traffic [17:17] of that port. That also means you get [17:19] some of the thruster nice things like [17:21] faster asset serving and response [17:23] caching and so on. [17:25] Um the by default a rails app will store [17:28] its data in the right place but this [17:30] works due to like a bit of trickery that [17:32] once is doing. Um so we didn't want the [17:35] once tool to be too rails specific which [17:37] is why it uses this path of SL storage [17:39] but rails already has a convention that [17:41] locally stored data like SQLite [17:43] databases so on will be under this path [17:45] rails/ storage um and we didn't want to [17:48] fight that convention either. So once [17:51] gets around this by just mounting the [17:53] same volume in both places. So it [17:54] doesn't matter which of these paths you [17:55] use. You're still writing them to the [17:56] same place and either one will work. [17:59] And then the healthare request [18:00] requirement, that's also something [18:01] that's just built into Rails. So that [18:04] means your kind of new vanilla Rails app [18:06] already sort of fits in this [18:07] environment. [18:09] But there are other things that you'll [18:10] typically reach for when you're building [18:12] the Rails app. Um, many of which also [18:14] work quite well when you're when you're [18:16] kind of shipping your app this way. [18:19] Something that's configured on by [18:21] default, but which you would want to [18:22] keep is automatic database migrations. [18:26] So you probably know that the the Docker [18:28] file that gets generated for new Rails [18:30] applications uses the entry point to run [18:33] DB prepare whenever you boot the [18:35] application container. So that has the [18:37] effect that if there's any pending [18:38] migrations that need to applied in that [18:40] environment. Then when you try to boot [18:41] the app, they'll get applied before the [18:42] app starts. [18:44] This works really well in that self [18:46] hosting environment because you know if [18:48] you imagine you've shipped version one [18:50] of your app you might have thousands of [18:51] people who have it installed who have [18:53] existing data and their databases are in [18:54] a particular state. If you then add some [18:57] more features that require some [18:58] migrations to run and you ship a newer [19:00] version of your docker image uh the once [19:03] tooling will find the newer version and [19:04] automatically cool that down and upgrade [19:06] everybody to the latest version. And in [19:09] doing that, when it boots the new app [19:10] container, the migrations will be [19:12] applied and so everyone's database will [19:13] be in the right form that you expect it. [19:15] So this process works really well. We've [19:17] been doing it this way for all of those [19:18] apps that I mentioned earlier and it's [19:20] been kind of flawless. [19:23] If you need uh background jobs in your [19:25] app, Solid Q fits in quite well in this [19:27] environment. So you can run its workers [19:29] in the app container. It can store it [19:31] data in SQL light. Um the default [19:34] settings that you get there just kind of [19:35] fit. Same with solid cache. Um and same [19:38] with active storage like all of these [19:40] can just store the data they need to [19:41] locally on disk which would be in that [19:43] volume and so they they tend to work [19:44] quite well. [19:48] So what all that means is that if you [19:50] wanted to build a new app and distribute [19:52] it this way then the steps you have to [19:54] follow to go from your new app to [19:56] someone being able to very easily [19:57] oneshot that install that we showed [19:59] earlier um is really just a few steps. [20:01] You have to make your new app, build [20:04] your features, and then just build and [20:07] push the Docker image for your app to [20:08] somewhere that's going to be accessible [20:10] to people, and then make one of these [20:12] install commands of that format that [20:14] references um your Docker image in it. [20:16] You can just then share that command [20:17] with anybody, and if they paste it into [20:20] a server, they'll have your app running [20:21] in kind of no time at all in that sort [20:23] of semi-managed environment that we [20:25] looked at. [20:29] So that really covers most of uh on [20:32] these requirements from your app. I just [20:35] also wanted to mention a few things that [20:36] sort of work in the other direction [20:37] which is things that once does that your [20:39] app might be able to take advantage of [20:41] just to kind of smooth out some parts of [20:43] the process of working in this way. [20:46] And there's really two categories of [20:47] these. There's some environment [20:48] variables that one sets up for you. Um [20:51] and there are also hooks that you can [20:54] use to make the backup process work [20:55] better. So to start with sorry to go [20:58] through a few environment variables and [21:00] the first thing is just to know that um [21:02] once is going to take care of this the [21:04] question of assigning a secret key base [21:06] for you. Um obviously in every [21:09] installation you do of a rails app you [21:10] need to have like a unique secret key [21:12] base so that secrets that are generated [21:14] by that application are unique to that [21:16] environment and can't be reused. So [21:18] people can't do something like take a [21:19] signed cookie from one installation of [21:21] your app and use it somewhere else. So, [21:23] in this self-hosted environment where [21:24] you have potentially thousands of these [21:26] installations, they all have to have [21:27] unique secrets. Um, once we'll do this [21:30] for you, it'll just make a random secret [21:32] key base for each installation that [21:34] someone does and we'll pass it through [21:35] the container, this environment [21:36] variable. Um, Rails already looks here [21:39] for that. So, you don't actually have to [21:40] do anything about this. I mentioned it [21:41] just so that you know that it's sort of [21:43] it's been handled. It's not something [21:44] you have to think about in your app [21:46] itself. [21:49] Similarly, uh once we'll create a key [21:51] pair for use with web push. So if your [21:54] application needs to send push [21:55] notifications, you can do that using the [21:57] web push standard. Uh but you'll need [22:00] some unique credentials again unique for [22:02] each installation. And so in case that's [22:04] something you're going to do, once sort [22:05] of proactively makes a key pair for you [22:07] and passes it through in the [22:08] environment. If you don't use web push, [22:10] you can just ignore them. But if you do [22:12] use web push, then it's already sort of [22:13] there and configured and it makes it [22:14] easier to do. [22:18] If your app has to send email, this one [22:21] can be a bit fiddly set up. So, you're [22:23] not going to be able to have your app [22:24] just send email from whatever random [22:27] server someone's going to install it on. [22:29] Or at least you could, but chances are [22:30] all that email just get flagged as spam [22:32] and not actually get delivered. What's [22:34] likely to work better is to refer people [22:36] to use some kind of third party service [22:38] to send email through. So, like a mail [22:40] gun, send grid, postmark, one of those [22:43] kind of folks. And all of those will [22:46] give people some credentials that they [22:48] can then use to configure their app to [22:49] send through. So to make that process a [22:52] little bit easier to set up, once has a [22:54] settings screen called email settings um [22:56] where someone can just open that up and [22:57] fill in the form with the details that [22:59] they got back from the email provider. [23:01] And if they do that, those will also get [23:03] passed into your app as a set of [23:04] environment variables like this. Uh we [23:07] have fizzy set up in this way. So if you [23:09] have an app that needs to send email, [23:10] you can go and look at the fizzy repo [23:12] and in our production RB, you'll see the [23:14] block where we deal with these [23:15] environment variables. You can just kind [23:16] of copy that bit into your app and make [23:18] that work. [23:21] And then the last environment variable I [23:22] wanted to mention is this numbum CPUs. [23:25] So once also has the screen where if [23:28] someone wants to, they can restrict the [23:30] amount of resources that each app gets [23:32] to use on a server. So you can limit how [23:34] much memory it can access and you can [23:36] limit the total number of CPU cores that [23:38] it was allowed to use. If someone does [23:40] that then one enforces it through Docker [23:43] but it will also pass through that num [23:46] CPUs um as the number of cores that your [23:48] app is supposed to be using into your [23:51] app. So that if you do something like uh [23:54] spawn background worker processes or [23:56] something like that like job workers you [23:58] might want to choose the number that you [24:00] spawn there according to how many cores [24:02] you have access to. so that you're not [24:03] trying to like run far too many [24:05] processes for the environment. So we [24:07] just pass that number through the [24:08] environment variable so it's easier for [24:09] you to set that up. [24:13] And then the other category of things [24:14] that I want to talk about was just these [24:16] hook scripts. So I think I mentioned [24:18] earlier that once has the ability to [24:20] automatically do backups for you because [24:22] that's something that most people are [24:23] going to want set up. Um it'll take a [24:26] backup once per day. um build an archive [24:29] that has all the applications data along [24:30] with like a metadata and settings that [24:32] it needs to recreate the installation [24:34] and it keeps a month's worth of those [24:36] sort of backup archives for you. [24:39] Mostly we can do this automatically but [24:41] there's one wrinkle to it which is that [24:42] it's not necessarily safe to just copy [24:44] data that's being written to you and [24:46] once doesn't really know what's going on [24:48] in your application. [24:50] Um, an example of where this could come [24:51] up is if you're using SQLite, like a [24:53] SQLite database is a file, but if you're [24:56] actively writing to that file, you can't [24:57] just sort of naively copy that file and [24:59] expect to get a valid copy because [25:01] depending on where the sort of page [25:02] changes landed as that as the copy was [25:05] going on, you might end up with a copy [25:06] that's corrupt. So, one really has two [25:09] strategies for dealing with this. The [25:11] first and the simplest is just that we [25:13] pause the container briefly while we do [25:16] that copying step. So that way no rights [25:18] are landing in the file system. We know [25:20] that what we get is an atomic copy and [25:22] it's always something that we're going [25:23] to be able to safely um restore from [25:25] later. [25:27] And often that's fine. Um I think part [25:29] of the um one of the or one of the nice [25:32] things about these self-hosted [25:34] environments is that they tend to be [25:35] small. Like the amount of data there is [25:37] usually smaller. The number of people [25:38] accessing it is fewer. And so if you [25:41] have a brief pause that happens once a [25:42] day, there's a good chance this will [25:43] just kind of fly under the radar and no [25:45] one will notice it. So in many cases you [25:47] can get away with it but it obviously [25:48] won't fit every situation and it doesn't [25:50] scale well if you have the kind of app [25:52] that generates a lot of data where that [25:54] copy step might actually take a while. [25:57] So to improve that it also supports [25:59] these application hooks that you can use [26:02] to sort of um sort of work alongside the [26:06] backup process and make it run a lot [26:07] more smoothly. [26:09] The way this works is that when once [26:12] it's about to take a backup, it will [26:14] first try and um right before it does [26:15] the copy, it'll try and invoke a [26:17] pre-backup script in your container if [26:19] there is one. If it finds there's one [26:21] there and it executes and it returns [26:23] success, then once assumes that your [26:25] application is taking care of whatever [26:27] details need to be taken care of such [26:29] that live copy in the file system is [26:31] fine. And so in that situation, it [26:33] doesn't do the pause. It just continues [26:35] to let your app run while it copies all [26:36] the data into its archive. [26:39] So the way we implement in this in our [26:41] apps um and you can look at something [26:43] like campfire for an example of this is [26:46] that our pre-backup is just a little [26:47] Ruby script that connects to the SQLite [26:50] database and uses an API SQLite has [26:52] called the online backup API that lets [26:55] you take like a transactionally safe [26:57] copy of the database into a separate [26:58] file. So it's fine to do that while [27:00] while the database is changing. So our [27:02] pre-backup pre-backup hoop just does [27:05] that and makes that safe copy onto the [27:06] file system so that that exists in the [27:08] backup. [27:10] If you layer restore a backup um once [27:13] we'll sort of unpack the data into a new [27:14] volume it'll get everything set up ready [27:16] to run your app. Then right before it [27:18] boots your app it'll try and call this [27:19] post restore. So in our example since we [27:21] made that safe copy of the database our [27:23] post restore script just moves that file [27:25] back into place where the application [27:27] database would normally be. So when the [27:29] app boots up, it's it's running the safe [27:30] copy. [27:34] So that's pretty much everything I [27:35] wanted to cover. Um, just wanted to [27:37] quickly recap to say that maybe [27:40] self-hosting is something you'd be [27:41] interested in trying. Maybe you have an [27:42] idea for an app that you think would [27:43] work good in this way. Um, if so, please [27:47] know that we can make the whole process [27:48] pretty easy. Um, once can deploy most [27:50] Rails app just as they are. So it's a [27:52] very simple thing to try. Um, if you're [27:54] interested then love for you to give it [27:56] a shot and let us know how you get on. [27:58] Thanks. [28:00] [applause] [28:02] [music] [28:07] >> [music]