diff --git a/DESCRIPTION b/DESCRIPTION index ad7b5b086..a8bb823c0 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -39,7 +39,6 @@ Suggests: base64enc, htmlwidgets, visNetwork, - analogsea (>= 0.7.0), later, readr, yaml, @@ -83,6 +82,7 @@ Collate: 'ui.R' 'utf8.R' 'utils-pipe.R' + 'utils.R' 'validate_api_spec.R' 'zzz.R' RdMacros: lifecycle diff --git a/NEWS.md b/NEWS.md index b1d8d71ad..3952e86ac 100644 --- a/NEWS.md +++ b/NEWS.md @@ -49,9 +49,11 @@ plumber 1.0.0 * `addSerializer()` has been deprecated in favor of `register_serializer()` (#584) -* `getCharacterSet()` has been deprecated in favor of `get_character_set()`. -* `randomCookieKey()` has been deprecated in favor of `random_cookie_key()`. -* `sessionCookie()` has been deprecated in favor of `session_cookie()`. +* DigitalOcean helper functions are now defunct (`do_*()`). The funtionality and documentation on how to deploy to DigitalOcean has been moved to [`plumberDeploy`](https://github.com/meztez/plumberDeploy) (by @meztez) (#649) + +* `getCharacterSet()` has been deprecated in favor of `get_character_set()` (#651) +* `randomCookieKey()` has been deprecated in favor of `random_cookie_key()` (#651) +* `sessionCookie()` has been deprecated in favor of `session_cookie()` (#651) ### New features diff --git a/R/digital-ocean.R b/R/digital-ocean.R index 7dc1c9c39..a6cd32045 100644 --- a/R/digital-ocean.R +++ b/R/digital-ocean.R @@ -1,470 +1,67 @@ -# can't really test these. -# nocov start - -checkAnalogSea <- function() { - if (!requireNamespace("analogsea", quietly = TRUE)) { - stop("The analogsea package is not available but is required in order to use the provisioning functions. Please install analogsea.", - call. = FALSE) - } - - - suggests <- read.dcf(system.file("DESCRIPTION", package = "plumber"))[1, "Suggests"] - pkgs <- strsplit(suggests, ",")[[1]] - pkgs <- trimws(pkgs) - analogsea_version <- gsub("[^.0-9]", "", pkgs[grepl("^analogsea ", pkgs)]) - if (utils::packageVersion("analogsea") < package_version(analogsea_version)) { - stop("The analogsea package is not high enough. Please update `analogsea`.", - call. = FALSE) - } -} - -#' Provision a DigitalOcean plumber server +#' DigitalOcean Plumber server +#' +#' These methods are now defunct. +#' Please use the [`plumberDeploy`](https://github.com/meztez/plumberDeploy) R package. #' -#' Create (if required), install the necessary prerequisites, and -#' deploy a sample plumber application on a DigitalOcean virtual machine. -#' You may sign up for a Digital Ocean account [here](https://m.do.co/c/add0b50f54c4). -#' This command is idempotent, so feel free to run it on a single server multiple times. -#' @param droplet The DigitalOcean droplet that you want to provision (see [analogsea::droplet()]). If empty, a new DigitalOcean server will be created. -#' @param unstable If `FALSE`, will install plumber from CRAN. If `TRUE`, will install the unstable version of plumber from GitHub. -#' @param example If `TRUE`, will deploy an example API named `hello` to the server on port 8000. -#' @param ... Arguments passed into the [analogsea::droplet_create()] function. -#' @details Provisions a Ubuntu 20.04-x64 droplet with the following customizations: -#' - A recent version of R installed -#' - plumber installed globally in the system library -#' - An example plumber API deployed at `/var/plumber` -#' - A systemd definition for the above plumber API which will ensure that the plumber -#' API is started on machine boot and respawned if the R process ever crashes. On the -#' server you can use commands like `systemctl restart plumber` to manage your API, or -#' `journalctl -u plumber` to see the logs associated with your plumber process. -#' - The `nginx`` web server installed to route web traffic from port 80 (HTTP) to your plumber -#' process. -#' - `ufw` installed as a firewall to restrict access on the server. By default it only -#' allows incoming traffic on port 22 (SSH) and port 80 (HTTP). -#' - A 4GB swap file is created to ensure that machines with little RAM (the default) are -#' able to get through the necessary R package compilations. +#' @keywords internal +#' @rdname digitalocean #' @export -do_provision <- function(droplet, unstable=FALSE, example=TRUE, ...){ - checkAnalogSea() - - if (missing(droplet)){ - # No droplet provided; create a new server - message("THIS ACTION COSTS YOU MONEY!") - message("Provisioning a new server for which you will get a bill from DigitalOcean.") - - createArgs <- list(...) - createArgs$tags <- c(createArgs$tags, "plumber") - createArgs$image <- "ubuntu-20-04-x64" - - droplet <- do.call(analogsea::droplet_create, createArgs) - - # Wait for the droplet to come online - analogsea::droplet_wait(droplet) - - # I often still get a closed port after droplet_wait returns. Buffer for just a bit - Sys.sleep(25) - - # Refresh the droplet; sometimes the original one doesn't yet have a network interface. - droplet <- analogsea::droplet(id=droplet$id) - } - - # Provision - lines <- droplet_capture(droplet, 'swapon | grep "/swapfile" | wc -l') - if (lines != "1"){ - analogsea::debian_add_swap(droplet) - } - install_new_r(droplet) - install_plumber(droplet, unstable) - install_api(droplet) - install_nginx(droplet) - install_firewall(droplet) - - if (example){ - do_deploy_api(droplet, "hello", system.file("plumber", "10-welcome", package="plumber"), port=8000, forward=TRUE) - } - - invisible(droplet) -} - -install_plumber <- function(droplet, unstable){ - # Satisfy sodium's requirements - analogsea::debian_apt_get_install(droplet, "libsodium-dev") - - if (unstable){ - analogsea::debian_apt_get_install(droplet, "libcurl4-openssl-dev") - analogsea::debian_apt_get_install(droplet, "libgit2-dev") - analogsea::debian_apt_get_install(droplet, "libssl-dev") - analogsea::debian_apt_get_install(droplet, "libsodium-dev") - analogsea::install_r_package(droplet, "remotes", repo="https://cran.rstudio.com") - analogsea::droplet_ssh(droplet, "Rscript -e \"remotes::install_github('rstudio/plumber')\"") - } else { - analogsea::install_r_package(droplet, "plumber") - } +do_provision <- function(...) { + plumberDeploy_helper("do_provision", list(...)) } -#' Captures the output from running some command via SSH -#' @noRd -droplet_capture <- function(droplet, command){ - tf <- tempdir() - randName <- paste(sample(c(letters, LETTERS), size=10, replace=TRUE), collapse="") - tff <- file.path(tf, randName) - on.exit({ - if (file.exists(tff)) { - file.remove(tff) - } - }) - analogsea::droplet_ssh(droplet, paste0(command, " > /tmp/", randName)) - analogsea::droplet_download(droplet, paste0("/tmp/", randName), tf) - analogsea::droplet_ssh(droplet, paste0("rm /tmp/", randName)) - lin <- readLines(tff) - lin -} - -install_api <- function(droplet){ - analogsea::droplet_ssh(droplet, "mkdir -p /var/plumber") - example_plumber_file <- system.file("plumber", "10-welcome", "plumber.R", package="plumber") - if (nchar(example_plumber_file) < 1) { - stop("Could not find example 10-welcome plumber file", call. = FALSE) - } - analogsea::droplet_upload( - droplet, - local = example_plumber_file, - remote = "/var/plumber/", - verbose = TRUE) -} - -install_firewall <- function(droplet){ - analogsea::droplet_ssh(droplet, "ufw allow http") - analogsea::droplet_ssh(droplet, "ufw allow ssh") - analogsea::droplet_ssh(droplet, "ufw -f enable") -} - -install_nginx <- function(droplet){ - analogsea::debian_apt_get_install(droplet, "nginx") - analogsea::droplet_ssh(droplet, "rm -f /etc/nginx/sites-enabled/default") # Disable the default site - analogsea::droplet_ssh(droplet, "mkdir -p /var/certbot") - analogsea::droplet_ssh(droplet, "mkdir -p /etc/nginx/sites-available/plumber-apis/") - analogsea::droplet_upload(droplet, local=system.file("server", "nginx.conf", package="plumber"), - remote="/etc/nginx/sites-available/plumber") - analogsea::droplet_ssh(droplet, "ln -sf /etc/nginx/sites-available/plumber /etc/nginx/sites-enabled/") - analogsea::droplet_ssh(droplet, "systemctl reload nginx") +#' @export +#' @rdname digitalocean +do_configure_https <- function(...) { + plumberDeploy_helper("do_configure_https", list(...)) } -install_new_r <- function(droplet){ - analogsea::droplet_ssh(droplet, "apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 51716619E084DAB9") - analogsea::droplet_ssh(droplet, "echo 'deb https://cran.rstudio.com/bin/linux/ubuntu focal-cran40/' >> /etc/apt/sources.list.d/cran.list") - # TODO: use the analogsea version once https://github.com/sckott/analogsea/issues/139 is resolved - #analogsea::debian_apt_get_update(droplet) - analogsea::droplet_ssh(droplet, "sudo apt-get update -qq") - analogsea::droplet_ssh(droplet, 'sudo DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade') - - analogsea::debian_install_r(droplet) +#' @export +#' @rdname digitalocean +do_deploy_api <- function(...) { + plumberDeploy_helper("do_deploy_api", list(...)) } -#' Add HTTPS to a plumber Droplet -#' -#' Adds TLS/SSL (HTTPS) to a droplet created using [do_provision()]. -#' -#' In order to get a TLS/SSL certificate, you need to point a domain name to the -#' IP address associated with your droplet. If you don't already have a domain -#' name, you can register one [here](http://tres.tl/domain). Point a (sub)domain -#' to the IP address associated with your plumber droplet before calling this -#' function. These changes may take a few minutes or hours to propagate around -#' the Internet, but once complete you can then execute this function with the -#' given domain to be granted a TLS/SSL certificate for that domain. -#' @details Obtains a free TLS/SSL certificate from -#' [letsencrypt](https://letsencrypt.org/) and installs it in nginx. It also -#' configures nginx to route all unencrypted HTTP traffic (port 80) to HTTPS. -#' Your TLS certificate will be automatically renewed and deployed. It also -#' opens port 443 in the firewall to allow incoming HTTPS traffic. -#' -#' Historically, HTTPS certificates required payment in advance. If you -#' appreciate this service, consider [donating to the letsencrypt -#' project](https://letsencrypt.org/donate/). -#' @param droplet The droplet on which to act. See [analogsea::droplet()]. -#' @param domain The domain name associated with this instance. Used to obtain a -#' TLS/SSL certificate. -#' @param email Your email address; given only to letsencrypt when requesting a -#' certificate to enable them to contact you about issues with renewal or -#' security. -#' @param termsOfService Set to `TRUE` to agree to the letsencrypt subscriber -#' agreement. At the time of writing, the current version is available [here](https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf). -#' Must be set to true to obtain a certificate through letsencrypt. -#' @param force If `FALSE`, will abort if it believes that the given domain name -#' is not yet pointing at the appropriate IP address for this droplet. If -#' `TRUE`, will ignore this check and attempt to proceed regardless. #' @export -do_configure_https <- function(droplet, domain, email, termsOfService=FALSE, force=FALSE){ - checkAnalogSea() - - # This could be done locally, but I don't have a good way of testing cross-platform currently. - # I can't figure out how to capture the output of the system() call inside - # of droplet_ssh, so just write to and download a file :\ - if (!force){ - nslookup <- tempfile() - - nsout <- droplet_capture(droplet, paste0("nslookup ", domain)) - - ips <- nsout[grepl("^Address: ", nsout)] - ip <- gsub("^Address: (.*)$", "\\1", ips) - - # It turns out that the floating IP is not data that we have about the droplet - # Also, if the floating IP was assigned after we created the droplet object that was - # passed in, then we might not have that information available anyways. - # It turns out that we can use the 'Droplet Metadata' system to query for this info - # from the droplet to get a real-time response. - metadata <- droplet_capture(droplet, "curl http://169.254.169.254/metadata/v1.json") - - parsed <- safeFromJSON(metadata) - floating <- unlist(lapply(parsed$floating_ip, function(ipv){ ipv$ip_address })) - ephemeral <- unlist(parsed$interfaces$public)["ipv4.ip_address"] - - if (ip %in% ephemeral) { - warning("You should consider using a Floating IP address on your droplet for DNS. Currently ", - "you're using the ephemeral IP address of your droplet for DNS which is dangerous; ", - "as soon as you terminate your droplet your DNS records will be pointing to an IP ", - "address you no longer control. A floating IP will give you the opportunity to ", - "create a new droplet and reassign the floating IP used with DNS later.") - } else if (! ip %in% floating) { - print(list(ip=ip, floatingIPs = unname(floating), ephemeralIPs = unname(ephemeral))) - stop("It doesn't appear that the domain name '", domain, "' is pointed to an IP address associated with this droplet. ", - "This could be due to a DNS misconfiguration or because the changes just haven't propagated through the Internet yet. ", - "If you believe this is an error, you can override this check by setting force=TRUE.") - } - message("Confirmed that '", domain, "' references one of the available IP addresses.") - } - - if(missing(domain)){ - stop("You must provide a valid domain name which points to this server in order to get an SSL certificate.") - } - if (missing(email)){ - stop("You must provide an email to letsencrypt -- the provider of your SSL certificate -- for 'urgent renewal and security notices'.") - } - if (!termsOfService){ - stop("You must agree to the letsencrypt terms of service before running this function") - } - - # Trim off any protocol prefix if one exists - domain <- sub("^https?://", "", domain) - # Trim off any trailing slash if one exists. - domain <- sub("/$", "", domain) - - # Prepare the nginx conf file. - conf <- readLines(system.file("server", "nginx-ssl.conf", package="plumber")) - conf <- gsub("\\$DOMAIN\\$", domain, conf) - - conffile <- tempfile() - writeLines(conf, conffile) - - analogsea::droplet_ssh(droplet, "add-apt-repository ppa:certbot/certbot") - analogsea::debian_apt_get_update(droplet) - analogsea::debian_apt_get_install(droplet, "certbot") - analogsea::droplet_ssh(droplet, "ufw allow https") - analogsea::droplet_ssh(droplet, sprintf("certbot certonly --webroot -w /var/certbot/ -n -d %s --email %s --agree-tos --renew-hook '/bin/systemctl reload nginx'", domain, email)) - analogsea::droplet_upload(droplet, conffile, "/etc/nginx/sites-available/plumber") - analogsea::droplet_ssh(droplet, "systemctl reload nginx") - - # TODO: add this as a catch() - file.remove(conffile) - - invisible(droplet) +#' @rdname digitalocean +do_forward <- function(...) { + plumberDeploy_helper("do_forward", list(...)) } -#' Deploy or Update an API -#' -#' Deploys an API from your local machine to make it available on the remote -#' plumber server. -#' @param droplet The droplet on which to act. It's expected that this droplet -#' was provisioned using [do_provision()]. See [analogsea::droplet()] to -#' obtain a reference to a running droplet. -#' @param path The remote path/name of the application -#' @param localPath The local path to the API that you want to deploy. The -#' entire directory referenced will be deployed, and the `plumber.R` file -#' inside of that directory will be used as the root plumber file. The -#' directory MUST contain a `plumber.R` file. -#' @param port The internal port on which this service should run. This will not -#' be user visible, but must be unique and point to a port that is available -#' on your server. If unsure, try a number around `8000`. -#' @param forward If `TRUE`, will setup requests targeting the root URL on the -#' server to point to this application. See the [do_forward()] function for -#' more details. -#' @param swagger If `TRUE`, will enable the Swagger interface for the remotely -#' deployed API. By default, the interface is disabled. -#' @param preflight R commands to run after [plumb()]ing the `plumber.R` file, -#' but before `run()`ing the plumber service. This is an opportunity to e.g. -#' add new filters. If you need to specify multiple commands, they should be -#' semi-colon-delimited. #' @export -do_deploy_api <- function(droplet, path, localPath, port, forward=FALSE, - swagger=FALSE, preflight){ - # Trim off any leading slashes - path <- sub("^/+", "", path) - # Trim off any trailing slashes if any exist. - path <- sub("/+$", "", path) - - if (grepl("/", path)){ - stop("Can't deploy to nested paths. '", path, "' should not have a / in it.") - } - - # TODO: check local path for plumber.R file. - apiPath <- file.path(localPath, "plumber.R") - if (!file.exists(apiPath)){ - stop("Your local API must contain a `plumber.R` file. ", apiPath, " does not exist") - } - - ### UPLOAD the API ### - remoteTmp <- paste0("/tmp/", paste0(sample(LETTERS, 10, replace=TRUE), collapse="")) - dirName <- basename(localPath) - analogsea::droplet_ssh(droplet, paste0("mkdir -p ", remoteTmp)) - analogsea::droplet_upload(droplet, local=localPath, remote=remoteTmp) - analogsea::droplet_ssh(droplet, paste("mv", paste0(remoteTmp, "/", dirName, "/"), paste0("/var/plumber/", path))) - - ### SYSTEMD ### - serviceName <- paste0("plumber-", path) - - service <- readLines(system.file("server", "plumber.service", package="plumber")) - service <- gsub("\\$PORT\\$", port, service) - service <- gsub("\\$PATH\\$", paste0("/", path), service) - - if (missing(preflight)){ - preflight <- "" - } else { - # Append semicolon if necessary - if (!grepl(";\\s*$", preflight)){ - preflight <- paste0(preflight, ";") - } - } - service <- gsub("\\$PREFLIGHT\\$", preflight, service) - - if (missing(swagger)){ - swagger <- "FALSE" - } else { - swagger <- "TRUE" - } - service <- gsub("\\$SWAGGER\\$", swagger, service) - - servicefile <- tempfile() - writeLines(service, servicefile) - - remotePath <- file.path("/etc/systemd/system", paste0(serviceName, ".service")) - - analogsea::droplet_upload(droplet, servicefile, remotePath) - analogsea::droplet_ssh(droplet, "systemctl daemon-reload") - - # TODO: add this as a catch() - file.remove(servicefile) - - # TODO: differentiate between new service (start) and existing service (restart) - analogsea::droplet_ssh(droplet, paste0("systemctl start ", serviceName, " && sleep 1")) #TODO: can systemctl listen for the port to come online so we don't have to guess at a sleep value? - analogsea::droplet_ssh(droplet, paste0("systemctl restart ", serviceName, " && sleep 1")) - analogsea::droplet_ssh(droplet, paste0("systemctl enable ", serviceName)) - analogsea::droplet_ssh(droplet, paste0("systemctl status ", serviceName)) - - ### NGINX ### - # Prepare the nginx conf file - conf <- readLines(system.file("server", "plumber-api.conf", package="plumber")) - conf <- gsub("\\$PORT\\$", port, conf) - conf <- gsub("\\$PATH\\$", path, conf) - - conffile <- tempfile() - writeLines(conf, conffile) - - remotePath <- file.path("/etc/nginx/sites-available/plumber-apis", paste0(path, ".conf")) - - analogsea::droplet_upload(droplet, conffile, remotePath) - - # TODO: add this as a catch() - file.remove(conffile) - - if (forward){ - do_forward(droplet, path) - } - - analogsea::droplet_ssh(droplet, "systemctl reload nginx") +#' @rdname digitalocean +do_remove_api <- function(...) { + plumberDeploy_helper("do_remove_api", list(...)) } -#' Forward Root Requests to an API -#' -#' @param droplet The droplet on which to act. It's expected that this droplet -#' was provisioned using [do_provision()]. -#' @param path The path to which root requests should be forwarded #' @export -do_forward <- function(droplet, path){ - # Trim off any leading slashes - path <- sub("^/+", "", path) - # Trim off any trailing slashes if any exist. - path <- sub("/+$", "", path) - - if (grepl("/", path)){ - stop("Can't deploy to nested paths. '", path, "' should not have a / in it.") - } - - forward <- readLines(system.file("server", "forward.conf", package="plumber")) - forward <- gsub("\\$PATH\\$", paste0(path), forward) - - forwardfile <- tempfile() - writeLines(forward, forwardfile) - - analogsea::droplet_upload(droplet, forwardfile, "/etc/nginx/sites-available/plumber-apis/_forward.conf") - - # TODO: add this as a catch() - file.remove(forwardfile) - - invisible(droplet) +#' @rdname digitalocean +do_remove_forward <- function(...) { + plumberDeploy_helper("do_remove_forward", list(...)) } -#' Remove an API from the server -#' -#' Removes all services and routing rules associated with a particular service. -#' Optionally purges the associated API directory from disk. -#' @param droplet The droplet on which to act. It's expected that this droplet -#' was provisioned using [do_provision()]. See [analogsea::droplet()] to -#' obtain a reference to a running droplet. -#' @param path The path/name of the plumber service -#' @param delete If `TRUE`, will also delete the associated directory -#' (`/var/plumber/whatever`) from the server. -#' @export -do_remove_api <- function(droplet, path, delete=FALSE){ - # Trim off any leading slashes - path <- sub("^/+", "", path) - # Trim off any trailing slashes if any exist. - path <- sub("/+$", "", path) - if (grepl("/", path)){ - stop("Can't deploy to nested paths. '", path, "' should not have a / in it.") - } +plumberDeploy_helper <- function(fn_name, args, new_fn_name = fn_name) { + cur_fn <- paste0(fn_name, "()") + new_fn <- paste0("plumberDeploy", "::", new_fn_name, "()") - # Given that we're about to `rm -rf`, let's just be safe... - if (grepl("\\.\\.", path)){ - stop("Paths don't allow '..'s.") + # check if plumberDeploy is called + if (!plumberDeploy_is_available()) { + # not found + # throw error + lifecycle::deprecate_stop("1.0.0", cur_fn, new_fn) } - if (nchar(path)==0){ - stop("Path cannot be empty.") - } - - serviceName <- paste0("plumber-", path) - analogsea::droplet_ssh(droplet, paste0("systemctl stop ", serviceName)) - analogsea::droplet_ssh(droplet, paste0("systemctl disable ", serviceName)) - analogsea::droplet_ssh(droplet, paste0("rm /etc/systemd/system/", serviceName, ".service")) - analogsea::droplet_ssh(droplet, paste0("rm /etc/nginx/sites-available/plumber-apis/", path, ".conf")) - analogsea::droplet_ssh(droplet, "systemctl reload nginx") + # plumberDeploy is found + # 1. Throw warning + # 2. Call method + lifecycle::deprecate_warn("1.0.0", cur_fn, new_fn) - if(delete){ - analogsea::droplet_ssh(droplet, paste0("rm -rf /var/plumber/", path)) - } + fn <- utils::getFromNamespace(fn_name, "plumberDeploy") + do.call(fn, args) } -#' Remove the forwarding rule -#' -#' Removes the forwarding rule from the root path on the server. The server will -#' no longer forward requests for `/` to an application. -#' @param droplet The droplet on which to act. It's expected that this droplet -#' was provisioned using [do_provision()]. See [analogsea::droplet()] to obtain a reference to a running droplet. -#' @export -do_remove_forward <- function(droplet){ - analogsea::droplet_ssh(droplet, "rm /etc/nginx/sites-available/plumber-apis/_forward.conf") - analogsea::droplet_ssh(droplet, "systemctl reload nginx") +plumberDeploy_is_available <- function() { + is_available("plumberDeploy") } - -# nocov end diff --git a/R/utils.R b/R/utils.R new file mode 100644 index 000000000..0fd227bf1 --- /dev/null +++ b/R/utils.R @@ -0,0 +1,8 @@ + +is_available <- function (package, version = NULL) { + installed <- nzchar(system.file(package = package)) + if (is.null(version)) { + return(installed) + } + installed && isTRUE(utils::packageVersion(package) >= version) +} diff --git a/README.md b/README.md index 7e4ca54b4..9ecb447f6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# plumber +# `plumber` [![R build status](https://github.com/rstudio/plumber/workflows/R-CMD-check/badge.svg)](https://github.com/rstudio/plumber/actions) @@ -38,7 +38,7 @@ function(a, b) { } ``` -These comments allow plumber to make your R functions available as API +These comments allow `plumber` to make your R functions available as API endpoints. You can use either `#*` as the prefix or `#'`, but we recommend the former since `#'` will collide with Roxygen. @@ -97,10 +97,10 @@ library(plumber) ## Hosting If you're just getting started with hosting cloud servers, the -[DigitalOcean](https://www.digitalocean.com) integration included in plumber +[DigitalOcean](https://www.digitalocean.com) integration included in `plumber` will be the best way to get started. You'll be able to get a server hosting your -custom API in just two R commands. Full documentation is available at -https://www.rplumber.io/articles/hosting.html#digitalocean-1. +custom API in just two R commands. To deploy to DigitalOcean, check out the +`plumber` companion package [`plumberDeploy`](https://github.com/meztez/plumberDeploy). [RStudio Connect](https://www.rstudio.com/products/connect/) is a commercial publishing platform that enables R developers to easily publish a variety of R @@ -111,7 +111,6 @@ A couple of other approaches to hosting plumber are also made available: - PM2 - https://www.rplumber.io/articles/hosting.html#pm2-1 - Docker - https://www.rplumber.io/articles/hosting.html#docker-basic- - ## Related Projects - [OpenCPU](https://www.opencpu.org/) - A server designed for hosting R APIs diff --git a/inst/.gitignore b/inst/.gitignore deleted file mode 100644 index 863f703ef..000000000 --- a/inst/.gitignore +++ /dev/null @@ -1 +0,0 @@ -analog-keys.R diff --git a/inst/hosted-new.R b/inst/hosted-new.R deleted file mode 100644 index 7cb13a36c..000000000 --- a/inst/hosted-new.R +++ /dev/null @@ -1,27 +0,0 @@ -library(analogsea) -library(plumber) - -install_package_secure <- function(droplet, pkg){ - analogsea::install_r_package(droplet, pkg, repo="https://cran.rstudio.com") -} - -drop <- plumber::do_provision(unstable=TRUE, example=FALSE, name="hostedplumber") - -do_deploy_api(drop, "append", "./inst/plumber/01-append/", 8001) -do_deploy_api(drop, "filters", "./inst/plumber/02-filters/", 8002) - -# GitHub -install_package_secure(drop, "digest") -# remotes is the other dependency, but by unstable=TRUE on do_provision we already have that -do_deploy_api(drop, "github", "./inst/plumber/03-github/", 8003) - -# Sessions -do_deploy_api(drop, "sessions", "./inst/plumber/06-sessions/", 8006, - preflight="pr$registerHooks(plumber::sessionCookie('secret', 'cookieName', path='/'));") - -# Mailgun -install_package_secure(drop, "htmltools") -do_deploy_api(drop, "mailgun", "./inst/plumber/07-mailgun/", 8007) - -# MANUAL: configure DNS, then -# do_configure_https(drop, "plumber.tres.tl"... ) diff --git a/inst/hosted/analogsea-provision.R b/inst/hosted/analogsea-provision.R deleted file mode 100644 index b5fbdea85..000000000 --- a/inst/hosted/analogsea-provision.R +++ /dev/null @@ -1,49 +0,0 @@ -library(analogsea) - -source("analog-keys.R") - -# pl <- droplet_create(name="plumber", ssh_keys="trestle-secure-rsa") %>% droplet_wait() -# pl <- droplet(13426136) - -install <- function(droplet){ - droplet %>% - debian_add_swap() %>% - install_new_r() %>% - install_docker() %>% - prepare_plumber() -} - -install_docker <- function(droplet){ - droplet %>% - # Deprecated (Shutting down dockerproject.org APT and YUM repos 2020-03-31) - droplet_ssh(c("sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D", - "echo 'deb https://apt.dockerproject.org/repo ubuntu-focal main' > /etc/apt/sources.list.d/docker.list")) %>% - debian_apt_get_update() %>% - droplet_ssh("sudo apt-get install linux-image-extra-$(uname -r)") %>% - debian_apt_get_install("docker-engine") %>% - droplet_ssh(c("curl -L https://github.com/docker/compose/releases/download/1.7.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose", - "chmod +x /usr/local/bin/docker-compose")) -} - -install_new_r <- function(droplet){ - droplet %>% - droplet_ssh(c("echo 'deb https://cran.rstudio.com/bin/linux/ubuntu focal-cran40/' >> /etc/apt/sources.list", - "apt-key adv --keyserver keyserver.ubuntu.com --recv-keys E084DAB9")) %>% - debian_apt_get_update() %>% - debian_install_r() -} - -prepare_plumber<- function(droplet){ - droplet %>% - droplet_ssh("git clone https://github.com/rstudio/plumber.git") %>% - droplet_ssh("cd plumber/inst/hosted/ && docker-compose up -d --build") -} - -# Update instructions for adding new images: -# - Update the docker-compose config file to include the new service. Test locally -# - Commit -# docker pull trestle/plumber #AFTER build is complete. -# git pull to get updates to docker-compose config -# docker-compose build NEW_IMAGE -# docker-compose up --no-deps -d NEW_IMAGE -# - https://docs.docker.com/compose/production/ diff --git a/inst/hosted/docker-compose.yml b/inst/hosted/docker-compose.yml deleted file mode 100644 index 5c84169a4..000000000 --- a/inst/hosted/docker-compose.yml +++ /dev/null @@ -1,42 +0,0 @@ -version: '2' -services: - append: - image: rstudio/plumber - command: '/examples/01-append/appender.R' - volumes: - - ../examples:/examples - restart: always - filters: - image: rstudio/plumber - command: '/examples/02-filters/filters.R' - volumes: - - ../examples:/examples - restart: always - github: - build: ../examples/03-github/ - volumes: - - ../examples:/examples - restart: always - sessions: - build: ../examples/06-sessions/ - volumes: - - ../examples:/examples - restart: always - mailgun: - build: ../examples/07-mailgun/ - volumes: - - ../examples:/examples - restart: always - nginx: - image: nginx:1.9 - ports: - - "80:80" - volumes: - - ./nginx.conf:/etc/nginx/nginx.conf:ro - restart: always - depends_on: - - append - - sessions - - mailgun - - filters - - github diff --git a/inst/hosted/nginx.conf b/inst/hosted/nginx.conf deleted file mode 100644 index 4cd3bba57..000000000 --- a/inst/hosted/nginx.conf +++ /dev/null @@ -1,57 +0,0 @@ -events { - worker_connections 4096; ## Default: 1024 -} - -http { - default_type application/octet-stream; - sendfile on; - tcp_nopush on; - server_names_hash_bucket_size 128; # this seems to be required for some vhosts - - server { - listen 80 default_server; - listen [::]:80 default_server ipv6only=on; - - root /usr/share/nginx/html; - index index.html index.htm; - - server_name rapier.tres.tl; - server_name plumber.tres.tl; - - location /append/ { - proxy_pass http://append:8000/; - proxy_set_header Host $host; - } - - location /filters/ { - proxy_pass http://filters:8000/; - proxy_set_header Host $host; - } - - location /github/ { - proxy_pass http://github:8000/; - proxy_set_header Host $host; - } - - location /sessions/ { - proxy_pass http://sessions:8000/; - proxy_set_header Host $host; - } - - location /mailgun/ { - proxy_pass http://mailgun:8000/; - proxy_set_header Host $host; - } - - #location /balanced/ { - # proxy_pass http://lb/; - # proxy_set_header Host $host; - #} - - - location ~ /\.ht { - deny all; - } - } -} - diff --git a/inst/server/forward.conf b/inst/server/forward.conf deleted file mode 100644 index e80dfff1b..000000000 --- a/inst/server/forward.conf +++ /dev/null @@ -1,3 +0,0 @@ -location = / { - return 307 /$PATH$; -} diff --git a/inst/server/nginx-ssl.conf b/inst/server/nginx-ssl.conf deleted file mode 100644 index 3e28d325b..000000000 --- a/inst/server/nginx-ssl.conf +++ /dev/null @@ -1,21 +0,0 @@ -server { - listen 80 default_server; - listen [::]:80 default_server; - server_name $DOMAIN$; - return 301 https://$server_name$request_uri; -} - -server { - listen 443 ssl; - listen [::]:443 ssl; - server_name $DOMAIN$; - - ssl_certificate /etc/letsencrypt/live/$DOMAIN$/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/$DOMAIN$/privkey.pem; - - include /etc/nginx/sites-available/plumber-apis/*; - - location /.well-known/ { - root /var/certbot/; - } -} diff --git a/inst/server/nginx.conf b/inst/server/nginx.conf deleted file mode 100644 index 3622cd2f0..000000000 --- a/inst/server/nginx.conf +++ /dev/null @@ -1,14 +0,0 @@ -# Plumber server configuration - -server { - listen 80 default_server; - listen [::]:80 default_server; - - server_name _; - - include /etc/nginx/sites-available/plumber-apis/*; - - location /.well-known/ { - root /var/certbot/; - } -} diff --git a/inst/server/plumber-api.conf b/inst/server/plumber-api.conf deleted file mode 100644 index bf19c392c..000000000 --- a/inst/server/plumber-api.conf +++ /dev/null @@ -1,4 +0,0 @@ -location /$PATH$/ { - proxy_pass http://localhost:$PORT$/; - proxy_set_header Host $host; -} diff --git a/inst/server/plumber.service b/inst/server/plumber.service deleted file mode 100644 index 23b08dee0..000000000 --- a/inst/server/plumber.service +++ /dev/null @@ -1,10 +0,0 @@ -[Unit] -Description=Plumber API - -[Service] -ExecStart=/usr/bin/Rscript -e "pr <- plumber::plumb('/var/plumber$PATH$/plumber.R'); pr$set_ui($SWAGGER$); $PREFLIGHT$ pr$run(port=$PORT$)" -Restart=on-abnormal -WorkingDirectory=/var/plumber/$PATH$/ - -[Install] -WantedBy=multi-user.target diff --git a/man/hookable.Rd b/man/Hookable.Rd similarity index 100% rename from man/hookable.Rd rename to man/Hookable.Rd diff --git a/man/plumber.Rd b/man/Plumber.Rd similarity index 100% rename from man/plumber.Rd rename to man/Plumber.Rd diff --git a/man/digitalocean.Rd b/man/digitalocean.Rd new file mode 100644 index 000000000..f1501fe39 --- /dev/null +++ b/man/digitalocean.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/digital-ocean.R +\name{do_provision} +\alias{do_provision} +\alias{do_configure_https} +\alias{do_deploy_api} +\alias{do_forward} +\alias{do_remove_api} +\alias{do_remove_forward} +\title{DigitalOcean Plumber server} +\usage{ +do_provision(...) + +do_configure_https(...) + +do_deploy_api(...) + +do_forward(...) + +do_remove_api(...) + +do_remove_forward(...) +} +\description{ +These methods are now defunct. +Please use the \href{https://github.com/meztez/plumberDeploy}{\code{plumberDeploy}} R package. +} +\keyword{internal} diff --git a/man/do_configure_https.Rd b/man/do_configure_https.Rd deleted file mode 100644 index df328f9a3..000000000 --- a/man/do_configure_https.Rd +++ /dev/null @@ -1,53 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/digital-ocean.R -\name{do_configure_https} -\alias{do_configure_https} -\title{Add HTTPS to a plumber Droplet} -\usage{ -do_configure_https( - droplet, - domain, - email, - termsOfService = FALSE, - force = FALSE -) -} -\arguments{ -\item{droplet}{The droplet on which to act. See \code{\link[analogsea:droplet]{analogsea::droplet()}}.} - -\item{domain}{The domain name associated with this instance. Used to obtain a -TLS/SSL certificate.} - -\item{email}{Your email address; given only to letsencrypt when requesting a -certificate to enable them to contact you about issues with renewal or -security.} - -\item{termsOfService}{Set to \code{TRUE} to agree to the letsencrypt subscriber -agreement. At the time of writing, the current version is available \href{https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf}{here}. -Must be set to true to obtain a certificate through letsencrypt.} - -\item{force}{If \code{FALSE}, will abort if it believes that the given domain name -is not yet pointing at the appropriate IP address for this droplet. If -\code{TRUE}, will ignore this check and attempt to proceed regardless.} -} -\description{ -Adds TLS/SSL (HTTPS) to a droplet created using \code{\link[=do_provision]{do_provision()}}. -} -\details{ -In order to get a TLS/SSL certificate, you need to point a domain name to the -IP address associated with your droplet. If you don't already have a domain -name, you can register one \href{http://tres.tl/domain}{here}. Point a (sub)domain -to the IP address associated with your plumber droplet before calling this -function. These changes may take a few minutes or hours to propagate around -the Internet, but once complete you can then execute this function with the -given domain to be granted a TLS/SSL certificate for that domain. - -Obtains a free TLS/SSL certificate from -\href{https://letsencrypt.org/}{letsencrypt} and installs it in nginx. It also -configures nginx to route all unencrypted HTTP traffic (port 80) to HTTPS. -Your TLS certificate will be automatically renewed and deployed. It also -opens port 443 in the firewall to allow incoming HTTPS traffic. - -Historically, HTTPS certificates required payment in advance. If you -appreciate this service, consider \href{https://letsencrypt.org/donate/}{donating to the letsencrypt project}. -} diff --git a/man/do_deploy_api.Rd b/man/do_deploy_api.Rd deleted file mode 100644 index 4b3d1fcb7..000000000 --- a/man/do_deploy_api.Rd +++ /dev/null @@ -1,48 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/digital-ocean.R -\name{do_deploy_api} -\alias{do_deploy_api} -\title{Deploy or Update an API} -\usage{ -do_deploy_api( - droplet, - path, - localPath, - port, - forward = FALSE, - swagger = FALSE, - preflight -) -} -\arguments{ -\item{droplet}{The droplet on which to act. It's expected that this droplet -was provisioned using \code{\link[=do_provision]{do_provision()}}. See \code{\link[analogsea:droplet]{analogsea::droplet()}} to -obtain a reference to a running droplet.} - -\item{path}{The remote path/name of the application} - -\item{localPath}{The local path to the API that you want to deploy. The -entire directory referenced will be deployed, and the \code{plumber.R} file -inside of that directory will be used as the root plumber file. The -directory MUST contain a \code{plumber.R} file.} - -\item{port}{The internal port on which this service should run. This will not -be user visible, but must be unique and point to a port that is available -on your server. If unsure, try a number around \code{8000}.} - -\item{forward}{If \code{TRUE}, will setup requests targeting the root URL on the -server to point to this application. See the \code{\link[=do_forward]{do_forward()}} function for -more details.} - -\item{swagger}{If \code{TRUE}, will enable the Swagger interface for the remotely -deployed API. By default, the interface is disabled.} - -\item{preflight}{R commands to run after \code{\link[=plumb]{plumb()}}ing the \code{plumber.R} file, -but before \code{run()}ing the plumber service. This is an opportunity to e.g. -add new filters. If you need to specify multiple commands, they should be -semi-colon-delimited.} -} -\description{ -Deploys an API from your local machine to make it available on the remote -plumber server. -} diff --git a/man/do_forward.Rd b/man/do_forward.Rd deleted file mode 100644 index 069103c90..000000000 --- a/man/do_forward.Rd +++ /dev/null @@ -1,17 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/digital-ocean.R -\name{do_forward} -\alias{do_forward} -\title{Forward Root Requests to an API} -\usage{ -do_forward(droplet, path) -} -\arguments{ -\item{droplet}{The droplet on which to act. It's expected that this droplet -was provisioned using \code{\link[=do_provision]{do_provision()}}.} - -\item{path}{The path to which root requests should be forwarded} -} -\description{ -Forward Root Requests to an API -} diff --git a/man/do_provision.Rd b/man/do_provision.Rd deleted file mode 100644 index 638bc3ae5..000000000 --- a/man/do_provision.Rd +++ /dev/null @@ -1,41 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/digital-ocean.R -\name{do_provision} -\alias{do_provision} -\title{Provision a DigitalOcean plumber server} -\usage{ -do_provision(droplet, unstable = FALSE, example = TRUE, ...) -} -\arguments{ -\item{droplet}{The DigitalOcean droplet that you want to provision (see \code{\link[analogsea:droplet]{analogsea::droplet()}}). If empty, a new DigitalOcean server will be created.} - -\item{unstable}{If \code{FALSE}, will install plumber from CRAN. If \code{TRUE}, will install the unstable version of plumber from GitHub.} - -\item{example}{If \code{TRUE}, will deploy an example API named \code{hello} to the server on port 8000.} - -\item{...}{Arguments passed into the \code{\link[analogsea:droplet_create]{analogsea::droplet_create()}} function.} -} -\description{ -Create (if required), install the necessary prerequisites, and -deploy a sample plumber application on a DigitalOcean virtual machine. -You may sign up for a Digital Ocean account \href{https://m.do.co/c/add0b50f54c4}{here}. -This command is idempotent, so feel free to run it on a single server multiple times. -} -\details{ -Provisions a Ubuntu 20.04-x64 droplet with the following customizations: -\itemize{ -\item A recent version of R installed -\item plumber installed globally in the system library -\item An example plumber API deployed at \verb{/var/plumber} -\item A systemd definition for the above plumber API which will ensure that the plumber -API is started on machine boot and respawned if the R process ever crashes. On the -server you can use commands like \verb{systemctl restart plumber} to manage your API, or -\verb{journalctl -u plumber} to see the logs associated with your plumber process. -\item The `nginx`` web server installed to route web traffic from port 80 (HTTP) to your plumber -process. -\item \code{ufw} installed as a firewall to restrict access on the server. By default it only -allows incoming traffic on port 22 (SSH) and port 80 (HTTP). -\item A 4GB swap file is created to ensure that machines with little RAM (the default) are -able to get through the necessary R package compilations. -} -} diff --git a/man/do_remove_api.Rd b/man/do_remove_api.Rd deleted file mode 100644 index 48876ae1a..000000000 --- a/man/do_remove_api.Rd +++ /dev/null @@ -1,22 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/digital-ocean.R -\name{do_remove_api} -\alias{do_remove_api} -\title{Remove an API from the server} -\usage{ -do_remove_api(droplet, path, delete = FALSE) -} -\arguments{ -\item{droplet}{The droplet on which to act. It's expected that this droplet -was provisioned using \code{\link[=do_provision]{do_provision()}}. See \code{\link[analogsea:droplet]{analogsea::droplet()}} to -obtain a reference to a running droplet.} - -\item{path}{The path/name of the plumber service} - -\item{delete}{If \code{TRUE}, will also delete the associated directory -(\verb{/var/plumber/whatever}) from the server.} -} -\description{ -Removes all services and routing rules associated with a particular service. -Optionally purges the associated API directory from disk. -} diff --git a/man/do_remove_forward.Rd b/man/do_remove_forward.Rd deleted file mode 100644 index f658632fd..000000000 --- a/man/do_remove_forward.Rd +++ /dev/null @@ -1,16 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/digital-ocean.R -\name{do_remove_forward} -\alias{do_remove_forward} -\title{Remove the forwarding rule} -\usage{ -do_remove_forward(droplet) -} -\arguments{ -\item{droplet}{The droplet on which to act. It's expected that this droplet -was provisioned using \code{\link[=do_provision]{do_provision()}}. See \code{\link[analogsea:droplet]{analogsea::droplet()}} to obtain a reference to a running droplet.} -} -\description{ -Removes the forwarding rule from the root path on the server. The server will -no longer forward requests for \code{/} to an application. -} diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index 5913983fe..39503aa43 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -112,13 +112,3 @@ reference: - 'PlumberStatic' - 'PlumberStep' - 'Hookable' - -- title: Digital Ocean - desc: TO BE REMOVED FROM PLUMBER!!! - contents: - - 'do_configure_https' - - 'do_deploy_api' - - 'do_forward' - - 'do_provision' - - 'do_remove_api' - - 'do_remove_forward' diff --git a/tests/testthat/test-deprecated.R b/tests/testthat/test-deprecated.R index 73c7472cf..55b01a838 100644 --- a/tests/testthat/test-deprecated.R +++ b/tests/testthat/test-deprecated.R @@ -31,6 +31,7 @@ test_that("addAssets continues to work", { expect_true(inherits(val, "PlumberResponse")) }) + test_that("getCharacterSet continues to work", { expect_equal( lifecycle::expect_deprecated(getCharacterSet(contentType = "foo")), @@ -53,3 +54,18 @@ test_that("hookable throws deprecated warning", { test_that("plumber throws deprecated warning", { expect_warning(plumber$new(), "Plumber") }) + + +test_that("Digital Ocean functions throw errors", { + skip_on_cran() + + # Do not test if plumberDeploy is installed, as real functions will executed + skip_if(plumberDeploy_is_available()) + + expect_error(do_provision(), class = "lifecycle_error_deprecated") + expect_error(do_configure_https(), class = "lifecycle_error_deprecated") + expect_error(do_deploy_api(), class = "lifecycle_error_deprecated") + expect_error(do_forward(), class = "lifecycle_error_deprecated") + expect_error(do_remove_api(), class = "lifecycle_error_deprecated") + expect_error(do_remove_forward(), class = "lifecycle_error_deprecated") +}) diff --git a/vignettes/hosting.Rmd b/vignettes/hosting.Rmd index 596d250cd..7f6ef13f1 100644 --- a/vignettes/hosting.Rmd +++ b/vignettes/hosting.Rmd @@ -28,18 +28,8 @@ For these reasons and more, you should consider setting up a separate server on [DigitalOcean](https://m.do.co/c/add0b50f54c4) is an easy-to-use Cloud Computing provider. They offer a simple way to spin up a Linux virtual machine and access it remotely. You can choose what size machine you want to run -- with options ranging from small machines with 512MB of RAM for a few dollars a month up to large machines with dozens of GB of RAM -- and only pay for it while it's online. -Plumber includes helper functions that enable you to automatically provision a Plumber server and deploy your APIs to it. So in order to setup a Plumber server running on DigitalOcean, you'll follow these steps: +To deploy your Plumber API to DigitalOcean, please check out the `plumber` companion package [`plumberDeploy`](https://github.com/meztez/plumberDeploy). -1. [Create a DigitalOcean account](https://m.do.co/c/add0b50f54c4). -1. Setup an SSH key and deploy the public portion to DigitalOcean so you'll be able to login to your server. -1. Install the `analogsea` R package and run a test command like `analogsea::droplets()` to confirm that it's able to connect to your DigitalOcean account. -1. Run `mydrop <- plumber::do_provision()`. This will start a virtual machine (or "droplet", as DigitalOcean calls them) and install Plumber and all the necessary prerequisite software. Once the provisioning is complete, you should be able to access port `8000` on your server's IP and see a response from Plumber. -1. Install any R packages on the server that your API requires using `analogsea::install_r_package()`. -1. You can use `plumber::do_deploy_api()` to deploy or update your own custom APIs to a particular port on your server. -1. (Optional) [Setup a domain name](http://tres.tl/dns) for your Plumber server so you can use www.myplumberserver.com instead of the server's IP address. -1. (Optional) Configure SSL - -Getting everything connected the first time can be a bit of work, but once you have `analogsea` connected to your DigitalOcean account, you're now able to spin up new Plumber servers in DigitalOcean hosting your APIs with just a couple of R commands. You can even write [scripts that provision an entire Plumber server](https://github.com/rstudio/plumber/blob/d9f4acc163168ae179c55e781cfa43f03551f825/inst/hosted-new.R) with multiple APIs associated. ## RStudio Connect {#rstudio-connect} @@ -47,7 +37,7 @@ Getting everything connected the first time can be a bit of work, but once you h RStudio Connect automatically manages the number of R processes necessary to handle the current load and balances incoming traffic across all available processes. It can also shut down idle processes when they're not in use. This allows you to run the appropriate number of R processes to scale your capacity to accommodate the current load. -> Conflict of interest: the primary author of plumber and this book works for RStudio on RStudio Connect. +> Conflict of interest: the primary authors of `plumber` work for RStudio. ## Docker (Basic) {#docker} @@ -402,7 +392,7 @@ If you want a big-picture view of the health of your server and all the pm2 serv If you use a Linux server you can use `systemd` to run Plumber as a service that can be accessed from your local network or even outside your network depending on your firewall rules. This option is similar to using the [Docker method](#docker). One of the main advantages of using `systemd` over using Docker is that `systemd` won't bypass firewall rules (Docker does!) and avoids the overhead of running a container. -Compared to `plumber::do_provision()` this option won't create a new droplet if you use [Digital Ocean](#digitalocean); it will run on your existing droplet instead. +Compared to `plumber::do_provision()` this option won't create a new droplet if you use [DigitalOcean](#digitalocean); it will run on your existing droplet instead. To implement this option you'll complete the following three steps from the terminal: