Docker shit

This commit is contained in:
2026-07-14 19:23:14 +02:00
parent 5585917eb1
commit b62bba0173
12 changed files with 334 additions and 7 deletions
+13
View File
@@ -0,0 +1,13 @@
target/
backend/target/
frontend/target/
frontend/dist/
frontend/node_modules/
dist/
.git/
.idea/
.env
.antigravitycli/
*.pdb
**/*.rs.bk
info ding video.mp4
+1 -1
View File
@@ -8,7 +8,7 @@ axum = "0.8.9"
axum-extra = { version = "0.12.6", features = ["cookie", "typed-header", "form"] } axum-extra = { version = "0.12.6", features = ["cookie", "typed-header", "form"] }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio", "tls-native-tls", "chrono"] } sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio", "tls-native-tls", "chrono", "migrate"] }
tokio = { version = "1.52.1", features = ["rt-multi-thread", "macros"] } tokio = { version = "1.52.1", features = ["rt-multi-thread", "macros"] }
dotenv = "0.15.0" dotenv = "0.15.0"
chrono = { workspace = true } chrono = { workspace = true }
+8
View File
@@ -77,6 +77,14 @@ async fn main() {
} }
}; };
// Run database migrations on container/application startup
println!("Running database migrations...");
if let Err(err) = sqlx::migrate!("./migrations").run(&pool).await {
println!("Failed to run database migrations: {:?}", err);
std::process::exit(1);
}
println!("Database migrations completed successfully");
// Configure CORS to allow requests from frontend // Configure CORS to allow requests from frontend
let cors = CorsLayer::new() let cors = CorsLayer::new()
.allow_origin(env.origin.parse::<HeaderValue>().unwrap()) .allow_origin(env.origin.parse::<HeaderValue>().unwrap())
+127
View File
@@ -0,0 +1,127 @@
# ============================================================
# Dockerfile — Full application (frontend + backend)
# ============================================================
# Multi-stage build:
# 1. chef install cargo-chef for dependency caching
# 2. planner generate recipe.json from the workspace
# 3. backend build the backend binary (release)
# 4. frontend build the frontend WASM bundle with Trunk
# 5. runtime minimal image: backend binary + static frontend + nginx
# ============================================================
# --------------- Stage 1: Chef base ---------------
FROM rust:1-bookworm AS chef
RUN cargo install cargo-chef
WORKDIR /app
# --------------- Stage 2: Planner ---------------
FROM chef AS planner
COPY Cargo.toml Cargo.lock ./
COPY src/ src/
COPY backend/ backend/
COPY frontend/ frontend/
RUN cargo chef prepare --recipe-path recipe.json
# --------------- Stage 3: Backend builder ---------------
FROM chef AS backend-builder
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json -p backend
COPY Cargo.toml Cargo.lock ./
COPY src/ src/
COPY backend/ backend/
COPY frontend/ frontend/
RUN cargo build --release -p backend
# --------------- Stage 4: Frontend builder ---------------
FROM chef AS frontend-builder
# Add the WASM target
RUN rustup target add wasm32-unknown-unknown
# Install Trunk (the WASM bundler used by the frontend) and sass (for SCSS)
RUN cargo install trunk
# Cache frontend dependencies
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --target wasm32-unknown-unknown --recipe-path recipe.json -p frontend
# Copy full workspace source for the build
COPY Cargo.toml Cargo.lock ./
COPY src/ src/
COPY backend/ backend/
COPY frontend/ frontend/
# Build the frontend WASM bundle
# Trunk outputs to frontend/dist by default
WORKDIR /app/frontend
RUN trunk build --release
# --------------- Stage 5: Runtime ---------------
FROM debian:bookworm-slim AS runtime
# Install runtime dependencies:
# - ca-certificates & libssl3: TLS for PostgreSQL connections
# - nginx: serves the frontend static files and reverse-proxies /api to the backend
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates libssl3 nginx && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy the compiled backend binary
COPY --from=backend-builder /app/target/release/backend /app/backend
# Copy migrations
COPY backend/migrations/ /app/migrations/
# Copy the frontend static build output
COPY --from=frontend-builder /app/frontend/dist /var/www/html
# Nginx configuration: serve frontend + reverse-proxy /api to the backend
RUN cat > /etc/nginx/sites-available/default <<'EOF'
server {
listen 80;
server_name _;
root /var/www/html;
index index.html;
# Serve static frontend files, fall back to index.html for client-side routing
location / {
try_files $uri $uri/ /index.html;
}
# Reverse-proxy API requests to the Axum backend
location /api/ {
proxy_pass http://127.0.0.1:8001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
EOF
# Startup script: launch backend in the background, then nginx in the foreground
RUN cat > /app/start.sh <<'SCRIPT'
#!/bin/bash
set -e
# Start the backend API server in the background
/app/backend &
# Start nginx in the foreground (keeps the container alive)
nginx -g 'daemon off;'
SCRIPT
RUN chmod +x /app/start.sh
EXPOSE 80
# Environment variables should be provided at runtime:
# DATABASE_URL PostgreSQL connection string
# TOKEN_SECRET JWT signing key
# ORIGIN Allowed CORS origin (should be the public URL of this container)
CMD ["/app/start.sh"]
+66
View File
@@ -0,0 +1,66 @@
# ============================================================
# Dockerfile.backend — Backend only
# ============================================================
# Multi-stage build:
# 1. chef install cargo-chef for dependency caching
# 2. planner generate a recipe.json from the workspace
# 3. builder build dependencies first (cached), then the backend binary
# 4. runtime minimal image with just the compiled binary
# ============================================================
# --------------- Stage 1: Chef base ---------------
FROM rust:1-bookworm AS chef
RUN cargo install cargo-chef
WORKDIR /app
# --------------- Stage 2: Planner ---------------
FROM chef AS planner
# Copy the full workspace so cargo-chef can resolve all dependencies
COPY Cargo.toml Cargo.lock ./
COPY src/ src/
COPY backend/ backend/
COPY frontend/ frontend/
RUN cargo chef prepare --recipe-path recipe.json
# --------------- Stage 3: Builder ---------------
FROM chef AS builder
# Cache dependency compilation in its own layer
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json -p backend
# Now copy the real source and build
COPY Cargo.toml Cargo.lock ./
COPY src/ src/
COPY backend/ backend/
COPY frontend/ frontend/
RUN cargo build --release -p backend
# --------------- Stage 4: Runtime ---------------
FROM debian:bookworm-slim AS runtime
# Install TLS root certificates and PostgreSQL client libs (needed by sqlx
# at runtime for TLS connections via native-tls)
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates libssl3 && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy the compiled binary
COPY --from=builder /app/target/release/backend /app/backend
# Copy migrations so they can be run at startup if needed
COPY backend/migrations/ /app/migrations/
EXPOSE 8001
# Environment variables should be provided at runtime via docker run --env
# or docker compose environment/env_file.
#
# Required:
# DATABASE_URL PostgreSQL connection string
# TOKEN_SECRET JWT signing key
# ORIGIN Allowed CORS origin (frontend URL)
CMD ["/app/backend"]
+46 -2
View File
@@ -6,9 +6,53 @@ services:
ports: ports:
- 5432:5432 - 5432:5432
environment: environment:
- POSTGRES_PASSWORD=tickets POSTGRES_PASSWORD: tickets
POSTGRES_DB: system_data
volumes: volumes:
- pg_data:/var/lib/postregsql/pg_data - pg_data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
# Full application (frontend + backend + nginx)
# Usage: docker compose --profile full up --build
app:
profiles: ["full"]
build:
context: ..
dockerfile: docker/Dockerfile
container_name: ticketsystem
restart: unless-stopped
ports:
- 8080:80
environment:
DATABASE_URL: postgres://postgres:tickets@postgres:5432/system_data
TOKEN_SECRET: 160257c8c5ff2298363529e963a5901d2efa6d6ae67d634487c4d2bbc41c533f
ORIGIN: http://localhost:8080
depends_on:
postgres:
condition: service_healthy
# Backend only (API server)
# Usage: docker compose --profile backend up --build
backend:
profiles: ["backend"]
build:
context: ..
dockerfile: docker/Dockerfile.backend
container_name: ticketsystem-backend
restart: unless-stopped
ports:
- 8001:8001
environment:
DATABASE_URL: postgres://postgres:tickets@postgres:5432/system_data
TOKEN_SECRET: 160257c8c5ff2298363529e963a5901d2efa6d6ae67d634487c4d2bbc41c533f
ORIGIN: http://localhost:8000
depends_on:
postgres:
condition: service_healthy
volumes: volumes:
pg_data: pg_data:
+40 -3
View File
@@ -19,23 +19,60 @@ use yew::prelude::*;
/// ``` /// ```
#[function_component] #[function_component]
pub fn ThemeToggle() -> Html { pub fn ThemeToggle() -> Html {
let is_dark = use_state(|| {
if let Some(window) = web_sys::window() {
if let Some(document) = window.document() {
if let Some(html) = document.document_element() {
return html.get_attribute("data-theme").unwrap_or_default() == "dark";
}
}
}
false
});
let onclick = { let onclick = {
Callback::from(|_| { let is_dark = is_dark.clone();
Callback::from(move |_| {
if let Some(window) = web_sys::window() { if let Some(window) = web_sys::window() {
if let Some(document) = window.document() { if let Some(document) = window.document() {
if let Some(html) = document.document_element() { if let Some(html) = document.document_element() {
let current = html.get_attribute("data-theme").unwrap_or_default(); let current = html.get_attribute("data-theme").unwrap_or_default();
let new_theme = if current == "dark" { "" } else { "dark" }; let new_theme = if current == "dark" { "" } else { "dark" };
let _ = html.set_attribute("data-theme", new_theme); let _ = html.set_attribute("data-theme", new_theme);
is_dark.set(new_theme == "dark");
} }
} }
} }
}) })
}; };
let icon_html = if *is_dark {
// Sun SVG for dark mode
html! {
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="theme-icon">
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2" />
<path d="M12 20v2" />
<path d="m4.93 4.93 1.41 1.41" />
<path d="m17.66 17.66 1.41 1.41" />
<path d="M2 12h2" />
<path d="M20 12h2" />
<path d="m6.34 17.66-1.41 1.41" />
<path d="m19.07 4.93-1.41 1.41" />
</svg>
}
} else {
// Moon SVG for light mode
html! {
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="theme-icon">
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
</svg>
}
};
html! { html! {
<button {onclick} class="sidebar-button"> <button {onclick} class="sidebar-button theme-toggle-btn">
{"🌙"} {icon_html}
</button> </button>
} }
} }
+2
View File
@@ -20,6 +20,7 @@ pub struct AdminSetupScheme {
pub first_name: String, pub first_name: String,
pub last_name: String, pub last_name: String,
pub username: String, pub username: String,
pub is_admin: bool,
pub pwd: String, pub pwd: String,
} }
@@ -154,6 +155,7 @@ pub fn initial_admin_setup() -> Html {
first_name: first_name_val, first_name: first_name_val,
last_name: last_name_val, last_name: last_name_val,
username: username_val, username: username_val,
is_admin: true,
pwd: pwd_val, pwd: pwd_val,
}; };
+5
View File
@@ -11,6 +11,8 @@
--color: #000000; --color: #000000;
--color-container: #f0f0f0; --color-container: #f0f0f0;
--color-text: #000000; --color-text: #000000;
--color-placeholder: #757575;
--color-input: #1a1a1a;
} }
[data-theme='dark'] { [data-theme='dark'] {
@@ -26,4 +28,7 @@
--color: #000000; --color: #000000;
--color-container: #1a1a1a; --color-container: #1a1a1a;
--color-text: #000000; --color-text: #000000;
--color-placeholder: #a0a0a0;
--color-input: #f0f0f0;
} }
@@ -6,11 +6,17 @@
padding: 0.75rem; padding: 0.75rem;
font-size: 1rem; font-size: 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
color: white; color: var(--color-input);
border: none; border: none;
cursor: pointer; cursor: pointer;
margin-bottom: 15px; margin-bottom: 15px;
} }
input::placeholder,
textarea::placeholder {
color: var(--color-placeholder);
opacity: 1;
}
button:hover { button:hover {
cursor: pointer; cursor: pointer;
@@ -55,6 +55,12 @@
cursor: pointer; cursor: pointer;
text-align: left; text-align: left;
margin-top: var(--spacing-sm); margin-top: var(--spacing-sm);
&.theme-toggle-btn {
display: flex;
justify-content: center;
align-items: center;
}
} }
.sidebar-header { .sidebar-header {
+13
View File
@@ -90,10 +90,12 @@ body {
padding: 16px; padding: 16px;
font-size: 1.8rem; font-size: 1.8rem;
background-color: #f0f0f0; background-color: #f0f0f0;
color: var(--color-input);
box-shadow: #6d6d6d; box-shadow: #6d6d6d;
box-shadow: 6px 6px 8px; box-shadow: 6px 6px 8px;
} }
input[type="checkbox"] { input[type="checkbox"] {
width: auto; width: auto;
} }
@@ -123,4 +125,15 @@ body {
box-sizing: border-box; box-sizing: border-box;
} }
input, select, textarea {
color: var(--color-input);
}
input::placeholder,
textarea::placeholder {
color: var(--color-placeholder);
opacity: 1;
}