config: simplify runtime environment model

This commit is contained in:
a.tolmachev
2026-04-11 12:40:00 +03:00
parent 9b6ce337e6
commit 51cf7691be
14 changed files with 127 additions and 66 deletions
+3 -5
View File
@@ -1,17 +1,16 @@
POSTGRES_DB=crank POSTGRES_DB=crank
POSTGRES_USER=crank POSTGRES_USER=crank
POSTGRES_PASSWORD=change-me POSTGRES_PASSWORD=change-me
CRANK_DATABASE_URL=postgres://crank:change-me@postgres:5432/crank POSTGRES_HOST=postgres
POSTGRES_PORT=5432
CRANK_ADMIN_API_IMAGE=crank/admin-api:dev CRANK_ADMIN_API_IMAGE=crank/admin-api:dev
CRANK_MCP_SERVER_IMAGE=crank/mcp-server:dev CRANK_MCP_SERVER_IMAGE=crank/mcp-server:dev
CRANK_UI_IMAGE=crank/ui:dev CRANK_UI_IMAGE=crank/ui:dev
CRANK_STORAGE_ROOT=/var/lib/crank/storage CRANK_STORAGE_ROOT=/var/lib/crank/storage
CRANK_PUBLISH_HOST=127.0.0.1
CRANK_ADMIN_BIND=0.0.0.0:3001 CRANK_ADMIN_BIND=0.0.0.0:3001
CRANK_MCP_BIND=0.0.0.0:3002 CRANK_MCP_BIND=0.0.0.0:3002
CRANK_MCP_REFRESH_MS=5000 CRANK_MCP_REFRESH_MS=5000
CRANK_LOG_LEVEL=info CRANK_LOG_LEVEL=info
CRANK_SECRET_PROVIDER=env
CRANK_MASTER_KEY=change-me-master-key CRANK_MASTER_KEY=change-me-master-key
CRANK_SESSION_SECRET=change-me-session-secret CRANK_SESSION_SECRET=change-me-session-secret
CRANK_PASSWORD_PEPPER=change-me-password-pepper CRANK_PASSWORD_PEPPER=change-me-password-pepper
@@ -20,5 +19,4 @@ CRANK_BOOTSTRAP_ADMIN_EMAIL=owner@crank.local
CRANK_BOOTSTRAP_ADMIN_PASSWORD=change-me-admin-password CRANK_BOOTSTRAP_ADMIN_PASSWORD=change-me-admin-password
CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME=Crank Owner CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME=Crank Owner
CRANK_DEMO_SEED=false CRANK_DEMO_SEED=false
CRANK_PUBLIC_BASE_URL=https://crank.example.com CRANK_BASE_URL=https://crank.example.com
CRANK_MCP_PUBLIC_URL=https://crank.example.com/mcp
+12 -12
View File
@@ -122,17 +122,15 @@ jobs:
POSTGRES_DB: ${{ secrets.POSTGRES_DB }} POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
POSTGRES_USER: ${{ secrets.POSTGRES_USER }} POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
CRANK_DATABASE_URL: ${{ secrets.CRANK_DATABASE_URL }} POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }}
POSTGRES_PORT: ${{ secrets.POSTGRES_PORT }}
CRANK_STORAGE_ROOT: ${{ secrets.CRANK_STORAGE_ROOT }} CRANK_STORAGE_ROOT: ${{ secrets.CRANK_STORAGE_ROOT }}
CRANK_PUBLISH_HOST: ${{ secrets.CRANK_PUBLISH_HOST }}
CRANK_ADMIN_BIND: ${{ secrets.CRANK_ADMIN_BIND }} CRANK_ADMIN_BIND: ${{ secrets.CRANK_ADMIN_BIND }}
CRANK_MCP_BIND: ${{ secrets.CRANK_MCP_BIND }} CRANK_MCP_BIND: ${{ secrets.CRANK_MCP_BIND }}
CRANK_MCP_REFRESH_MS: ${{ secrets.CRANK_MCP_REFRESH_MS }} CRANK_MCP_REFRESH_MS: ${{ secrets.CRANK_MCP_REFRESH_MS }}
CRANK_LOG_LEVEL: ${{ secrets.CRANK_LOG_LEVEL }} CRANK_LOG_LEVEL: ${{ secrets.CRANK_LOG_LEVEL }}
CRANK_SECRET_PROVIDER: ${{ secrets.CRANK_SECRET_PROVIDER }}
CRANK_MASTER_KEY: ${{ secrets.CRANK_MASTER_KEY }} CRANK_MASTER_KEY: ${{ secrets.CRANK_MASTER_KEY }}
CRANK_PUBLIC_BASE_URL: ${{ secrets.CRANK_PUBLIC_BASE_URL }} CRANK_BASE_URL: ${{ secrets.CRANK_BASE_URL }}
CRANK_MCP_PUBLIC_URL: ${{ secrets.CRANK_MCP_PUBLIC_URL }}
CRANK_SESSION_SECRET: ${{ secrets.CRANK_SESSION_SECRET }} CRANK_SESSION_SECRET: ${{ secrets.CRANK_SESSION_SECRET }}
CRANK_PASSWORD_PEPPER: ${{ secrets.CRANK_PASSWORD_PEPPER }} CRANK_PASSWORD_PEPPER: ${{ secrets.CRANK_PASSWORD_PEPPER }}
CRANK_SESSION_TTL_HOURS: ${{ secrets.CRANK_SESSION_TTL_HOURS }} CRANK_SESSION_TTL_HOURS: ${{ secrets.CRANK_SESSION_TTL_HOURS }}
@@ -151,17 +149,15 @@ jobs:
append_if_set POSTGRES_DB "$POSTGRES_DB" append_if_set POSTGRES_DB "$POSTGRES_DB"
append_if_set POSTGRES_USER "$POSTGRES_USER" append_if_set POSTGRES_USER "$POSTGRES_USER"
append_if_set POSTGRES_PASSWORD "$POSTGRES_PASSWORD" append_if_set POSTGRES_PASSWORD "$POSTGRES_PASSWORD"
append_if_set CRANK_DATABASE_URL "$CRANK_DATABASE_URL" append_if_set POSTGRES_HOST "$POSTGRES_HOST"
append_if_set POSTGRES_PORT "$POSTGRES_PORT"
append_if_set CRANK_STORAGE_ROOT "$CRANK_STORAGE_ROOT" append_if_set CRANK_STORAGE_ROOT "$CRANK_STORAGE_ROOT"
append_if_set CRANK_PUBLISH_HOST "$CRANK_PUBLISH_HOST"
append_if_set CRANK_ADMIN_BIND "$CRANK_ADMIN_BIND" append_if_set CRANK_ADMIN_BIND "$CRANK_ADMIN_BIND"
append_if_set CRANK_MCP_BIND "$CRANK_MCP_BIND" append_if_set CRANK_MCP_BIND "$CRANK_MCP_BIND"
append_if_set CRANK_MCP_REFRESH_MS "$CRANK_MCP_REFRESH_MS" append_if_set CRANK_MCP_REFRESH_MS "$CRANK_MCP_REFRESH_MS"
append_if_set CRANK_LOG_LEVEL "$CRANK_LOG_LEVEL" append_if_set CRANK_LOG_LEVEL "$CRANK_LOG_LEVEL"
append_if_set CRANK_SECRET_PROVIDER "$CRANK_SECRET_PROVIDER"
append_if_set CRANK_MASTER_KEY "$CRANK_MASTER_KEY" append_if_set CRANK_MASTER_KEY "$CRANK_MASTER_KEY"
append_if_set CRANK_PUBLIC_BASE_URL "$CRANK_PUBLIC_BASE_URL" append_if_set CRANK_BASE_URL "$CRANK_BASE_URL"
append_if_set CRANK_MCP_PUBLIC_URL "$CRANK_MCP_PUBLIC_URL"
append_if_set CRANK_SESSION_SECRET "$CRANK_SESSION_SECRET" append_if_set CRANK_SESSION_SECRET "$CRANK_SESSION_SECRET"
append_if_set CRANK_PASSWORD_PEPPER "$CRANK_PASSWORD_PEPPER" append_if_set CRANK_PASSWORD_PEPPER "$CRANK_PASSWORD_PEPPER"
append_if_set CRANK_SESSION_TTL_HOURS "$CRANK_SESSION_TTL_HOURS" append_if_set CRANK_SESSION_TTL_HOURS "$CRANK_SESSION_TTL_HOURS"
@@ -184,13 +180,17 @@ jobs:
set -e set -e
cd '${{ secrets.DEPLOY_PATH }}' cd '${{ secrets.DEPLOY_PATH }}'
required_vars=' required_vars='
CRANK_DATABASE_URL POSTGRES_HOST
POSTGRES_PORT
POSTGRES_DB
POSTGRES_USER
POSTGRES_PASSWORD
CRANK_MASTER_KEY CRANK_MASTER_KEY
CRANK_SESSION_SECRET CRANK_SESSION_SECRET
CRANK_PASSWORD_PEPPER CRANK_PASSWORD_PEPPER
CRANK_BOOTSTRAP_ADMIN_EMAIL CRANK_BOOTSTRAP_ADMIN_EMAIL
CRANK_BOOTSTRAP_ADMIN_PASSWORD CRANK_BOOTSTRAP_ADMIN_PASSWORD
CRANK_PUBLIC_BASE_URL CRANK_BASE_URL
' '
for var in \$required_vars; do for var in \$required_vars; do
value=\$(grep -E \"^\${var}=\" .env | tail -n1 | cut -d= -f2- || true) value=\$(grep -E \"^\${var}=\" .env | tail -n1 | cut -d= -f2- || true)
+5 -5
View File
@@ -2,15 +2,15 @@
## Current ## Current
### `feat/rust-docker-build-cache` ### `feat/runtime-env-simplification`
Status: in_progress Status: in_progress
DoD: DoD:
- Rust Docker builds use a cache-friendly multi-stage layout - runtime uses component-based PostgreSQL config as the primary model
- `admin-api` and `mcp-server` reuse dependency layers across rebuilds - `CRANK_BASE_URL` replaces split public URL variables
- noisy local artifacts are excluded from Docker build context - deployment uses one GitHub secret per runtime env variable
- deployment fails early when required secret-store env is missing - deprecated env variables are removed from compose, examples, and docs
## Next ## Next
+1 -1
View File
@@ -23,6 +23,7 @@ serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
serde_yaml.workspace = true serde_yaml.workspace = true
sha2.workspace = true sha2.workspace = true
sqlx.workspace = true
thiserror.workspace = true thiserror.workspace = true
time.workspace = true time.workspace = true
tokio = { workspace = true, features = ["fs"] } tokio = { workspace = true, features = ["fs"] }
@@ -33,4 +34,3 @@ uuid.workspace = true
[dev-dependencies] [dev-dependencies]
reqwest.workspace = true reqwest.workspace = true
serial_test = "3" serial_test = "3"
sqlx.workspace = true
+26 -5
View File
@@ -9,6 +9,7 @@ mod storage;
use std::{env, net::SocketAddr, path::PathBuf}; use std::{env, net::SocketAddr, path::PathBuf};
use crank_registry::PostgresRegistry; use crank_registry::PostgresRegistry;
use sqlx::postgres::PgConnectOptions;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tracing::info; use tracing::info;
@@ -28,15 +29,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
) )
.init(); .init();
let database_url = env::var("CRANK_DATABASE_URL")?;
let storage_root = PathBuf::from( let storage_root = PathBuf::from(
env::var("CRANK_STORAGE_ROOT").unwrap_or_else(|_| "/var/lib/crank/storage".into()), env::var("CRANK_STORAGE_ROOT").unwrap_or_else(|_| "/var/lib/crank/storage".into()),
); );
let bind_addr = env::var("CRANK_ADMIN_BIND").unwrap_or_else(|_| "0.0.0.0:3001".into()); let bind_addr = env::var("CRANK_ADMIN_BIND").unwrap_or_else(|_| "0.0.0.0:3001".into());
let public_base_url = let base_url = env::var("CRANK_BASE_URL").unwrap_or_else(|_| "http://localhost:3000".into());
env::var("CRANK_PUBLIC_BASE_URL").unwrap_or_else(|_| "http://localhost:3000".into());
let socket_addr: SocketAddr = bind_addr.parse()?; let socket_addr: SocketAddr = bind_addr.parse()?;
let registry = PostgresRegistry::connect(&database_url).await?; let registry = PostgresRegistry::connect_with_options(database_options_from_env()?).await?;
let auth_settings = AuthSettings { let auth_settings = AuthSettings {
session_secret: env::var("CRANK_SESSION_SECRET")?, session_secret: env::var("CRANK_SESSION_SECRET")?,
password_pepper: env::var("CRANK_PASSWORD_PEPPER")?, password_pepper: env::var("CRANK_PASSWORD_PEPPER")?,
@@ -44,7 +43,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.ok() .ok()
.and_then(|value| value.parse::<i64>().ok()) .and_then(|value| value.parse::<i64>().ok())
.unwrap_or(24), .unwrap_or(24),
cookie_secure: public_base_url.starts_with("https://"), cookie_secure: base_url.starts_with("https://"),
bootstrap_admin: BootstrapAdminConfig { bootstrap_admin: BootstrapAdminConfig {
email: env::var("CRANK_BOOTSTRAP_ADMIN_EMAIL")?, email: env::var("CRANK_BOOTSTRAP_ADMIN_EMAIL")?,
password: env::var("CRANK_BOOTSTRAP_ADMIN_PASSWORD")?, password: env::var("CRANK_BOOTSTRAP_ADMIN_PASSWORD")?,
@@ -79,3 +78,25 @@ fn env_flag(name: &str) -> bool {
Some("1" | "true" | "yes" | "on") Some("1" | "true" | "yes" | "on")
) )
} }
fn database_options_from_env() -> Result<PgConnectOptions, Box<dyn std::error::Error>> {
if let Ok(database_url) = env::var("CRANK_DATABASE_URL") {
return Ok(database_url.parse::<PgConnectOptions>()?);
}
let host = env::var("POSTGRES_HOST").unwrap_or_else(|_| "postgres".into());
let port = env::var("POSTGRES_PORT")
.ok()
.and_then(|value| value.parse::<u16>().ok())
.unwrap_or(5432);
let database = env::var("POSTGRES_DB").unwrap_or_else(|_| "crank".into());
let username = env::var("POSTGRES_USER").unwrap_or_else(|_| "crank".into());
let password = env::var("POSTGRES_PASSWORD").unwrap_or_else(|_| "crank".into());
Ok(PgConnectOptions::new()
.host(&host)
.port(port)
.database(&database)
.username(&username)
.password(&password))
}
+1 -1
View File
@@ -16,6 +16,7 @@ futures-util = "0.3"
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
sha2.workspace = true sha2.workspace = true
sqlx.workspace = true
time.workspace = true time.workspace = true
tokio = { workspace = true, features = ["sync"] } tokio = { workspace = true, features = ["sync"] }
tracing.workspace = true tracing.workspace = true
@@ -27,4 +28,3 @@ crank-adapter-grpc = { path = "../../crates/crank-adapter-grpc", features = ["te
crank-mapping = { path = "../../crates/crank-mapping" } crank-mapping = { path = "../../crates/crank-mapping" }
crank-schema = { path = "../../crates/crank-schema" } crank-schema = { path = "../../crates/crank-schema" }
reqwest.workspace = true reqwest.workspace = true
sqlx.workspace = true
+26 -4
View File
@@ -7,6 +7,7 @@ use std::{env, net::SocketAddr, time::Duration};
use crank_registry::PostgresRegistry; use crank_registry::PostgresRegistry;
use crank_runtime::SecretCrypto; use crank_runtime::SecretCrypto;
use sqlx::postgres::PgConnectOptions;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tracing::info; use tracing::info;
@@ -21,18 +22,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
) )
.init(); .init();
let database_url = env::var("CRANK_DATABASE_URL")?;
let bind_addr = env::var("CRANK_MCP_BIND").unwrap_or_else(|_| "0.0.0.0:3002".into()); let bind_addr = env::var("CRANK_MCP_BIND").unwrap_or_else(|_| "0.0.0.0:3002".into());
let public_base_url = env::var("CRANK_PUBLIC_BASE_URL").ok(); let base_url = env::var("CRANK_BASE_URL").ok();
let refresh_interval = env::var("CRANK_MCP_REFRESH_MS") let refresh_interval = env::var("CRANK_MCP_REFRESH_MS")
.ok() .ok()
.and_then(|value| value.parse::<u64>().ok()) .and_then(|value| value.parse::<u64>().ok())
.map(Duration::from_millis) .map(Duration::from_millis)
.unwrap_or_else(|| Duration::from_secs(5)); .unwrap_or_else(|| Duration::from_secs(5));
let socket_addr: SocketAddr = bind_addr.parse()?; let socket_addr: SocketAddr = bind_addr.parse()?;
let registry = PostgresRegistry::connect(&database_url).await?; let registry = PostgresRegistry::connect_with_options(database_options_from_env()?).await?;
let secret_crypto = SecretCrypto::new(&env::var("CRANK_MASTER_KEY")?)?; let secret_crypto = SecretCrypto::new(&env::var("CRANK_MASTER_KEY")?)?;
let app = build_app(registry, refresh_interval, public_base_url, secret_crypto); let app = build_app(registry, refresh_interval, base_url, secret_crypto);
let listener = TcpListener::bind(socket_addr).await?; let listener = TcpListener::bind(socket_addr).await?;
info!("mcp-server listening on {}", socket_addr); info!("mcp-server listening on {}", socket_addr);
@@ -42,6 +42,28 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(()) Ok(())
} }
fn database_options_from_env() -> Result<PgConnectOptions, Box<dyn std::error::Error>> {
if let Ok(database_url) = env::var("CRANK_DATABASE_URL") {
return Ok(database_url.parse::<PgConnectOptions>()?);
}
let host = env::var("POSTGRES_HOST").unwrap_or_else(|_| "postgres".into());
let port = env::var("POSTGRES_PORT")
.ok()
.and_then(|value| value.parse::<u16>().ok())
.unwrap_or(5432);
let database = env::var("POSTGRES_DB").unwrap_or_else(|_| "crank".into());
let username = env::var("POSTGRES_USER").unwrap_or_else(|_| "crank".into());
let password = env::var("POSTGRES_PASSWORD").unwrap_or_else(|_| "crank".into());
Ok(PgConnectOptions::new()
.host(&host)
.port(port)
.database(&database)
.username(&username)
.password(&password))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{ use std::{
+6 -4
View File
@@ -91,13 +91,16 @@ until docker exec "$POSTGRES_CONTAINER" pg_isready -U "$POSTGRES_USER" -d "$POST
sleep 1 sleep 1
done done
export CRANK_DATABASE_URL="postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@127.0.0.1:$POSTGRES_PORT/$POSTGRES_DB" export POSTGRES_HOST="127.0.0.1"
export POSTGRES_PORT="$POSTGRES_PORT"
export POSTGRES_DB="$POSTGRES_DB"
export POSTGRES_USER="$POSTGRES_USER"
export POSTGRES_PASSWORD="$POSTGRES_PASSWORD"
export CRANK_STORAGE_ROOT="$TMP_DIR/storage" export CRANK_STORAGE_ROOT="$TMP_DIR/storage"
export CRANK_ADMIN_BIND="0.0.0.0:$ADMIN_PORT" export CRANK_ADMIN_BIND="0.0.0.0:$ADMIN_PORT"
export CRANK_MCP_BIND="0.0.0.0:$MCP_PORT" export CRANK_MCP_BIND="0.0.0.0:$MCP_PORT"
export CRANK_MCP_REFRESH_MS="1000" export CRANK_MCP_REFRESH_MS="1000"
export CRANK_LOG_LEVEL="info" export CRANK_LOG_LEVEL="info"
export CRANK_SECRET_PROVIDER="env"
export CRANK_SESSION_SECRET="e2e-session-secret" export CRANK_SESSION_SECRET="e2e-session-secret"
export CRANK_PASSWORD_PEPPER="e2e-password-pepper" export CRANK_PASSWORD_PEPPER="e2e-password-pepper"
export CRANK_MASTER_KEY="0000000000000000000000000000000000000000000000000000000000000000" export CRANK_MASTER_KEY="0000000000000000000000000000000000000000000000000000000000000000"
@@ -106,8 +109,7 @@ export CRANK_BOOTSTRAP_ADMIN_EMAIL="$ADMIN_EMAIL"
export CRANK_BOOTSTRAP_ADMIN_PASSWORD="$ADMIN_PASSWORD" export CRANK_BOOTSTRAP_ADMIN_PASSWORD="$ADMIN_PASSWORD"
export CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME="Crank E2E" export CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME="Crank E2E"
export CRANK_DEMO_SEED="true" export CRANK_DEMO_SEED="true"
export CRANK_PUBLIC_BASE_URL="http://127.0.0.1:$UI_PORT" export CRANK_BASE_URL="http://127.0.0.1:$UI_PORT"
export CRANK_MCP_PUBLIC_URL="http://127.0.0.1:$MCP_PORT"
export CRANK_E2E_STREAM_FIXTURE_PORT="$STREAM_FIXTURE_PORT" export CRANK_E2E_STREAM_FIXTURE_PORT="$STREAM_FIXTURE_PORT"
export CRANK_E2E_GRPC_FIXTURE_BIND="$GRPC_FIXTURE_BIND" export CRANK_E2E_GRPC_FIXTURE_BIND="$GRPC_FIXTURE_BIND"
+9 -1
View File
@@ -10,7 +10,7 @@ use serde::{Serialize, de::DeserializeOwned};
use serde_json::Value; use serde_json::Value;
use sqlx::{ use sqlx::{
PgPool, Postgres, Row, Transaction, PgPool, Postgres, Row, Transaction,
postgres::{PgPoolOptions, PgRow}, postgres::{PgConnectOptions, PgPoolOptions, PgRow},
types::Json, types::Json,
}; };
@@ -46,6 +46,14 @@ impl PostgresRegistry {
Self::connect_in_schema(database_url, None).await Self::connect_in_schema(database_url, None).await
} }
pub async fn connect_with_options(
connect_options: PgConnectOptions,
) -> Result<Self, RegistryError> {
let pool = PgPoolOptions::new().connect_with(connect_options).await?;
migrations::apply_postgres(&pool).await?;
Ok(Self { pool })
}
pub fn pool(&self) -> &PgPool { pub fn pool(&self) -> &PgPool {
&self.pool &self.pool
} }
+15 -9
View File
@@ -23,13 +23,16 @@ services:
dockerfile: apps/admin-api/Dockerfile dockerfile: apps/admin-api/Dockerfile
restart: unless-stopped restart: unless-stopped
environment: environment:
CRANK_DATABASE_URL: ${CRANK_DATABASE_URL:-postgres://crank:crank@postgres:5432/crank} POSTGRES_HOST: ${POSTGRES_HOST:-postgres}
POSTGRES_PORT: ${POSTGRES_PORT:-5432}
POSTGRES_DB: ${POSTGRES_DB:-crank}
POSTGRES_USER: ${POSTGRES_USER:-crank}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-crank}
CRANK_STORAGE_ROOT: ${CRANK_STORAGE_ROOT:-/var/lib/crank/storage} CRANK_STORAGE_ROOT: ${CRANK_STORAGE_ROOT:-/var/lib/crank/storage}
CRANK_ADMIN_BIND: ${CRANK_ADMIN_BIND:-0.0.0.0:3001} CRANK_ADMIN_BIND: ${CRANK_ADMIN_BIND:-0.0.0.0:3001}
CRANK_LOG_LEVEL: ${CRANK_LOG_LEVEL:-info} CRANK_LOG_LEVEL: ${CRANK_LOG_LEVEL:-info}
CRANK_SECRET_PROVIDER: ${CRANK_SECRET_PROVIDER:-env}
CRANK_MASTER_KEY: ${CRANK_MASTER_KEY} CRANK_MASTER_KEY: ${CRANK_MASTER_KEY}
CRANK_PUBLIC_BASE_URL: ${CRANK_PUBLIC_BASE_URL:-http://localhost:3000} CRANK_BASE_URL: ${CRANK_BASE_URL:-http://localhost:3000}
CRANK_SESSION_SECRET: ${CRANK_SESSION_SECRET} CRANK_SESSION_SECRET: ${CRANK_SESSION_SECRET}
CRANK_PASSWORD_PEPPER: ${CRANK_PASSWORD_PEPPER} CRANK_PASSWORD_PEPPER: ${CRANK_PASSWORD_PEPPER}
CRANK_SESSION_TTL_HOURS: ${CRANK_SESSION_TTL_HOURS:-24} CRANK_SESSION_TTL_HOURS: ${CRANK_SESSION_TTL_HOURS:-24}
@@ -43,7 +46,7 @@ services:
volumes: volumes:
- artifact_storage:${CRANK_STORAGE_ROOT:-/var/lib/crank/storage} - artifact_storage:${CRANK_STORAGE_ROOT:-/var/lib/crank/storage}
ports: ports:
- "${CRANK_PUBLISH_HOST:-127.0.0.1}:3001:3001" - "127.0.0.1:3001:3001"
healthcheck: healthcheck:
test: ["CMD", "curl", "--fail", "http://127.0.0.1:3001/health"] test: ["CMD", "curl", "--fail", "http://127.0.0.1:3001/health"]
interval: 15s interval: 15s
@@ -57,21 +60,24 @@ services:
dockerfile: apps/mcp-server/Dockerfile dockerfile: apps/mcp-server/Dockerfile
restart: unless-stopped restart: unless-stopped
environment: environment:
CRANK_DATABASE_URL: ${CRANK_DATABASE_URL:-postgres://crank:crank@postgres:5432/crank} POSTGRES_HOST: ${POSTGRES_HOST:-postgres}
POSTGRES_PORT: ${POSTGRES_PORT:-5432}
POSTGRES_DB: ${POSTGRES_DB:-crank}
POSTGRES_USER: ${POSTGRES_USER:-crank}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-crank}
CRANK_STORAGE_ROOT: ${CRANK_STORAGE_ROOT:-/var/lib/crank/storage} CRANK_STORAGE_ROOT: ${CRANK_STORAGE_ROOT:-/var/lib/crank/storage}
CRANK_MCP_BIND: ${CRANK_MCP_BIND:-0.0.0.0:3002} CRANK_MCP_BIND: ${CRANK_MCP_BIND:-0.0.0.0:3002}
CRANK_MCP_REFRESH_MS: ${CRANK_MCP_REFRESH_MS:-5000} CRANK_MCP_REFRESH_MS: ${CRANK_MCP_REFRESH_MS:-5000}
CRANK_LOG_LEVEL: ${CRANK_LOG_LEVEL:-info} CRANK_LOG_LEVEL: ${CRANK_LOG_LEVEL:-info}
CRANK_SECRET_PROVIDER: ${CRANK_SECRET_PROVIDER:-env}
CRANK_MASTER_KEY: ${CRANK_MASTER_KEY} CRANK_MASTER_KEY: ${CRANK_MASTER_KEY}
CRANK_PUBLIC_BASE_URL: ${CRANK_PUBLIC_BASE_URL:-http://localhost:3000} CRANK_BASE_URL: ${CRANK_BASE_URL:-http://localhost:3000}
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
volumes: volumes:
- artifact_storage:${CRANK_STORAGE_ROOT:-/var/lib/crank/storage} - artifact_storage:${CRANK_STORAGE_ROOT:-/var/lib/crank/storage}
ports: ports:
- "${CRANK_PUBLISH_HOST:-127.0.0.1}:3002:3002" - "127.0.0.1:3002:3002"
healthcheck: healthcheck:
test: ["CMD", "curl", "--fail", "http://127.0.0.1:3002/health"] test: ["CMD", "curl", "--fail", "http://127.0.0.1:3002/health"]
interval: 15s interval: 15s
@@ -85,7 +91,7 @@ services:
dockerfile: apps/ui/Dockerfile dockerfile: apps/ui/Dockerfile
restart: unless-stopped restart: unless-stopped
ports: ports:
- "${CRANK_PUBLISH_HOST:-127.0.0.1}:3000:3000" - "127.0.0.1:3000:3000"
healthcheck: healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/"] test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/"]
interval: 15s interval: 15s
+1 -1
View File
@@ -7,7 +7,7 @@
## 2. Предусловия ## 2. Предусловия
- доступен `PostgreSQL`; - доступен `PostgreSQL`;
- задан `CRANK_DATABASE_URL`; - заданы `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`;
- задан `CRANK_STORAGE_ROOT`; - задан `CRANK_STORAGE_ROOT`;
- доступны `CRANK_ADMIN_BIND` и `CRANK_MCP_BIND`; - доступны `CRANK_ADMIN_BIND` и `CRANK_MCP_BIND`;
- если нужен заполненный demo state без ручного онбординга, задан `CRANK_DEMO_SEED=true`; - если нужен заполненный demo state без ручного онбординга, задан `CRANK_DEMO_SEED=true`;
+4 -13
View File
@@ -165,17 +165,10 @@ services:
- поднимать `postgres`, `admin-api`, `mcp-server`, `ui`; - поднимать `postgres`, `admin-api`, `mcp-server`, `ui`;
- использовать env vars вместо hardcoded config; - использовать env vars вместо hardcoded config;
- иметь persistent storage для БД и artifacts; - иметь persistent storage для БД и artifacts;
- поддерживать настраиваемый bind host для published ports; - публиковать `ui`, `admin-api` и `mcp-server` на loopback по умолчанию;
- публиковать `ui`, `admin-api` и `mcp-server` на loopback, если reverse proxy живет на том же хосте;
- публиковать `ui`, `admin-api` и `mcp-server` на LAN bind address или `0.0.0.0`, если reverse proxy вынесен на отдельный хост;
- не публиковать внутренние сервисы наружу без необходимости; - не публиковать внутренние сервисы наружу без необходимости;
- содержать healthchecks. - содержать healthchecks.
Практически это должно быть управляемо через env вроде `CRANK_PUBLISH_HOST`:
- `127.0.0.1` для local reverse proxy на том же Linux host;
- конкретный внутренний IP или `0.0.0.0` для отдельного центрального reverse proxy в LAN.
## 10. Healthchecks ## 10. Healthchecks
Нужны как минимум: Нужны как минимум:
@@ -238,20 +231,18 @@ CD для MVP должен:
Workflow собирает `.env` на сервере из отдельных GitHub secrets. Для runtime-переменных нужно Workflow собирает `.env` на сервере из отдельных GitHub secrets. Для runtime-переменных нужно
создавать secrets с теми же именами, что и у приложений: создавать secrets с теми же именами, что и у приложений:
- `POSTGRES_HOST`
- `POSTGRES_PORT`
- `POSTGRES_DB` - `POSTGRES_DB`
- `POSTGRES_USER` - `POSTGRES_USER`
- `POSTGRES_PASSWORD` - `POSTGRES_PASSWORD`
- `CRANK_DATABASE_URL`
- `CRANK_STORAGE_ROOT` - `CRANK_STORAGE_ROOT`
- `CRANK_PUBLISH_HOST`
- `CRANK_ADMIN_BIND` - `CRANK_ADMIN_BIND`
- `CRANK_MCP_BIND` - `CRANK_MCP_BIND`
- `CRANK_MCP_REFRESH_MS` - `CRANK_MCP_REFRESH_MS`
- `CRANK_LOG_LEVEL` - `CRANK_LOG_LEVEL`
- `CRANK_SECRET_PROVIDER`
- `CRANK_MASTER_KEY` - `CRANK_MASTER_KEY`
- `CRANK_PUBLIC_BASE_URL` - `CRANK_BASE_URL`
- `CRANK_MCP_PUBLIC_URL`
- `CRANK_SESSION_SECRET` - `CRANK_SESSION_SECRET`
- `CRANK_PASSWORD_PEPPER` - `CRANK_PASSWORD_PEPPER`
- `CRANK_SESSION_TTL_HOURS` - `CRANK_SESSION_TTL_HOURS`
+17 -4
View File
@@ -55,7 +55,11 @@ var/crank/
Минимально ожидаются: Минимально ожидаются:
- `CRANK_DATABASE_URL` - `POSTGRES_HOST`
- `POSTGRES_PORT`
- `POSTGRES_DB`
- `POSTGRES_USER`
- `POSTGRES_PASSWORD`
- `CRANK_ADMIN_API_IMAGE` - `CRANK_ADMIN_API_IMAGE`
- `CRANK_MCP_SERVER_IMAGE` - `CRANK_MCP_SERVER_IMAGE`
- `CRANK_UI_IMAGE` - `CRANK_UI_IMAGE`
@@ -64,10 +68,8 @@ var/crank/
- `CRANK_MCP_BIND` - `CRANK_MCP_BIND`
- `CRANK_MCP_REFRESH_MS` - `CRANK_MCP_REFRESH_MS`
- `CRANK_LOG_LEVEL` - `CRANK_LOG_LEVEL`
- `CRANK_SECRET_PROVIDER`
- `CRANK_MASTER_KEY` - `CRANK_MASTER_KEY`
- `CRANK_PUBLIC_BASE_URL` - `CRANK_BASE_URL`
- `CRANK_MCP_PUBLIC_URL`
Опционально: Опционально:
@@ -151,6 +153,17 @@ runtime env-переменной. Это значит, что:
- `CRANK_DEMO_SEED` хранится как GitHub secret `CRANK_DEMO_SEED`; - `CRANK_DEMO_SEED` хранится как GitHub secret `CRANK_DEMO_SEED`;
- и так же для остальных runtime-переменных. - и так же для остальных runtime-переменных.
Для БД основной runtime-контракт теперь компонентный:
- `POSTGRES_HOST`
- `POSTGRES_PORT`
- `POSTGRES_DB`
- `POSTGRES_USER`
- `POSTGRES_PASSWORD`
`CRANK_DATABASE_URL` допускается только как backward-compatible fallback для локальных тестов и
переходного периода, но не как основная deployment-модель.
## 8.1. Delivery artifacts ## 8.1. Delivery artifacts
Для production-like запуска проект должен поставляться с: Для production-like запуска проект должен поставляться с:
+1 -1
View File
@@ -14,7 +14,7 @@
- `auth_profile_ref` в `ExecutionConfig`; - `auth_profile_ref` в `ExecutionConfig`;
- CRUD endpoints для `auth_profiles`; - CRUD endpoints для `auth_profiles`;
- UI поля для upstream auth headers; - UI поля для upstream auth headers;
- env `CRANK_SECRET_PROVIDER` и `CRANK_MASTER_KEY` в runtime docs. - env `CRANK_MASTER_KEY` в runtime docs.
Что еще не работает end-to-end: Что еще не работает end-to-end: