Lyra is a web application designed to manage GPU-enabled build environments and containers effortlessly.
Server deployment guide: [English / 한국어]
- Docker & Docker Compose
- Python 3.11+ (for local development)
- Node.js 20+ (for local development)
Before running compose, create .env:
cp .env.sample .envAt minimum, set secure values for:
APP_SECRET_KEY(valid Fernet key, base64-encoded 32-byte key)POSTGRES_PASSWORD
Default (CPU-only):
docker-compose up -d --buildWith GPU Support (NVIDIA):
docker-compose -f docker-compose.gpu.yml up -d --buildWorker Node Only (no frontend):
docker compose -f docker-compose.worker.yml up -d --buildWorker Node Only + GPU:
docker compose -f docker-compose.worker.gpu.yml up -d --build- Frontend: http://localhost
- Backend API: http://localhost:8000
- API Docs: http://localhost:8000/docs
Use this section when deploying to a server (IP/domain), not localhost-only development.
- Create environment file:
cp .env.sample .env- Set required
.envvalues:
POSTGRES_USER=postgres
POSTGRES_PASSWORD=CHANGE_THIS_DB_PASSWORD
POSTGRES_DB=lyra
DATABASE_URL=postgresql+asyncpg://postgres:CHANGE_THIS_DB_PASSWORD@db/lyra
CELERY_BROKER_URL=redis://redis:6379/0
CELERY_RESULT_BACKEND=redis://redis:6379/0
APP_SECRET_KEY=REPLACE_WITH_VALID_FERNET_KEY
ALLOW_ORIGINS=http://YOUR_SERVER_IP,https://YOUR_DOMAIN
SSH_HOST_KEY_POLICY=reject
SSH_KNOWN_HOSTS_PATH=/root/.ssh/known_hostsAPP_SECRET_KEYis required for root password encryption/decryption.POSTGRES_PASSWORDandDATABASE_URLcredentials must match.- If
POSTGRES_USERorPOSTGRES_DBis changed, updateDATABASE_URLaccordingly. ALLOW_ORIGINSmust include the exact browser origin(s) that will access Lyra.- If users open
http://<server-ip>, includehttp://<server-ip>. - If users open
https://example.com, includehttps://example.com.
- If users open
- If external DB/Redis access is not needed, do not expose
5432/6379outside trusted networks.
- Run deployment:
docker compose up -d --buildGPU hosts:
docker compose -f docker-compose.gpu.yml up -d --buildWorker-only hosts:
docker compose -f docker-compose.worker.yml up -d --build
# or (GPU)
docker compose -f docker-compose.worker.gpu.yml up -d --buildWorker node required env:
LYRA_NODE_ROLE=worker
Worker token policy:
- When worker backend starts, Lyra generates a runtime worker API token and prints it in logs as plaintext.
- Main server must use that token value when registering the worker server.
- Token is persisted in Docker named volume
worker_runtime_dataand reused across worker restarts. - If you remove
worker_runtime_data, a new token is generated on next worker startup.
- Verify containers:
docker compose ps.
├── backend/ # FastAPI + Celery + SQLAlchemy
│ ├── app/ # Application logic (routers, models, tasks)
│ ├── tests/ # Pytest suite
│ └── Dockerfile # Python environment definition
├── frontend/ # React + Vite + TailwindCSS
│ ├── src/ # Application components and pages
│ └── Dockerfile # Nginx-based frontend deployment
├── docs/ # Project documentation & TODOs
├── .github/workflows/ # GitHub Actions CI/CD pipelines
├── .pre-commit-config.yaml# Local CI check configuration
└── docker-compose.yml # Service orchestration
└── docker-compose.worker.yml / docker-compose.worker.gpu.yml # Worker-only deployment
This project uses pre-commit to ensure code quality locally. To set it up:
pip install pre-commit
pre-commit installNow, every git commit will automatically run linting (flake8, eslint) and tests (pytest).
You can manually run all checks across the codebase:
pre-commit run --all-filesBackend tests (recommended invocation):
cd backend && pytest -qThis project uses Alembic for handling database schema changes.
Create a new migration (after modifying models.py):
docker compose exec backend alembic revision --autogenerate -m "Description of changes"Apply migrations to DB:
docker compose exec backend alembic upgrade headEnvironment now includes optional service flags in API payloads:
enable_jupyter(default:true)enable_code_server(default:true)
PUT /api/settings/{key} only allows direct updates for:
app_namefavicon_data_urldashboard_announcement_markdownssh_portssh_hostssh_usernamessh_auth_methodssh_passwordssh_host_fingerprint
Internal keys are protected and cannot be directly accessed/updated:
jupyter_token:*custom_ports:*
Error policy:
400for invalid or unsupported keys403for protected internal keys
- Backend enforces SSH host key verification for both:
POST /api/terminal/test-sshGET /api/terminal/ws(websocket terminal path)
- Policy mode is controlled by
SSH_HOST_KEY_POLICY:reject(recommended): unknown/untrusted host keys are rejectedaccept-new(development only): unknown keys are auto-accepted
SSH_KNOWN_HOSTS_PATHpoints to the known_hosts file used for trust checks.- If
ssh_host_fingerprintsetting is configured, fingerprint validation is applied before authentication and takes precedence over implicit trust. - Fingerprint formats:
SHA256:<base64>- MD5 fingerprint (
aa:bb:...) also supported for compatibility
SSH_KNOWN_HOSTS_PATH is resolved inside the backend container.
Pre-register target host keys before users open Terminal:
docker compose exec backend sh -lc 'mkdir -p /root/.ssh && ssh-keyscan -H <SSH_HOST> >> /root/.ssh/known_hosts'Example:
docker compose exec backend sh -lc 'mkdir -p /root/.ssh && ssh-keyscan -H host.docker.internal >> /root/.ssh/known_hosts'- Get fingerprint:
ssh-keyscan <SSH_HOST> | ssh-keygen -lf -- Save SHA256 fingerprint to setting key
ssh_host_fingerprint(for example:SHA256:...). - When
ssh_host_fingerprintis set, fingerprint verification is enforced before authentication.
Provisioning Volume Mounts supports both manual host path input and SSH-based directory browsing.
- SSH settings must be configured in
Settings > Host Server Connection. - Required keys:
ssh_hostssh_portssh_usernamessh_auth_methodssh_password(when auth method ispassword)
- Host key trust must be valid for current policy (
SSH_HOST_KEY_POLICY+SSH_KNOWN_HOSTS_PATH/ fingerprint).
Lyra separates failures into two groups:
- Configuration/Connection issues:
- missing SSH settings
- authentication failed
- host key verification failed
- generic connection validation failed
- Host path issues:
- permission denied
- path not found
- generic browse failure
When configuration is missing, Provisioning shows an inline row error with a Go to Settings CTA.
POST /api/filesystem/host/listreturns at most500entries per request.- If more entries exist, response includes
truncated=trueand UI shows a partial-list notice.
- Click
Browsewith SSH settings missing; verify inline warning +Go to Settings. - Configure SSH and verify
Browseopens picker and selecting a directory fillshost_path. - Verify permission denied path error is shown inline.
- Verify non-existent path error is shown inline.
- Verify large directory shows partial-list (
truncated) notice.
Frontend supports en and ko with default language en.
- Language selection is persisted in browser storage key
lyra.language. - Common/static page text uses domain keys such as
settings.*,templates.*,terminal.*. - Toast/error/status/dynamic user messages use
feedback.*.
- Do not render raw server errors directly.
- Use i18n fallback formatting for dynamic API errors:
withApiMessage(t, 'feedback.<domain>.<event>', serverMessage)
- If server message is missing, fallback key
feedback.common.unknownErroris used.
- Add key/value in
frontend/src/i18n/locales/en/common.ts - Add matching key/value in
frontend/src/i18n/locales/ko/common.ts - Replace UI string with
t('...')in page/component code - Run i18n checks + lint/build
npm --prefix frontend run i18n:scan
npm --prefix frontend run i18n:keys
npm --prefix frontend run lint
npm --prefix frontend run build- Provisioning service checkboxes (
JupyterLab,code-server) add/remove managed Dockerfile blocks automatically. - Managed blocks are read-only in the editor. User edits are preserved only in the user-authored Dockerfile area.
- Template save stores only the user-authored Dockerfile area; managed blocks are excluded.
- On environment creation, the final Dockerfile is composed from:
- user-authored Dockerfile content
- currently selected managed service blocks
- Runtime auto-install in worker is disabled for Jupyter and code-server.
- Required tools must be installed at image build time through Dockerfile (including managed blocks).
- Worker starts services conditionally by feature flags:
enable_jupyterenable_code_server
- Jupyter launch API returns
409when Jupyter is disabled for that environment. - Exposed host ports follow enabled services:
- SSH is always exposed
- Jupyter/code-server ports are exposed only when enabled
- Supported app themes:
dark,light - Persisted key:
lyra.theme(browserlocalStorage) - Default theme:
light - Terminal (
xterm) is intentionally fixed to dark for readability/consistency
- Theme state/provider:
frontend/src/context/ThemeContext.tsx
- Global tokens:
frontend/src/index.css- Core tokens:
--bg,--bg-elevated,--surface,--border,--text,--text-muted,--primary,--danger,--success,--overlay - Terminal tokens:
--terminal-bg,--terminal-border
- App-level wiring:
frontend/src/main.tsx(initial theme class apply)frontend/src/App.tsx(provider usage)
- Monaco Editor (
@monaco-editor/react)- Use
vs-darkwhen app theme is dark - Use
vswhen app theme is light - Applied in:
frontend/src/pages/Provisioning.tsxfrontend/src/pages/Templates.tsx
- Use
- xterm
- Always dark theme (not bound to app light/dark toggle)
- Applied in:
frontend/src/pages/TerminalPage.tsx
Run before opening a PR:
npm --prefix frontend run lint
npm --prefix frontend run buildManual checks:
- Toggle
Settings > General > Theme(dark/light) and verify layout colors update on:- Dashboard
- Provisioning
- Settings
- Templates
- Verify Monaco switches with app theme in:
- Provisioning Dockerfile editor
- Templates detail modal editor
- Verify Terminal page remains visually stable with fixed dark terminal area in both app themes.
- Verify key interactions remain unchanged:
- Environment create/start/stop/delete
- Template load/save/delete
- SSH settings save/test
- Verify managed Dockerfile behavior:
- Toggle Jupyter/code-server checkboxes and confirm managed blocks are added/removed in Dockerfile editor.
- Confirm managed block text cannot be edited directly (auto-restored).
- Save template and confirm managed block markers are not included in template Dockerfile content.
- Verify service flag behavior:
- Dashboard Access shows
-per disabled service. - Jupyter launch returns
409when Jupyter is disabled for the environment.
- Dashboard Access shows
