Hello World server
Welcome back! It's been almost 5 years since the last Rust tutorial series.
🧑💻 In this article…
⏵ Let’s take the first steps to Build a Dockerized RESTful API application in Rust.
⏵ We’ll start withcargo
, Tokio & Axum; then let’s Dockerize our webserver.
1. Create a new project
cargo new myapp
💡 This generates the following project structure,
├── Cargo.toml
└── src
└── main.rs
with a simple hello world application in src/main.rs
fn main() {
println!("Hello, world!");
}
and Cargo.toml
with package details
[package]
name = "myapp"
version = "0.1.0"
edition = "2021"
[dependencies]
🧑💻 Run cargo run
inside myapp
folder. You should see Hello, world!
in terminal.
2. Add Tokio
🔎 Rust’s standard library doesn’t include a runtime by default. It’s a design choice and we can have even multiple runtimes in a single program, each managing its own set of asynchronous tasks. Tokio is a popular runtime, but other runtimes, such as async-std, smol, and others, also exist.
cargo add tokio --features "full"
💡 This adds Tokio as project dependency to the Cargo.toml
[dependencies]
tokio = { version = "1.35.0", features = ["full"] }
Let’s update src/main.rs
to make hello world application asynchronous,
async fn hello() {
println!("Hello, world!");
}
#[tokio::main]
async fn main() {
hello().await;
}
- The
hello
function is an asynchronous function (indicated by theasync
keyword). It printsHello, world!
to the console. - The
main
function is also asynchronous (thanks to#[tokio::main]
). It calls thehello
function and awaits its completion.
🧑💻 Run cargo run
inside myapp
folder to verify the changes.
3. Add Axum
🔎 Hyper is a high-performance, asynchronous HTTP library for Rust. Axum is a web framework using Hyper, developed by the same team behind the Tokio. Axum is specifically designed to be a modern and ergonomic web framework. It embraces Rust’s
async/await
syntax and aims to simplify the process of writing asynchronous code for web development.
cargo add axum
💡 This adds Axum as project dependency to the Cargo.toml
[dependencies]
axum = "0.7.2"
tokio = { version = "1.35.0", features = ["full"] }
Let’s update src/main.rs
to make hello world Axum server,
use axum::{routing::get, Router};
async fn hello() -> &'static str {
"Hello, world!"
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/hello", get(hello));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
use axum::{routing::get, Router};
: Imports necessary components from the Axum library. It brings in theget
function for defining a route with the HTTP GET method and theRouter
type for creating a routing table.async fn hello() -> &'static str
: Defines an asynchronous function namedhello
that returns a static string (&'static str
)let app = Router::new().route("/hello", get(hello));
: Creates a new AxumRouter
namedapp
. It defines a single route for the path "/hello" using the HTTP GET method and associates it with thehello
async function.let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
: Binds a TCP listener to the address "0.0.0.0:3000". This listener will accept incoming connections on port 3000.axum::serve(listener, app).await.unwrap();
: Uses Axum'sserve
function to start serving theapp
on the specifiedlistener
. The server runs asynchronously, waiting for incoming requests and handling them according to the defined routes.
🧑💻 Run cargo run
inside myapp
folder to verify. You should see “Hello, world!” in localhost:3000/hello in your webbrowser or using tools like curl
4. Dockerize the application
Let’s add our development Dockerfile
inside myapp
folder,
FROM rust:alpine
WORKDIR /myapp
RUN apk add --no-cache musl-dev
RUN --mount=type=bind,source=src,target=src \
--mount=type=bind,source=Cargo.toml,target=Cargo.toml \
--mount=type=bind,source=Cargo.lock,target=Cargo.lock \
--mount=type=cache,target=/app/target/ \
--mount=type=cache,target=/usr/local/cargo/registry/ \
<<EOF
set -e
cargo build --locked --release
EOF
CMD ["/myapp/target/release/myapp"]
EXPOSE 3000
Let’s breakdown the content,
FROM rust:alpine
: Specifies the base Docker image uses the official Rust image based on .WORKDIR /myapp
: Sets the working directory inside the container to "/myapp".RUN apk add --no-cache musl-dev
: Installs themusl-dev
package, which is necessary for building binaries that use the musl libc.- The lines starting with
RUN --mount=type=bind...
: Use a heredoc (<<EOF
) to run commands inside the container. --mount
is used to bind local directories and caches into the container. This is a feature provided by BuildKit in Docker.set -e
ensures that the script exits if any command returns a non-zero status.cargo build --locked --release
builds the Rust application in release mode.CMD ["/myapp/target/release/myapp"]
: Sets the default command to run when the container starts. It specifies the path to the built release binary of the Rust application.
We can run docker build -t myapp .
and docker run -p 3000:3000 myapp
to test this. But, let’s add a docker-compose.yaml
.
version: '3'
services:
app:
build: .
ports:
- "3000:3000"
🧑💻 Make sure you stop previous docker run
command and run docker-compose up
to start our web server and check localhost:3000/hello
“Do or do not. There is no try”. — Yoda.