Deploying a Rust HTTP server to DigitalOcean App Platform
- 5 minutes read - 1017 wordsDigitalOcean launched an App Platform with many Supported Languages and Frameworks. I used Golang first, then wondered how to use non-natively-supported languages, i.e. Rust.
The good news is that Docker is a supported framework and so, you can run pretty much anything.
Repo: https://github.com/DazWilkin/do-apps-rust
Rust
I’m a Rust noob. I’m always receptive to feedback on improvements to the code. I looked to mirror the Golang example. I’m using rocket and rocket-prometheus for the first time:
You will want to install rust nightly (as Rocket has a dependency that requires it) and then you can override the default toolchain for the current project using:
rustup override set nightly
Then:
PORT=8080 cargo run
And you may then browse, e.g.:
http://localhost:8080/env
http://localhost:8080/headers
http://localhost:8080/healthz
http://localhost:8080/metrics
Dockerfile
The repo includes a Dockerfile
that builds the project using the rust nightly (required by a dependency of Rocket) and, using a multi-stage build, results in a ~100MB binary.
The DigitalOcean Apps deployments (see below) will build the container image for us, but you may build and run this locally:
REPO=[[YOUR-REPO]]
docker-build \
--tag=${REPO} \
--file=./Dockerfile \
.
And, then you can run this on your favorite ports:
CONT_PORT=[[CONTAINER-PORT]]
HOST_PORT=[[HOST-PORT]]
docker run \
--interactive --tty \
--env=PORT=${CONT_PORT} \
--publish=${HOST_PORT}:${CONT_PORT} \
${REPO}
NOTE DigitalOcean App runtime will use
8080
for theCONT_PORT
and80
for theHOST_PORT
Specification
DigitalOcean App Platform provides a Specification to define the app in order to be able to deploy it using doctl
. I cheated, deployed the default Dockerfile
sample and then revised the spec to suit my needs:
name: do-apps-rust
region: nyc
services:
- dockerfile_path: Dockerfile
github:
branch: master
deploy_on_push: true
repo: DazWilkin/do-apps-rust
health_check:
http_path: /healthz
http_port: 8080
instance_count: 1
instance_size_slug: basic-xxs
name: do-apps-rust
routes:
- path: /
doctl CLI
DigitalOcean provides an excellent command-line interface (CLI) called doctl that includes support for apps
(sic.) commands…. App Platform but apps
.
I enjoy the flexibility (and scriptability) of CLIs and so will use doctl
to create and deploy the app.
Here’s the documentation for doctl apps
Using the previously created spec, we can now:
doctl apps create --spec=./spec.yaml
The command should return Notice: App created
and a table (containing one row of data) that includes the ID, created|updated times but with no active nor in-progress deployments.
To grab the app’s ID:
ID=$(doctl apps list --format=ID --no-header) && echo ${ID}
NOTE The above assumes you’ve a single app
If you’d like, you may browse this app using DigitalOcean’s console:
$(which chromium) https://cloud.digitalocean.com/apps/${ID}
Should look similar to:
We may then deploy the app:
doctl apps create-deployment ${ID}
Using DigitalOcean’s console, you may then select “Deployments” and then click “Details” alongside the single deployment that should be listed.
Or you can type:
doctl apps list-deployments ${ID} --format=ID,Cause
ID Cause
2018108d-a1d2-44a1-a44c-1d9b6ec89e8f initial deployment
The console should be similar to this:
Once the deployment succeeds, the console will provide a link to “Live App” and a URL of the form do-apps-rust-*.digitalocean.app
that you can click to browse the app.
Alternatively, you can use the CLI to view the logs:
doctl apps logs ${ID}
NOTE The above panics until logs are available (see #878)
Yields:
do-apps-rust 2020-10-08T18:41:21.218370791Z Configured for staging.
do-apps-rust 2020-10-08T18:41:21.218511124Z => address: 0.0.0.0
do-apps-rust 2020-10-08T18:41:21.219030258Z => port: 8080
do-apps-rust 2020-10-08T18:41:21.219345227Z => log: normal
do-apps-rust 2020-10-08T18:41:21.220583016Z => workers: 16
do-apps-rust 2020-10-08T18:41:21.220797997Z => secret key: generated
do-apps-rust 2020-10-08T18:41:21.221053287Z => limits: forms = 32KiB
do-apps-rust 2020-10-08T18:41:21.221247034Z => keep-alive: 5s
do-apps-rust 2020-10-08T18:41:21.221483195Z => tls: disabled
do-apps-rust 2020-10-08T18:41:21.221928518Z Mounting /metrics:
do-apps-rust 2020-10-08T18:41:21.222302189Z => GET /metrics
do-apps-rust 2020-10-08T18:41:21.222616852Z Mounting /:
do-apps-rust 2020-10-08T18:41:21.222898195Z => GET /cached (cached)
do-apps-rust 2020-10-08T18:41:21.223094737Z => GET / (default)
do-apps-rust 2020-10-08T18:41:21.223315555Z => GET /env (env)
do-apps-rust 2020-10-08T18:41:21.223564016Z => GET /headers (headers)
do-apps-rust 2020-10-08T18:41:21.223912062Z => GET /hello/<name>/<age> (hello)
do-apps-rust 2020-10-08T18:41:21.224101833Z Fairings:
do-apps-rust 2020-10-08T18:41:21.224339284Z => 1 request: Prometheus metric collection
do-apps-rust 2020-10-08T18:41:21.224578878Z => 1 response: Prometheus metric collection
do-apps-rust 2020-10-08T18:41:21.225320238Z Rocket has launched from http://0.0.0.0:8080
do-apps-rust 2020-10-08T18:47:56.540480309Z GET / text/html:
do-apps-rust 2020-10-08T18:47:56.540626070Z => Matched: GET / (default)
do-apps-rust 2020-10-08T18:47:56.540663254Z => Outcome: Success
do-apps-rust 2020-10-08T18:47:56.543531368Z => Response succeeded.
do-apps-rust 2020-10-08T18:47:56.903673975Z GET /favicon.ico image/avif:
do-apps-rust 2020-10-08T18:47:56.903724808Z => Error: No matching routes for GET /favicon.ico image/avif.
do-apps-rust 2020-10-08T18:47:56.903737888Z => Warning: Responding with 404 Not Found catcher.
do-apps-rust 2020-10-08T18:47:56.903988998Z => Response succeeded.
do-apps-rust 2020-10-08T18:48:02.733693816Z GET /metrics text/html:
do-apps-rust 2020-10-08T18:48:02.733743842Z => Error: No matching routes for GET /,etrics text/html.
do-apps-rust 2020-10-08T18:48:02.733758128Z => Warning: Responding with 404 Not Found catcher.
do-apps-rust 2020-10-08T18:48:02.734035404Z => Response succeeded.
do-apps-rust 2020-10-08T18:48:09.506403905Z GET /metrics text/html:
do-apps-rust 2020-10-08T18:48:09.506453494Z => Matched: GET /metrics
do-apps-rust 2020-10-08T18:48:09.508885240Z => Outcome: Success
do-apps-rust 2020-10-08T18:48:09.509071469Z => Response succeeded.
Then, you can grab the app’s endpoint:
ENDPOINT=$(doctl apps list --format=DefaultIngress --no-header) && echo ${ENDPOINT}
https://do-apps-rust-9awdx.ondigitalocean.app
And, can explore the app, using the paths as before, e.g.:
curl --silent --request GET ${ENDPOINT}/metrics
Yields:
# HELP rocket_http_requests_duration_seconds HTTP request duration in seconds for all requests
# TYPE rocket_http_requests_duration_seconds histogram
rocket_http_requests_duration_seconds_bucket{endpoint="/",method="GET",status="200",le="0.005"} 1
rocket_http_requests_duration_seconds_bucket{endpoint="/",method="GET",status="200",le="0.01"} 1
rocket_http_requests_duration_seconds_bucket{endpoint="/",method="GET",status="200",le="0.025"} 1
rocket_http_requests_duration_seconds_bucket{endpoint="/",method="GET",status="200",le="0.05"} 1
rocket_http_requests_duration_seconds_bucket{endpoint="/",method="GET",status="200",le="0.1"} 1
rocket_http_requests_duration_seconds_bucket{endpoint="/",method="GET",status="200",le="0.25"} 1
rocket_http_requests_duration_seconds_bucket{endpoint="/",method="GET",status="200",le="0.5"} 1
rocket_http_requests_duration_seconds_bucket{endpoint="/",method="GET",status="200",le="1"} 1
rocket_http_requests_duration_seconds_bucket{endpoint="/",method="GET",status="200",le="2.5"} 1
rocket_http_requests_duration_seconds_bucket{endpoint="/",method="GET",status="200",le="5"} 1
rocket_http_requests_duration_seconds_bucket{endpoint="/",method="GET",status="200",le="10"} 1
rocket_http_requests_duration_seconds_bucket{endpoint="/",method="GET",status="200",le="+Inf"} 1
rocket_http_requests_duration_seconds_sum{endpoint="/",method="GET",status="200"} 0.000257543
rocket_http_requests_duration_seconds_count{endpoint="/",method="GET",status="200"} 1
# HELP rocket_http_requests_total Total number of HTTP requests
# TYPE rocket_http_requests_total counter
rocket_http_requests_total{endpoint="/",method="GET",status="200"} 1
DNS
I own dazwilkin.com
and have do.dazwilkin.com
registered as a (sub)domain on DigitalOcean. I added apps.do.dazwilkin.com
on DigitalOcean and, for giggles, x.dazwilkin.com
using the domain’s primary DNS provider.
IIUC, DigitalOcean App Platform will secure (LetsEncrypt?) certs for these DNS names so that I should be able to:
https://apps.do.dazwilkin.com/
https://x.dazwilkin.com/
Changes like this to the spec – as expected – trigger (re)deployments:
doctl apps list-deployments ${ID} \
--format=ID,Cause
Yields:
ID Cause
1f995a1e-dcc2-40be-9848-bb3d8f43b81b app spec updated
95edec36-1596-4842-8f6e-7e134bccba45 app spec updated
dfaf7b0d-6f12-427a-ad20-2fc66d2114f2 initial deployment
And:
curl --silent --request GET https://apps.do.dazwilkin.com/
Hello World!
Changes
I assumed the project created a GitHub Action to detect repo changes but it uses some other (!?) mechanism.
You can test this easily by pushing a commit to your repo and the monitoring for a new deployment:
doctl apps list-deployments ${ID} \
--format=ID,Cause
Yields:
ID Cause
ed763b4a-1fe9-4cef-ba90-4d263d6ad2b2 commit 8828049 pushed to github.com/DazWilkin/do-apps-rust/tree/master
1f995a1e-dcc2-40be-9848-bb3d8f43b81b app spec updated
95edec36-1596-4842-8f6e-7e134bccba45 app spec updated
dfaf7b0d-6f12-427a-ad20-2fc66d2114f2 initial deployment
NOTE Here’s the matching commit on GitHub
Tidy-up
When you’re done, you may wish to ensure you delete your app:
doctl apps delete ${ID} --force
Notice: App deleted
And then double-check:
doctl apps list
That’s all!