diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cfccc61 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +**/target +**/node_modules +**/.svelte-kit +frontend/build +.git +**/.env +**/.env.* diff --git a/.gitignore b/.gitignore index de7a0e2..4045ec0 100644 --- a/.gitignore +++ b/.gitignore @@ -18,5 +18,8 @@ node_modules/ # ─── env / secrets ─── # .env +.env.test .env.local .env.*.local + +docs/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..49eb9b3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM node:22-alpine AS frontend-builder +WORKDIR /frontend +RUN npm install -g pnpm@10 +COPY frontend/package.json frontend/pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile +COPY frontend/ ./ +RUN pnpm build + +FROM rust:1-bookworm AS backend-builder +WORKDIR /backend +ENV SQLX_OFFLINE=true +COPY backend/ ./ +RUN cargo build --release + +FROM debian:12-slim AS runtime +WORKDIR /app +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* \ + && mkdir -p /app/storage +COPY --from=backend-builder /backend/target/release/backend /app/backend +COPY --from=frontend-builder /frontend/build /app/static +ENV STORAGE_PATH=/app/storage + +EXPOSE 3000 + +CMD ["/app/backend"] \ No newline at end of file diff --git a/backend/src/main.rs b/backend/src/main.rs index c662fe3..90e7da8 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -13,7 +13,11 @@ use axum::{ }; use sqlx::PgPool; use tower_cookies::CookieManagerLayer; -use tower_http::{cors::CorsLayer, services::ServeDir, trace::TraceLayer}; +use tower_http::{ + cors::CorsLayer, + services::{ServeDir, ServeFile}, + trace::TraceLayer, +}; use tracing::info; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -39,8 +43,10 @@ async fn main() -> Result<()> { .init(); dotenvy::dotenv().ok(); + let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let pool = PgPool::connect(&database_url).await.unwrap(); + sqlx::migrate!().run(&pool).await?; let file_repository = FileRepository::new(pool.clone())?; let routes_file = routes_file(file_repository.clone()) .route_layer(middleware::from_fn(mw_require_auth)) @@ -67,7 +73,9 @@ async fn main() -> Result<()> { .allow_credentials(true) .allow_headers([header::CONTENT_TYPE]), ) - .fallback_service(ServeDir::new("./")); + .fallback_service( + ServeDir::new("/app/static").fallback(ServeFile::new("/app/static/index.html")), + ); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; info!("listening on {}", listener.local_addr().unwrap()); diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..a4b7b4e --- /dev/null +++ b/compose.yml @@ -0,0 +1,36 @@ +services: + db: + image: docker.io/library/postgres:16 + restart: unless-stopped + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=pass + - POSTGRES_DB=loft + ports: + - "5432:5432" + volumes: + - loft_db_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d loft"] + interval: 5s + timeout: 5s + retries: 5 + + app: + image: loft + restart: unless-stopped + build: . + ports: + - "3000:3000" + depends_on: + db: + condition: service_healthy + environment: + - DATABASE_URL=postgres://postgres:pass@db:5432/loft + - STORAGE_PATH=/app/storage + volumes: + - loft_storage:/app/storage + +volumes: + loft_db_data: + loft_storage: