This guide walks you through deploying your first _server.yml application using buoyant.
buoyant is an R package for deploying web server applications to DigitalOcean. It works with any R web framework that implements the _server.yml standard, including:
_server.yml standardWhen you deploy an application with buoyant:
do_configure_https())_server.yml StandardThe _server.yml standard is a lightweight specification that allows different R web frameworks to be deployed consistently. At minimum, your application needs a _server.yml file with an engine field:
engine: plumber2The engine R package must contain a launch_server(settings, host = NULL, port = NULL, ...) function that: - Accepts the path to the starting server file as the first argument. The starting server file must have a _server.yml in the same folder. - Can override host and port via parameters - Starts the web server
There are no regulations on any other fields within _server.yml.
{plumber2} adheres to the _server.yml standard and in the following we will go through how it does so as an example for other engine developers who wish to support it.
{plumber2} currently defines 3 fields in addition to the engine field:
routes: An array of R files or folders that contain the server logic as annotated functions and objects.constructor: A path to a single R file that constructs the Plumber2 object.options: A list of options to use for the server. These all corresponds to options that can be seen in ?plumber2::all_opts().{plumber2} provide a function (create_server_yml()) that creates this file for you so that the user can be confident that it is formatted correctly. This is generally a good idea and also provides a place to document the content of the file.
launch_server()The launch_server() function is quite simple for {plumber2} since the api() constructor itself understands _server.yml files as input. Because of this the function looks like this:
{.r launch_server} function(settings, host = NULL, port = NULL, ...) { pa <- api(settings) if (!is.null(host)) { pa$host <- host } if (!is.null(port)) { pa$port <- port } api_run(pa) } <environment: namespace:plumber2>
Create a _server.yml compliant application. For example, with {plumber2}:
{.yaml server_yaml} #| file: _server.yml engine: plumber2 routes: app.R constructor: ~ options: {}
```{.r app} #| file: app.R #* @get /hello function() { list(message = “Hello from buoyant!”) }
#* @get /add #* @query a First number #* @query b Second number function(query) { result <- as.numeric(querya) + as.numeric(queryb) list(result = result) }
## Step 2: Test Locally (Optional)
If you have `{plumber2}` installed:
```{.r}
library(plumber2)
api("_server.yml") |> api_run()
Visit http://127.0.0.1:8080/api-docs to see your API documentation.
# Authenticate with DigitalOcean
# This will open a browser for authentication
analogsea::do_oauth()
# Check authentication and existing droplets
analogsea::droplets() # Should return a list (may be empty)Make sure you provide digitial ocean your public key at https://cloud.digitalocean.com/ssh_keys. Github has some good advice on creating a new public key if you don’t already have one: https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent.
Be sure to store your Host to digitalocean.com within your ssh config to point to your private key that you’ve created:
Host github.com
UseKeychain yes
IdentityFile ~/.ssh/newly_created_key_file
If you didn’t use a password when creating your SSH key, you can skip the UseKeychain line.
You can verify your DigitalOcean SSH key exists with the proper host with by calling buoyant::do_keyfile() and having it return the path to your private key.
library(buoyant)
# This will cost money! Check DigitalOcean pricing first.
# A basic droplet costs around $6/month (as of 2025)
droplet <- do_provision(
name = "my-api-server",
region = "sfo3", # San Francisco
size = "s-1vcpu-1gb" # Basic tier
)
# This will take a few minutes...
# It installs R, nginx, firewall, and sets up the server structure# Validate your _server.yml file first
validate_server_yml("./")
# Deploy!
droplet |>
do_deploy_server(
path = "api", # URL sub-path: /api
local_path = "./_dev/barret", # Local directory
port = 8000, # Internal port
forward = TRUE, # Forwards `/` to `/api`
overwrite = TRUE
)The deployment process:
{plumber2})# Get the URL
url <- do_ip(droplet, "/api")
print(url) # e.g., "http://165.232.134.23/api"
# Open in browser
browseURL(paste0(url, "/hello"))
# Or use httr2 to test
httr2::request(paste0(url, "/hello")) |>
httr2::req_headers(Accept = "text/plain") |>
httr2::req_perform() |>
httr2::resp_body_string()
#> [1] "Hello from buoyant!"
httr2::request(paste0(url, "/add?a=5&b=3")) |>
httr2::req_headers(Accept = "text/plain") |>
httr2::req_perform() |>
httr2::resp_body_string()
#> [1] "8"SSH into your droplet to check logs:
analogsea::droplet_ssh(droplet, "journalctl -u server-api -f")Or to see if the service is running
analogsea::droplet_ssh(droplet, "systemctl status server-api")If you have a domain name:
# Point your domain to the droplet IP
ip <- do_ip(droplet)
print(ip)
# After DNS is configured (wait for propagation):
do_configure_https(
droplet = droplet,
domain = "api.yourdomain.com",
email = "you@example.com",
terms_of_service = TRUE # Agree to Let's Encrypt TOS
)Now your API is available at https://api.yourdomain.com/api
You’ve successfully deployed your first application! Here are some next steps:
overwrite = TRUE when you make changesdo_configure_https() with your domain namejournalctl -u server-myappFor detailed information, see:
{plumber2} docs: https://plumber2.posit.co{analogsea} docs: https://github.com/pachadotdev/analogsea_server.yml specification: https://plumber2.posit.co/articles/server_yml.html