This vignette describes the
one-time infrastructure steps required to deploy
roreviewapi on a Digital Ocean droplet, including the
editor volunteer search feature introduced in v0.2.
The following environment variables must be set in the
Dockerfile before building the image. Each appears twice:
once as an ENV declaration at the top of the file, and once
in the ~/.Renviron block that makes the value available to
R at runtime.
| Variable | Description |
|---|---|
GITHUB_PAT |
GitHub personal access token |
POSTMARK_API_TOKEN |
Postmark server API token (from the Postmark dashboard) |
POSTMARK_FROM |
Verified sender address registered with Postmark |
AIRTABLE_API_KEY |
AirTable personal access token |
AIRTABLE_BASE_ID |
ID of the AirTable base containing the EiC rotation table |
ROREVIEWAPI_BASE_URL |
Public HTTPS base URL of the deployed API
(https://reviewbot.ropensci.org) |
PKGCHECK_TOKEN |
pkgcheck authentication token (set directly in
~/.Renviron) |
Replace each <placeholder> in
Dockerfile with the real value before building.
From:
address on all outgoing emails and must be set as
POSTMARK_FROM.POSTMARK_API_TOKEN
in the Dockerfile.data.records:read scope on the relevant base. Set this as
AIRTABLE_API_KEY.https://airtable.com/appXXXXXXXX/...). Set this as
AIRTABLE_BASE_ID.The editor-in-chief-rotation table within that base must
have period_start, period_end, and
acting_eic_email fields.
Create the persistent data directory that will be bind-mounted into the container. This directory holds the SQLite database and the notify-email cache file and must survive container rebuilds.
Ask your DNS administrator to add an A record pointing
your chosen subdomain to the droplet’s IP address, for example:
review.example.org. IN A <droplet-ip>
Allow up to 24 hours for propagation, though it is usually much faster.
Once the DNS record is live, obtain a Let’s Encrypt certificate on
the droplet. The --standalone method requires port 80 to be
free (stop the running stack first if necessary):
The certificate and key are written to
/etc/letsencrypt/live/review.example.org/. Certbot installs
a systemd timer that renews certificates automatically; confirm it is
active:
The HTTP-01 challenge works by certbot binding a temporary server to port 80. Two things can prevent this:
Port 80 occupied. The Docker stack must be stopped before running certbot, otherwise nginx holds port 80 and the challenge fails immediately:
Port 80 blocked by a firewall. There are two independent firewalls to check:
Droplet firewall (ufw):
If ufw is active and port 80 is not listed, open it:
Digital Ocean cloud firewall: in the DO console under Networking → Firewalls, confirm there is an inbound rule allowing TCP port 80 from all sources. The cloud firewall sits in front of the droplet and blocks traffic before it reaches ufw, so both layers must allow port 80.
To confirm DNS has propagated before retrying certbot, check that the A record resolves to the droplet IP:
Set NGINX_SERVER_NAME in the environment before starting
the stack — this value is substituted into nginx.conf at
container startup:
The compose file:
/srv/roreviewapi/data into the plumber container
at /data/email, and sets
ROREVIEWAPI_EMAIL_DB=/data/email/searches.sqlite so the
SQLite database persists across rebuilds./etc/letsencrypt read-only into the nginx
container so the certificate is available.Four endpoints support the volunteer editor search workflow. All
require the shared secret token.
GET /send_searchFetches current editor email addresses from AirTable and GitHub, inserts a search record into the database, and dispatches personalised click-link emails via Postmark. The submission type (standard vs. stats) is determined automatically from the GitHub issue template.
Parameters: repourl, repo (org/repo),
issue_id, secret.
GET /click/<token>Records a volunteer response. Returns an HTTP 200 confirmation page, an “already used” page on duplicate clicks, or an “expired” page if the search has been deactivated. Sends a notification email to the current editor-in-chief on the first valid click.
No authentication required — the token itself is the credential.
GET /list_searchesReturns a data frame of all active and inactive searches with
recipient totals and click counts. Use this to find the
repourl value needed to deactivate a search.
Parameters: secret.
GET /deactivate_searchMarks the search inactive (preventing further click responses) and permanently deletes all recipient rows and the search record from the database.
Parameters: repourl, secret.
At startup, serve_api() fetches the current
editor-in-chief email address from AirTable and writes it to
/data/email/notify_email.txt (alongside the SQLite
database). This cache is refreshed every 24 hours via a background
timer. If the AirTable call fails, the existing cached value is
preserved and an error is logged.
The cached address is used as the notification recipient whenever a volunteer clicks a search link.