There’s a couple of options I’ve come across when I need a path to a directory with Actix, usually for static files like Javascript or CSS.
Option 1: CARGO_MANIFEST_DIR
The first is to use the CARGO_MANIFEST_DIR
environment variable which is set when compiling a crate; it points to the directory containing the package manifest (Cargo.toml
). You can get this value by using the env!
macro from std::env
:
use std::env;
fn main() {
println!(env!("CARGO_MANIFEST_DIR"));
}
You can use this value with Actix to point to the directory containing your static files:
use actix_files::Files;
use actix_web::{App, HttpServer};
use std::env;
use std::path::Path;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.service(actix_files::Files::new(
"/static",
Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/static")),
))
})
.bind("0.0.0.0:8080")?
.run()
.await
}
Downsides
There are a number of problems with this approach though:
- Variable is set by Cargo, so complilation has to use
cargo build
rather thanrustc
- The variable is set at compile-time, so will be hardcoded to the location of the manifest file when you run
cargo build
. This means it can work quite nicely in development, but if you use a continuous integration setup like Github Actions or CircleCI, thenCARGO_MANIFEST_DIR
will be set to the path of the CI runner which won’t be correct for your deployed binary. You could, of course, compile your crate on your production server in the place you want to run it, but that feels ever-so-slightly hacky.
Option 2: Environment variable
Using a very similar approach, you can set your own environment variable to customise the path. This is especially useful since you can add a fallback option, and change the value between environments.
use std::env;
fn main() {
let static_path = env::var("STATIC_PATH").unwrap_or_else(|_| "./".to_string());
println!("path: {}", static_path);
}
Outputs
cargo run -> path: ./
STATIC_PATH=/some/dir cargo run -> path: /some/dir
This is used with Actix like this:
use actix_files::Files;
use actix_web::{App, HttpServer};
use std::env;
use std::path::Path;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let static_path = env::var("STATIC_PATH").unwrap_or_else(|_| "./".to_string());
// change port based on env too
let port = env::var("PORT").unwrap_or_else(|_| "8080".to_string());
// note move so static_path is available in closure
HttpServer::new(move || {
App::new()
.service(actix_files::Files::new(
"/static",
Path::new(&format!("{}/static", static_path)),
))
})
.bind(format!("0.0.0.0:{}", port))? // update port
.run()
.await
}
Option 3: Nginx
If you’re running a web service like Actix, another alternative is to use a dedicated server like Nginx in front of your static content and skip Actix entirely. I usually keep the Actix file serving for local development, then add Nginx in front to intercept requests to static files.
Actix service:
use actix_files::Files;
use actix_web::{App, HttpServer};
use std::env;
use std::path::Path;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let static_path = env::var("STATIC_PATH").unwrap_or_else(|_| "./".to_string());
let port = env::var("PORT").unwrap_or_else(|_| "8080".to_string());
// note move so static_path is available in closure
HttpServer::new(move || {
App::new()
.service(actix_files::Files::new(
"/static",
Path::new(&format!("{}/static", static_path)),
))
})
.bind(format!("0.0.0.0:{}", port))?
.run()
.await
}
Then the Nginx configuration would look something like
# The Actix service
upstream printenvvars {
server localhost:8080;
}
server {
# usual setup stuff
server_name envvars.xyz;
# any requests to envvars.xyz/static/ will be intercepted
# and served by Nginx instead of Actix
location /static/ {
alias /path/to/your/static/files/;
}
# reverse proxy everything else to Actix
location / {
proxy_pass http://printenvvars;
}
}
This means Nginx can intercept any requests to the /static
route and serve the files directly, so the requests for static files never get to Actix in production, while all other requests get passed to the Actix using the proxy_pass
directive. The advantages of this is that Nginx is incredibly good at handling requests and serving static files; it’s quite a common pattern to have Nginx handle all web requests and then reverse proxy the requests to different application servers.
Nginx as a reverse proxy really works well if you have a completely static frontend and an API, for instance a React single-page app calling your Actix server. If you were using server-side rendering with something like Tera, then using an environment variable would be better instead.