0Pricing
R Academy · Lesson

Deploying Plumber APIs to Production

Containerize and deploy Plumber APIs with Docker and cloud platforms.

Deploying Plumber APIs to Production is a free R Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the R Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Production Deployment Options

A Plumber API can be deployed in several ways:

  • Docker container — portable, reproducible, works anywhere
  • Posit Connect — push-button deploy with scheduling
  • Digital Ocean / EC2 — a plain Linux VM running R

Docker is the most portable option and the industry standard for production R APIs.

Plumber API Entry Point

Create a top-level api.R that starts the Plumber server. Use Sys.getenv('PORT', unset='8000') so the port can be set by the container orchestrator without changing code.

# api.R
# library(plumber)
#
# pr <- plumb('routes.R')
# port <- as.integer(Sys.getenv('PORT', unset = '8000'))
# pr$run(host = '0.0.0.0', port = port)
#
# Listening on 0.0.0.0 is required inside Docker
# (127.0.0.1 is only reachable inside the container)

Base Docker Image — rocker/r-ver

The rocker/r-ver image provides a minimal, version-pinned R environment. Always pin to a specific R version (e.g., rocker/r-ver:4.3.2) to ensure reproducible builds. Avoid :latest in production.

# Dockerfile
# FROM rocker/r-ver:4.3.2
#
# Alternatively use rocker/plumber which pre-installs plumber:
# FROM rstudio/plumber:latest
#
# rocker/r-ver is more minimal and gives you full control
# over which packages are installed.

Installing R Packages in the Dockerfile

Use RUN Rscript -e "install.packages(...)" to install packages during the image build. Install system dependencies first (e.g., libssl-dev for httr2) using apt-get.

# FROM rocker/r-ver:4.3.2
#
# RUN apt-get update && apt-get install -y \
#     libssl-dev \
#     libcurl4-openssl-dev \
#  && rm -rf /var/lib/apt/lists/*
#
# RUN Rscript -e "install.packages(c('plumber', 'jsonlite', 'httr2'), repos='https://cloud.r-project.org')"

Copying Files and Setting the Port

Copy your R source files into the container with COPY. Declare the port with EXPOSE so Docker knows which port the container listens on. This is documentation only — it does not publish the port.

# FROM rocker/r-ver:4.3.2
# ...
# WORKDIR /app
# COPY routes.R .
# COPY api.R .
#
# EXPOSE 8000
#
# CMD ["Rscript", "api.R"]

Building and Running the Docker Image

Build the image with docker build, then run a container mapping the host port to the container port. The -e flag injects environment variables for secrets.

# Build the image:
# docker build -t my-r-api:1.0 .
#
# Run a container:
# docker run -d \
#   -p 8000:8000 \
#   -e API_SECRET_KEY='my_secret' \
#   -e DATABASE_URL='postgres://...' \
#   --name r-api \
#   my-r-api:1.0
#
# Test:
# curl http://localhost:8000/ping

Reading Secrets with Sys.getenv()

In production, never put secrets in the Dockerfile or source code. Read them at runtime with Sys.getenv(). Pass them through Docker -e flags, Kubernetes secrets, or environment-variable management systems like AWS Secrets Manager.

# In routes.R:
# db_url   <- Sys.getenv('DATABASE_URL', unset = '')
# api_key  <- Sys.getenv('API_SECRET_KEY', unset = '')
#
# if (nchar(db_url) == 0)  stop('DATABASE_URL is required')
# if (nchar(api_key) == 0) stop('API_SECRET_KEY is required')
#
# Fail fast at startup rather than failing silently at request time
cat('Validate all required env vars at startup with stop()
')

Health Check Endpoint

A /ping or /health endpoint lets load balancers and orchestrators (Kubernetes, ECS) confirm the API is alive. It should return 200 quickly with no authentication, and optionally check DB connectivity.

# #* Health check
# #* @preempt auth
# #* @get /ping
# function() {
#   list(
#     status  = 'ok',
#     version = '1.0.0',
#     time    = format(Sys.time(), '%Y-%m-%dT%H:%M:%SZ')
#   )
# }
#
# Docker HEALTHCHECK:
# HEALTHCHECK CMD curl -f http://localhost:8000/ping || exit 1

Complete Dockerfile Example

Putting it all together — a production-ready Dockerfile for a Plumber API:

# FROM rocker/r-ver:4.3.2
# RUN apt-get update && apt-get install -y libssl-dev libcurl4-openssl-dev \
#  && rm -rf /var/lib/apt/lists/*
# RUN Rscript -e "install.packages(c('plumber','jsonlite'), repos='https://cloud.r-project.org')"
# WORKDIR /app
# COPY routes.R api.R ./
# EXPOSE 8000
# HEALTHCHECK CMD curl -f http://localhost:8000/ping || exit 1
# CMD ["Rscript", "api.R"]

Logging in Production

Structured logging helps diagnose issues in production. Use cat() or the logger package to write timestamped logs to stdout — Docker and most platforms capture stdout automatically and route it to a log aggregator.

# Log format: ISO timestamp + level + message
log_info <- function(msg) {
  cat(format(Sys.time(), '%Y-%m-%dT%H:%M:%S'), '[INFO]', msg, '
')
}

log_info('API starting up')
log_info(paste('Port:', Sys.getenv('PORT', '8000')))

Reverse Proxy with Nginx

In production, put an Nginx reverse proxy in front of Plumber to handle TLS termination, rate limiting, and request buffering. Nginx passes requests to Plumber on localhost, while the outside world connects to Nginx on port 443.

# Nginx config snippet (nginx.conf):
# server {
#   listen 443 ssl;
#   ssl_certificate     /etc/letsencrypt/.../fullchain.pem;
#   ssl_certificate_key /etc/letsencrypt/.../privkey.pem;
#
#   location /api/ {
#     proxy_pass         http://127.0.0.1:8000/;
#     proxy_set_header   Host $host;
#     proxy_set_header   X-Real-IP $remote_addr;
#   }
# }

Quick Check: Docker EXPOSE

What does the EXPOSE 8000 instruction in a Dockerfile actually do?

Deploying Plumber APIs Recap

Key steps for production Plumber deployment:

  • Listen on 0.0.0.0 and read PORT from environment
  • Use rocker/r-ver:X.Y.Z (version-pinned) as the base image
  • Install packages in the Dockerfile; copy only source files
  • Inject secrets via -e env vars — never in source or Dockerfile
  • Add a /ping health endpoint (with #* @preempt auth)
  • Use Nginx as a reverse proxy for TLS and rate limiting

Frequently asked questions

Is the “Deploying Plumber APIs to Production” lesson free?

Yes — the full text of “Deploying Plumber APIs to Production” is free to read here on the web, and the R Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the R Academy course, upgrade to CoddyKit PRO.

What will I learn in “Deploying Plumber APIs to Production”?

Containerize and deploy Plumber APIs with Docker and cloud platforms. You practise R Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start R Academy?

No prior experience is required. R Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Deploying Plumber APIs to Production” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this R Academy lesson?

Yes. Every R Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Introduction to Plumber and REST
  2. Creating GET and POST Endpoints
  3. Authentication and API Security
  4. Deploying Plumber APIs to Production
← Back to R Academy