Managing Dependencies
This guide explains how to install Python packages for your Hassette apps when running in Docker.
Overview
Hassette's Docker startup script installs project dependencies automatically and optionally discovers requirements.txt files when enabled. Two methods are available:
- Project-based — using
pyproject.tomlanduv.lock(recommended for complex projects) - Requirements files — using
requirements.txt(simple approach, opt-in)
How Constraints Work
Hassette's Docker image includes a constraints file (/app/constraints.txt) that records the compatible version ranges for all framework dependencies. When you install your own packages, the startup script passes this file to uv, so any dependency that would conflict with hassette's requirements causes a clear error message rather than a silent downgrade. If you see a conflict error, it usually means your uv.lock was generated against a different hassette version than the image — running uv lock locally and committing the result fixes it. See Dependency Conflicts for details.
How the Startup Script Works
When the container starts, the startup script performs these steps in order:
flowchart TD
A[Container Starts]:::neutral --> B[Activate venv\nValidate hassette importable]:::step
B --> C{uv.lock exists?}:::decision
C -->|Yes| D[uv export → /tmp/user-deps.txt]:::step
D --> E[uv pip install -r user-deps.txt\n-c constraints.txt]:::step
E --> F[uv pip install --no-deps project]:::step
C -->|No| G{pyproject.toml exists?}:::decision
G -->|Yes| H[Log: run uv lock to generate a lockfile]:::fallback
G -->|No| I[Skip project install]:::fallback
F --> J{HASSETTE__INSTALL_DEPS=1?}:::decision
H --> J
I --> J
J -->|Yes| K[Discover requirements.txt files via fd\nexact match only]:::step
K --> L[For each: uv pip install -r file\n-c constraints.txt]:::step
L --> N{HASSETTE__PRUNE_UV_CACHE=1?}:::decision
J -->|No| N
N -->|Yes| O[uv cache prune]:::step
O --> M[Start hassette]:::neutral
N -->|No| M
classDef neutral fill:#f0f0f0,stroke:#999,color:#333
classDef step fill:#e8f0ff,stroke:#6688cc,color:#333
classDef decision fill:#f0f8e8,stroke:#88aa66,color:#333
classDef fallback fill:#fff0e8,stroke:#cc8844,color:#333
Key Behaviors
- Export-then-install: When a
uv.lockis found, the startup script exports your resolved dependencies to a temporary requirements file and installs them through the constraints file. This routes all dependency resolution through the constraints file rather than bypassing it. - Opt-in requirements discovery:
requirements.txtfiles are only discovered whenHASSETTE__INSTALL_DEPS=1is set. By default, no requirements files are scanned. - Exact filename match: Only files named exactly
requirements.txtare discovered — notrequirements-dev.txt,requirements_test.txt, or other variants. This prevents dev and test dependencies from being silently installed in the production container. - Constraints protection for all installs: Every
uv pip install— whether from a project lockfile or arequirements.txt— passes-c /app/constraints.txt. Conflicts produce a clear error message before the container exits. - Fail-fast: A failing dependency install exits the container immediately with an actionable message. With
restart: unless-stopped, Docker retries automatically, giving transient network issues a chance to resolve. - Timeouts: All network calls are wrapped with
timeout(300 s for project export/install, 120 s per requirements file). - Cache pruning: After dependency installation, stale uv cache entries are pruned by default. Disable with
HASSETTE__PRUNE_UV_CACHE=0if startup time is critical and you prefer to manage cache size manually.
Understanding APP_DIR vs PROJECT_DIR
These two environment variables serve different purposes:
| Variable | Purpose | Used By |
|---|---|---|
HASSETTE__APPS__DIRECTORY |
Where Hassette looks for .py files containing App/AppSync classes |
Hassette runtime |
HASSETTE__PROJECT_DIR |
Where the startup script looks for pyproject.toml/uv.lock to install dependencies |
Startup script |
Key Distinction
HASSETTE__APPS__DIRECTORY tells Hassette where your code lives. HASSETTE__PROJECT_DIR tells the startup script where your package definition lives. These can be the same directory or different directories depending on your project structure.
Project Structures
Simple Flat Structure
For basic apps where you do not need to import sibling files, use a simple flat structure:
project_dir/
├── docker-compose.yml
├── config/
│ ├── hassette.toml
│ └── .env
└── apps/
├── my_app.py
├── another_app.py
└── requirements.txt # optional
docker-compose.yml:
services:
hassette:
image: ghcr.io/nodejsmith/hassette:latest-py3.13
volumes:
- ./config:/config
- ./apps:/apps
- data:/data
- uv_cache:/uv_cache
# No need to set APP_DIR or PROJECT_DIR - defaults work fine
volumes:
data:
uv_cache:
In this setup:
HASSETTE__APPS__DIRECTORYdefaults to/apps✓HASSETTE__PROJECT_DIRdefaults to/apps✓
Opt-in required for requirements.txt
A requirements.txt in /apps is not installed automatically. You must set HASSETTE__INSTALL_DEPS=1 for the startup script to discover and install it. See Using requirements.txt below.
Traditional src/ Layout
For projects using the standard Python src/ layout:
project_dir/
├── docker-compose.yml
├── config/
│ ├── hassette.toml
│ └── .env
├── pyproject.toml
├── uv.lock
└── src/
└── my_apps/
├── __init__.py
├── app_one.py
└── app_two.py
docker-compose.yml:
services:
hassette:
image: ghcr.io/nodejsmith/hassette:latest-py3.13
volumes:
- ./config:/config
- .:/apps # Mount entire project to /apps
- data:/data
- uv_cache:/uv_cache
environment:
# Startup script finds pyproject.toml/uv.lock at /apps
- HASSETTE__PROJECT_DIR=/apps
# Hassette finds app files in /apps/src/my_apps
- HASSETTE__APPS__DIRECTORY=/apps/src/my_apps
volumes:
data:
uv_cache:
In this setup:
- The project root (containing
pyproject.toml) is mounted to/apps HASSETTE__PROJECT_DIR=/appstells the startup script where to find dependenciesHASSETTE__APPS__DIRECTORY=/apps/src/my_appstells Hassette where to find your app files- Your app files can import from the
my_appspackage normally
Using pyproject.toml
Create a pyproject.toml in your project:
[project]
name = "my-hassette-apps"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"requests>=2.31.0",
"aiohttp>=3.9.0",
"pydantic>=2.0.0",
]
With a Lock File (Required)
Generate a lock file before deploying:
uv lock
git add uv.lock
git commit -m "add uv.lock"
If a uv.lock file exists alongside your pyproject.toml, the startup script uses the export-then-install pattern: it exports your resolved dependencies as a flat requirements list and installs them through the constraints file.
Lock file is required for project-based installs
If your pyproject.toml is present but no uv.lock exists, the startup script logs a message directing you to run uv lock and skips the project install. If you can't run uv locally, use the requirements.txt path with HASSETTE__INSTALL_DEPS=1 instead.
Using requirements.txt
For simpler setups, place a requirements.txt file in /config or /apps:
apps/
├── my_app.py
└── requirements.txt
apps/requirements.txt:
requests>=2.31.0
aiohttp>=3.9.0
Opt-in required
Requirements file discovery is disabled by default. Set HASSETTE__INSTALL_DEPS=1 in your compose environment to enable it.
environment:
- HASSETTE__INSTALL_DEPS=1
The startup script uses fd to find files named exactly requirements.txt in both /config and /apps (up to 5 directory levels deep), then installs them in sorted path order with constraints applied.
Exact filename match only
Only files named exactly requirements.txt are discovered. Files named requirements-dev.txt, requirements_test.txt, or any other variant are ignored. If you need multiple files, use the project-based install with pyproject.toml + uv.lock.
Startup Performance
Using uv.lock for Faster Starts
The uv_cache Docker volume caches downloaded packages. Combined with uv.lock, this makes subsequent container starts very fast because packages that are already cached don't need to be re-downloaded:
volumes:
- uv_cache:/uv_cache # Persist package cache
Pre-building a Custom Image
For the fastest startup times, build a custom image with your dependencies pre-installed. Use the export-then-install pattern so the constraints file is still enforced:
FROM ghcr.io/nodejsmith/hassette:latest-py3.13
# Copy your project files
COPY pyproject.toml uv.lock /project/
# Export resolved deps as a flat requirements list
RUN uv export \
--no-hashes --frozen \
--directory /project \
--no-default-groups \
--no-dev --no-editable --no-emit-project \
--output-file /tmp/user-deps.txt
# Install through constraints
RUN uv pip install -r /tmp/user-deps.txt -c /app/constraints.txt
# Install the project package itself (no dep resolution)
RUN uv pip install --no-deps /project
RUN rm /tmp/user-deps.txt
Then in docker-compose.yml:
services:
hassette:
build: .
volumes:
- ./apps:/apps/src/my_apps # Just mount app code
- ./config:/config
Known Limitations
Local Path Dependencies
User projects with local path dependencies (e.g., foo = { path = "../shared-lib" }) will fail during the export step because uv export emits file:///absolute/path references that don't resolve inside the container. If your project uses monorepo-style local deps, use the custom image build pattern above — copy all relevant packages into the image at build time and install them before deploying.
Complete Examples
Example 1: Simple Flat Structure
services:
hassette:
image: ghcr.io/nodejsmith/hassette:latest-py3.13
volumes:
- ./config:/config
- ./apps:/apps
- data:/data
- uv_cache:/uv_cache
environment:
- TZ=America/New_York
- HASSETTE__INSTALL_DEPS=1
volumes:
data:
uv_cache:
requests>=2.31.0
Example 2: src/ Layout with Lock File
services:
hassette:
image: ghcr.io/nodejsmith/hassette:latest-py3.13
volumes:
- ./config:/config
- .:/apps
- data:/data
- uv_cache:/uv_cache
environment:
- HASSETTE__PROJECT_DIR=/apps
- HASSETTE__APPS__DIRECTORY=/apps/src/my_apps
- TZ=America/New_York
volumes:
data:
uv_cache:
[project]
name = "my-hassette-apps"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"requests>=2.31.0",
"aiohttp>=3.9.0",
]
See Also
- Docker Overview — Quick start guide
- Troubleshooting — Common issues and solutions