Hello World server

Dumindu Madunuwan
Learning Rust
Published in
4 min readDec 18, 2023

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 the async keyword). It prints Hello, world! to the console.
  • The main function is also asynchronous (thanks to #[tokio::main]). It calls the hello 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 the get function for defining a route with the HTTP GET method and the Router type for creating a routing table.
  • async fn hello() -> &'static str: Defines an asynchronous function named hello that returns a static string (&'static str)
  • let app = Router::new().route("/hello", get(hello));: Creates a new Axum Router named app. It defines a single route for the path "/hello" using the HTTP GET method and associates it with the hello 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's serve function to start serving the app on the specified listener. 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 the musl-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.

🚀 learning-rust.github.io | 🧑‍💻 Follow! dumindu |🥤Buy me a coffee

--

--

Dumindu Madunuwan
Learning Rust

I am a web developer and a designer who loves Rust, Golang, Firefox, UI/UX, Linux and Gnome.