Sistema de Almacenamiento con Rust y MongoDB

Guía completa para construir un sistema similar a Google Drive con reproducción de video, visualización de imágenes y documentos

Tabla de Contenidos

1. Introducción

Este manual detalla cómo construir un sistema de almacenamiento en la nube similar a Google Drive utilizando:

Rust

Para el backend de alto rendimiento con seguridad garantizada en tiempo de compilación.

MongoDB

Como base de datos flexible para almacenar metadatos de archivos y relaciones.

GridFS

Para almacenamiento eficiente de archivos grandes directamente en MongoDB.

Actix-web

Framework web rápido y seguro para construir la API REST.

FFmpeg

Para procesamiento de video y generación de miniaturas.

Swagger

Documentación interactiva de la API para desarrolladores.

Características principales

2. Arquitectura del Sistema

2.1. Diagrama de componentes

Arquitectura del sistema
┌─────────────────────────────────────────────────────────────────────┐
│                            Aplicación Cliente                        │
└─────────────────────────────────────────────────────────────────────┘
                                      │
                                      ▼
┌─────────────────────────────────────────────────────────────────────┐
│                              API REST                                │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐ │
│  │ Autenticación│  │  Gestión de  │  │  Streaming  │  │Previsualiza-│ │
│  │ (JWT+Bcrypt) │  │  Archivos    │  │   de Video  │  │   ciones    │ │
│  └─────────────┘  └─────────────┘  └─────────────┘  └─────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
                                      │
                   ┌──────────────────┼──────────────────┐
                   │                  │                  │
                   ▼                  ▼                  ▼
┌───────────────────────┐  ┌───────────────────────┐  ┌───────────────────────┐
│      MongoDB          │  │      GridFS           │  │      FFmpeg           │
│  (Metadatos y         │  │  (Almacenamiento      │  │  (Procesamiento       │
│   relaciones)         │  │   de archivos)        │  │   de multimedia)      │
└───────────────────────┘  └───────────────────────┘  └───────────────────────┘
            

2.2. Flujo de almacenamiento de archivos

  1. Cliente envía archivo mediante multipart/form-data
  2. API valida autenticación y permisos
  3. Se extraen metadatos básicos (tamaño, tipo MIME, hash)
  4. Para archivos multimedia, se generan miniaturas y versiones optimizadas
  5. El archivo se almacena en GridFS
  6. Los metadatos se guardan en colecciones de MongoDB
  7. Se registra la actividad en el sistema
  8. Se devuelve al cliente la información del archivo almacenado

3. Configuración del Proyecto

3.1. Dependencias principales

Cargo.toml
[package]
name = "rust-drive-api"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4.0"
actix-files = "0.6"  # Para servir archivos estáticos
actix-multipart = "0.5"  # Para manejar uploads
mongodb = { version = "2.0", features = ["sync", "gridfs"] }
bcrypt = "0.14"  # Para hashing de contraseñas
jsonwebtoken = "8.0"  # Para JWT
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
validator = { version = "2.0", features = ["derive"] }  # Validación
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1.0", features = ["v4", "serde"] }
futures = "0.3"  # Para operaciones async
tokio = { version = "1.0", features = ["full"] }  # Runtime async
mime = "0.3"  # Para tipos MIME
mime_guess = "2.0"  # Para detectar tipos de archivo
utoipa = { version = "2.0", features = ["actix_extras"] }  # Swagger
utoipa-swagger-ui = "2.0"  # UI Swagger
reqwest = { version = "0.11", features = ["json"] }  # Para llamadas HTTP
bytes = "1.0"  # Para manejo de bytes
lazy_static = "1.4"  # Para variables globales
thiserror = "1.0"  # Manejo de errores
log = "0.4"  # Logging
env_logger = "0.9"  # Logger
config = "0.13"  # Configuración
dotenv = "0.15"  # Variables de entorno

3.2. Configuración de la aplicación

config/default.toml
[server]
host = "127.0.0.1"
port = 8080
max_upload_size = "10GB"  # Límite para uploads
temp_dir = "./tmp"  # Directorio temporal

[database]
url = "mongodb://localhost:27017"
name = "drive_db"
timeout_ms = 5000

[auth]
jwt_secret = "your_very_secure_jwt_secret"
jwt_expires_in = 1440  # minutos (24 horas)
salt_rounds = 12  # Para bcrypt

[storage]
gridfs_bucket = "files"  # Nombre del bucket GridFS
thumbnails_bucket = "thumbnails"  # Para miniaturas
previews_bucket = "previews"  # Para previsualizaciones

[ffmpeg]
path = "/usr/bin/ffmpeg"  # Ruta al binario ffmpeg
thumbnail_size = "320x180"  # Tamaño miniaturas video
video_qualities = ["480p", "720p", "1080p"]  # Calidades para streaming

4. Modelos de Datos

4.1. Modelo de Usuario

src/models/user.rs
use mongodb::bson::{oid::ObjectId, DateTime};
use serde::{Serialize, Deserialize};
use validator::Validate;

#[derive(Debug, Serialize, Deserialize)]
pub struct User {
    #[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
    pub id: Option<ObjectId>,
    pub email: String,
    #[serde(skip_serializing)]
    pub password: String,
    pub name: String,
    pub storage_used: i64,  // Bytes usados
    pub storage_limit: i64, // Límite en bytes
    pub is_active: bool,
    pub is_admin: bool,
    pub created_at: DateTime,
    pub updated_at: DateTime,
}

#[derive(Debug, Deserialize, Validate)]
pub struct NewUser {
    #[validate(email)]
    pub email: String,
    #[validate(length(min = 8))]
    pub password: String,
    #[validate(length(min = 2))]
    pub name: String,
}

#[derive(Debug, Deserialize, Validate)]
pub struct LoginUser {
    #[validate(email)]
    pub email: String,
    #[validate(length(min = 8))]
    pub password: String,
}

4.2. Modelo de Archivo

src/models/file.rs
use mongodb::bson::{oid::ObjectId, DateTime, doc};
use serde::{Serialize, Deserialize};
use std::path::PathBuf;

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct File {
    #[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
    pub id: Option<ObjectId>,
    pub name: String,
    pub path: String,  // Ruta lógica en el sistema
    pub size: i64,     // Tamaño en bytes
    pub mime_type: String,
    pub file_id: ObjectId,  // ID en GridFS
    pub is_folder: bool,
    pub parent_id: Option<ObjectId>,  // Carpeta contenedora
    pub owner_id: ObjectId,  // Usuario propietario
    pub shared_with: Vec<ObjectId>,  // Usuarios con acceso
    pub permissions: Vec<Permission>,
    pub versions: Vec<FileVersion>,
    pub thumbnail_id: Option<ObjectId>,  // ID de miniatura en GridFS
    pub preview_id: Option<ObjectId>,   // ID de previsualización
    pub created_at: DateTime,
    pub updated_at: DateTime,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct FileVersion {
    pub version: i32,
    pub file_id: ObjectId,
    pub size: i64,
    pub created_at: DateTime,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Permission {
    pub user_id: ObjectId,
    pub can_read: bool,
    pub can_write: bool,
    pub can_share: bool,
}

impl File {
    pub fn collection(db: &mongodb::sync::Database) -> mongodb::sync::Collection<Self> {
        db.collection("files")
    }
    
    pub fn get_full_path(&self) -> PathBuf {
        PathBuf::from(&self.path).join(&self.name)
    }
    
    pub fn is_media(&self) -> bool {
        self.mime_type.starts_with("image/") || 
        self.mime_type.starts_with("video/") || 
        self.mime_type.starts_with("audio/")
    }
    
    pub fn is_document(&self) -> bool {
        self.mime_type == "application/pdf" || 
        self.mime_type.starts_with("text/") || 
        self.mime_type == "application/msword" ||
        self.mime_type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
    }
}

5. Autenticación y Autorización

5.1. Configuración de JWT

src/utils/auth.rs
use chrono::{Duration, Utc};
use jsonwebtoken::{encode, decode, Header, EncodingKey, DecodingKey, Validation};
use serde::{Serialize, Deserialize};
use mongodb::bson::oid::ObjectId;
use crate::config::AuthConfig;

#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
    pub sub: String,  // user_id
    pub exp: usize,   // expiry
    pub is_admin: bool,
}

pub fn create_jwt_token(
    user_id: ObjectId,
    is_admin: bool,
    auth_config: &AuthConfig,
) -> Result<String, jsonwebtoken::errors::Error> {
    let expiration_time = Utc::now()
        .checked_add_signed(Duration::minutes(auth_config.jwt_expires_in))
        .expect("Invalid timestamp")
        .timestamp();
    
    let claims = Claims {
        sub: user_id.to_hex(),
        exp: expiration_time as usize,
        is_admin,
    };
    
    encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(auth_config.jwt_secret.as_bytes()),
    )
}

pub fn decode_jwt_token(
    token: &str,
    auth_config: &AuthConfig,
) -> Result<Claims, jsonwebtoken::errors::Error> {
    decode::<Claims>(
        token,
        &DecodingKey::from_secret(auth_config.jwt_secret.as_bytes()),
        &Validation::default(),
    )
    .map(|data| data.claims)
}

5.2. Middleware de Autenticación

src/middleware/auth.rs
use actix_web::{dev::ServiceRequest, Error, http};
use actix_web_httpauth::extractors::bearer::{BearerAuth, Config};
use jsonwebtoken::errors::ErrorKind;
use mongodb::bson::oid::ObjectId;

use crate::{
    utils::auth::decode_jwt_token,
    config::AuthConfig,
    models::user::User,
    errors::ApiError,
};

pub async fn validator(
    req: ServiceRequest,
    credentials: BearerAuth,
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
    let auth_config = req.app_data::<AuthConfig>()
        .ok_or_else(|| {
            let error = Error::from(ApiError::InternalServerError);
            (error, req)
        })?;
    
    match decode_jwt_token(credentials.token(), auth_config) {
        Ok(claims) => {
            // Verificar que el usuario existe
            let user_id = ObjectId::parse_str(&claims.sub)
                .map_err(|_| {
                    let error = AuthenticationError::from(Config::default())
                        .with_error("Invalid user ID in token");
                    (error.into(), req)
                })?;
            
            let db = req.app_data::<mongodb::sync::Database>()
                .ok_or_else(|| {
                    let error = Error::from(ApiError::InternalServerError);
                    (error, req)
                })?;
            
            let user_exists = User::collection(db)
                .count_documents(doc! { "_id": user_id }, None)
                .map_err(|_| {
                    let error = Error::from(ApiError::InternalServerError);
                    (error, req)
                })? > 0;
            
            if !user_exists {
                let error = AuthenticationError::from(Config::default())
                    .with_error("User no longer exists");
                return Err((error.into(), req));
            }
            
            // Añadir claims a las extensiones de la petición
            req.extensions_mut().insert(claims);
            Ok(req)
        },
        Err(e) => {
            let config = req.app_data::<Config>().cloned().unwrap_or_default();
            
            let error = match e.kind() {
                ErrorKind::ExpiredSignature => {
                    AuthenticationError::from(config).with_error("Token expired")
                },
                _ => AuthenticationError::from(config).with_error("Invalid token"),
            };
            
            Err((error.into(), req))
        }
    }
}

6. Almacenamiento de Archivos

6.1. Servicio de Almacenamiento con GridFS

src/services/storage.rs
use mongodb::{
    sync::{Client, Database, GridFsBucket},
    bson::{doc, oid::ObjectId},
    options::GridFsBucketOptions,
};
use std::path::Path;
use crate::config::StorageConfig;

pub struct StorageService {
    files_bucket: GridFsBucket,
    thumbnails_bucket: GridFsBucket,
    previews_bucket: GridFsBucket,
}

impl StorageService {
    pub fn new(db: &Database, config: &StorageConfig) -> Self {
        let files_options = GridFsBucketOptions::builder()
            .bucket_name(&config.gridfs_bucket)
            .build();
        
        let thumbnails_options = GridFsBucketOptions::builder()
            .bucket_name(&config.thumbnails_bucket)
            .build();
            
        let previews_options = GridFsBucketOptions::builder()
            .bucket_name(&config.previews_bucket)
            .build();
        
        Self {
            files_bucket: db.gridfs_bucket(files_options),
            thumbnails_bucket: db.gridfs_bucket(thumbnails_options),
            previews_bucket: db.gridfs_bucket(previews_options),
        }
    }
    
    pub fn upload_file(
        &self,
        filename: &str,
        mime_type: &str,
        data: &[u8],
    ) -> Result<ObjectId, mongodb::error::Error> {
        let mut upload_stream = self.files_bucket.open_upload_stream(filename, None);
        upload_stream.write_all(data)?;
        let file_id = upload_stream.finish()?;
        Ok(file_id)
    }
    
    pub fn download_file(
        &self,
        file_id: ObjectId,
    ) -> Result<Vec<u8>, mongodb::error::Error> {
        let mut cursor = self.files_bucket.open_download_stream(file_id)?;
        let mut buffer = Vec::new();
        cursor.read_to_end(&mut buffer)?;
        Ok(buffer)
    }
    
    pub fn delete_file(&self, file_id: ObjectId) -> Result<(), mongodb::error::Error> {
        self.files_bucket.delete(file_id)?;
        Ok(())
    }
    
    // Métodos similares para thumbnails y previews...
}

7. Streaming de Video

src/services/video.rs
use std::process::{Command, Stdio};
use std::path::Path;
use tempfile::NamedTempFile;
use crate::{config::FfmpegConfig, errors::ApiError};

pub struct VideoService {
    ffmpeg_path: String,
    thumbnail_size: String,
    video_qualities: Vec<String>,
}

impl VideoService {
    pub fn new(config: &FfmpegConfig) -> Self {
        Self {
            ffmpeg_path: config.path.clone(),
            thumbnail_size: config.thumbnail_size.clone(),
            video_qualities: config.video_qualities.clone(),
        }
    }
    
    pub fn generate_thumbnail(
        &self,
        video_data: &[u8],
    ) -> Result<Vec<u8>, ApiError> {
        // Crear archivo temporal para el video
        let mut video_file = NamedTempFile::new()?;
        video_file.write_all(video_data)?;
        let video_path = video_file.path().to_str().unwrap();
        
        // Crear archivo temporal para la miniatura
        let thumbnail_file = NamedTempFile::new()?;
        let thumbnail_path = thumbnail_file.path().to_str().unwrap();
        
        // Ejecutar ffmpeg para generar miniatura
        let status = Command::new(&self.ffmpeg_path)
            .args(&[
                "-i", video_path,
                "-ss", "00:00:01",  // Capturar en el segundo 1
                "-vframes", "1",
                "-s", &self.thumbnail_size,
                "-f", "image2",
                thumbnail_path,
            ])
            .status()?;
        
        if !status.success() {
            return Err(ApiError::new(500, "Failed to generate thumbnail"));
        }
        
        // Leer la miniatura generada
        let thumbnail_data = std::fs::read(thumbnail_path)?;
        Ok(thumbnail_data)
    }
    
    pub fn transcode_video(
        &self,
        video_data: &[u8],
        quality: &str,
    ) -> Result<Vec<u8>, ApiError> {
        // Implementar transcodificación a diferentes calidades
        // Similar a generate_thumbnail pero con parámetros de transcodificación
        // ...
        todo!()
    }
}

8. Previsualizaciones

src/services/preview.rs
use std::process::{Command, Stdio};
use std::io::{Write, Read};
use tempfile::NamedTempFile;
use crate::errors::ApiError;

pub struct PreviewService;

impl PreviewService {
    pub fn generate_pdf_preview(
        &self,
        pdf_data: &[u8],
    ) -> Result<Vec<u8>, ApiError> {
        // Usar pdftoppm o similar para generar imagen de la primera página
        let mut pdf_file = NamedTempFile::new()?;
        pdf_file.write_all(pdf_data)?;
        let pdf_path = pdf_file.path().to_str().unwrap();
        
        let output_file = NamedTempFile::new()?;
        let output_path = output_file.path().to_str().unwrap();
        
        let status = Command::new("pdftoppm")
            .args(&[
                "-f", "1",  // Primera página
                "-l", "1",  // Solo una página
                "-jpeg",
                pdf_path,
                output_path,
            ])
            .status()?;
        
        if !status.success() {
            return Err(ApiError::new(500, "Failed to generate PDF preview"));
        }
        
        let preview_data = std::fs::read(format!("{}-1.jpg", output_path))?;
        Ok(preview_data)
    }
    
    pub fn generate_archive_preview(
        &self,
        archive_data: &[u8],
    ) -> Result<Vec<String>, ApiError> {
        // Listar contenido de archivos comprimidos (ZIP, RAR, etc.)
        let mut archive_file = NamedTempFile::new()?;
        archive_file.write_all(archive_data)?;
        let archive_path = archive_file.path().to_str().unwrap();
        
        let output = Command::new("unzip")
            .args(&["-l", archive_path])
            .output()?;
        
        if !output.status.success() {
            return Err(ApiError::new(500, "Failed to list archive contents"));
        }
        
        let contents = String::from_utf8(output.stdout)?;
        let files = contents.lines()
            .skip(3)  // Saltar líneas de encabezado
            .take(10)  // Mostrar solo los primeros 10 archivos
            .map(|s| s.trim().to_string())
            .collect();
        
        Ok(files)
    }
}

9. Rutas y Controladores

9.1. Rutas de Archivos

src/routes/files.rs
use actix_web::{web, Scope};
use crate::handlers::files::{
    upload_file_handler,
    download_file_handler,
    stream_video_handler,
    get_file_preview_handler,
    list_files_handler,
    create_folder_handler,
    share_file_handler,
};

pub fn files_routes() -> Scope {
    web::scope("/files")
        .service(
            web::resource("")
                .route(web::get().to(list_files_handler))
                .route(web::post().to(upload_file_handler))
        )
        .service(
            web::resource("/folder")
                .route(web::post().to(create_folder_handler))
        )
        .service(
            web::resource("/{file_id}")
                .route(web::get().to(download_file_handler))
        )
        .service(
            web::resource("/{file_id}/preview")
                .route(web::get().to(get_file_preview_handler))
        )
        .service(
            web::resource("/{file_id}/stream")
                .route(web::get().to(stream_video_handler))
        )
        .service(
            web::resource("/{file_id}/share")
                .route(web::post().to(share_file_handler))
        )
}

9.2. Controlador de Upload

src/handlers/files.rs
use actix_web::{web, HttpResponse, HttpRequest};
use actix_multipart::Multipart;
use futures_util::StreamExt;
use mongodb::bson::{doc, DateTime};
use std::path::Path;

use crate::{
    models::file::File,
    services::{storage::StorageService, video::VideoService, preview::PreviewService},
    utils::auth::Claims,
    errors::ApiError,
    AppState,
};

pub async fn upload_file_handler(
    state: web::Data<AppState>,
    req: HttpRequest,
    mut payload: Multipart,
) -> Result<HttpResponse, ApiError> {
    // Verificar autenticación
    let claims = req.extensions().get::<Claims>()
        .ok_or(ApiError::Unauthorized("Invalid token".into()))?;
    let user_id = mongodb::bson::oid::ObjectId::parse_str(&claims.sub)
        .map_err(|_| ApiError::BadRequest("Invalid user ID".into()))?;
    
    // Procesar multipart form
    while let Some(item) = payload.next().await {
        let mut field = item?;
        let content_type = field.content_disposition().unwrap();
        let filename = content_type.get_filename().unwrap();
        
        // Validar tamaño del archivo
        let mut file_data = Vec::new();
        while let Some(chunk) = field.next().await {
            file_data.extend_from_slice(&chunk?);
            
            // Verificar límite de almacenamiento
            if file_data.len() as i64 > state.config.server.max_upload_size {
                return Err(ApiError::PayloadTooLarge);
            }
        }
        
        // Determinar tipo MIME
        let mime_type = mime_guess::from_path(filename).first_or_octet_stream();
        
        // Almacenar archivo en GridFS
        let file_id = state.storage.upload_file(filename, &mime_type.to_string(), &file_data)?;
        
        // Generar miniaturas y previsualizaciones para tipos específicos
        let thumbnail_id = if mime_type.type_() == mime::IMAGE || mime_type.type_() == mime::VIDEO {
            let thumbnail_data = if mime_type.type_() == mime::IMAGE {
                // Redimensionar imagen para miniatura
                image::load_from_memory(&file_data)?
                    .resize(320, 180, image::imageops::FilterType::Lanczos3)
                    .to_bytes()
            } else {
                // Generar miniatura de video
                state.video.generate_thumbnail(&file_data)?
            };
            
            Some(state.storage.upload_thumbnail(
                &format!("thumb_{}", filename),
                "image/jpeg",
                &thumbnail_data,
            )?)
        } else {
            None
        };
        
        // Generar previsualización para documentos
        let preview_id = if mime_type == mime::APPLICATION_PDF {
            let preview_data = state.preview.generate_pdf_preview(&file_data)?;
            Some(state.storage.upload_preview(
                &format!("preview_{}.jpg", filename),
                "image/jpeg",
                &preview_data,
            )?)
        } else {
            None
        };
        
        // Guardar metadatos en MongoDB
        let file = File {
            id: None,
            name: filename.to_string(),
            path: "/".to_string(),  // Carpeta raíz por defecto
            size: file_data.len() as i64,
            mime_type: mime_type.to_string(),
            file_id,
            is_folder: false,
            parent_id: None,
            owner_id: user_id,
            shared_with: Vec::new(),
            permissions: Vec::new(),
            versions: Vec::new(),
            thumbnail_id,
            preview_id,
            created_at: DateTime::now(),
            updated_at: DateTime::now(),
        };
        
        File::collection(&state.db).insert_one(file, None)?;
    }
    
    Ok(HttpResponse::Ok().json(json!({"status": "success"})))
}

10. Documentación con Swagger

src/main.rs (extracto)
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;

#[derive(OpenApi)]
#[openapi(
    paths(
        handlers::auth::login_handler,
        handlers::auth::register_handler,
        handlers::files::upload_file_handler,
        handlers::files::download_file_handler,
        handlers::files::stream_video_handler,
        handlers::files::get_file_preview_handler,
    ),
    components(
        schemas(
            models::user::User,
            models::file::File,
            models::file::Permission,
            errors::ApiError,
        )
    ),
    tags(
        (name = "Auth", description = "Authentication endpoints"),
        (name = "Files", description = "File management endpoints"),
    )
)]
struct ApiDoc;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // ... configuración previa ...
    
    let app = HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(app_state.clone()))
            .configure(routes::config)
            .service(
                SwaggerUi::new("/swagger-ui/{_:.*}")
                    .url("/api-docs/openapi.json", ApiDoc::openapi()),
            )
    })
    .bind((config.server.host, config.server.port))?
    .run();
    
    app.await
}

11. Despliegue en Linux

11.1. Dockerfile para producción

Dockerfile
# Stage 1: Builder
FROM rust:1.60 as builder

# Instalar dependencias del sistema
RUN apt-get update && apt-get install -y \
    cmake \
    libssl-dev \
    pkg-config \
    ffmpeg \
    poppler-utils \  # Para pdftoppm
    unzip \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY . .

# Build estático con musl para imagen minimal
RUN rustup target add x86_64-unknown-linux-musl
RUN cargo build --target x86_64-unknown-linux-musl --release

# Stage 2: Runtime image
FROM debian:buster-slim

# Instalar dependencias de runtime
RUN apt-get update && apt-get install -y \
    ffmpeg \
    poppler-utils \
    && rm -rf /var/lib/apt/lists/*

# Crear directorio para archivos temporales
RUN mkdir -p /app/tmp
ENV TEMP_DIR=/app/tmp

# Copiar binario
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/rust-drive-api /usr/local/bin/rust-drive-api

# Variables de entorno
ENV RUST_LOG=info
ENV APP__DATABASE__URL=mongodb://mongo:27017
ENV APP__DATABASE__NAME=drive_db
ENV APP__AUTH__JWT_SECRET=your_secure_secret_key
ENV APP__FFMPEG__PATH=/usr/bin/ffmpeg

EXPOSE 8080
CMD ["rust-drive-api"]

11.2. docker-compose.yml

docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    depends_on:
      - mongo
    volumes:
      - ./data/tmp:/app/tmp  # Directorio temporal
    environment:
      - APP__DATABASE__URL=mongodb://mongo:27017
      - APP__DATABASE__NAME=drive_db
      - APP__AUTH__JWT_SECRET=your_secure_secret_key
      - APP__AUTH__SALT_ROUNDS=12
      - APP__STORAGE__GRIDFS_BUCKET=files
      - APP__STORAGE__THUMBNAILS_BUCKET=thumbnails
      - APP__STORAGE__PREVIEWS_BUCKET=previews
      - APP__FFMPEG__PATH=/usr/bin/ffmpeg
      - APP__FFMPEG__THUMBNAIL_SIZE=320x180
    restart: unless-stopped

  mongo:
    image: mongo:5.0
    environment:
      - MONGO_INITDB_ROOT_USERNAME=admin
      - MONGO_INITDB_ROOT_PASSWORD=password
    volumes:
      - ./data/mongo:/data/db
    ports:
      - "27017:27017"
    restart: unless-stopped

  mongo-express:
    image: mongo-express:0.54
    restart: always
    ports:
      - "8081:8081"
    environment:
      - ME_CONFIG_MONGODB_ADMINUSERNAME=admin
      - ME_CONFIG_MONGODB_ADMINPASSWORD=password
      - ME_CONFIG_MONGODB_SERVER=mongo
    depends_on:
      - mongo

volumes:
  data:

Consejos para producción