Install
Self-host Bloom with Docker Compose, Cloudron, or Platinity / K3s, then back it up and upgrade it.
Bloom is EmbedLabs’ product lifecycle management app. It runs from ghcr.io/mbedlabs/bloom with PostgreSQL. Use this Docker Compose stack:
Requirements
Section titled “Requirements”- Docker Engine with Docker Compose v2
1. Create the compose file
Section titled “1. Create the compose file”# docker-compose.ymlservices: bloom: image: ghcr.io/mbedlabs/bloom:${BLOOM_VERSION:-stable} env_file: - .env environment: DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} BLOOM_ATTACHMENT_DIR: /app/attachments # Bloom migrates on start, then launches the app command: ["/bin/sh", "-c", "gosu appuser alembic upgrade head && exec /usr/local/bin/start-product"] depends_on: postgres: condition: service_healthy ports: - "8000:8080" volumes: - bloom-attachments:/app/attachments
postgres: image: postgres:16 environment: POSTGRES_USER: ${POSTGRES_USER:-bloom} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB:-bloom} healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-bloom} -d ${POSTGRES_DB:-bloom}"] interval: 10s timeout: 5s retries: 5 volumes: - bloom-postgres-data:/var/lib/postgresql/data
volumes: bloom-postgres-data: bloom-attachments:The compose file exposes Bloom on host port 8000. To use another port, change the left side of 8000:8080 and use the same port in APP_BASE_URL and BLOOM_APP_URL.
2. Create the .env
Section titled “2. Create the .env”Generate independent secrets, one per value, for example with openssl rand:
openssl rand -hex 32openssl rand -hex 32openssl rand -hex 24Then write the .env:
# .envBLOOM_ENV=production
# stable follows tested non-prerelease version tags, independently of GitHub Releases.BLOOM_VERSION=stable
POSTGRES_USER=bloomPOSTGRES_PASSWORD=replace-with-a-strong-database-passwordPOSTGRES_DB=bloomSECRET_KEY=replace-with-a-strong-random-secret
ADMIN_EMAIL=you@your-company.comADMIN_PASSWORD=replace-with-a-strong-admin-passwordADMIN_FULL_NAME=Bloom AdminAUTO_SEED_ADMIN=false
# Your public Bloom URL, used in the links Bloom emails (invites, verification, resets)APP_BASE_URL=https://bloom.example.comSet at least POSTGRES_PASSWORD, SECRET_KEY, ADMIN_EMAIL, ADMIN_PASSWORD, ADMIN_FULL_NAME, APP_BASE_URL and BLOOM_APP_URL. BLOOM_APP_URL is covered in Connecting to Bud. In production, Bloom rejects a missing or weak signing key and requires an administrator password of at least 16 characters.
Keep RUN_STARTUP_DATA_REPAIR=false. The container applies database migrations with Alembic before starting the application.
Optional features use independent secrets:
- Set
SERVICE_TOKEN_PEPPERto a random value of at least 32 characters when Bloom will issue scoped service credentials for Bud. See Connecting to Bud. - Set
INTEGRATION_ENCRYPTION_KEYto a Fernet key when GitHub, GitLab, or Jira synchronization will store credentials. See Issue tracker sync.
Bloom starts without these optional secrets. Only the corresponding integration remains unavailable. Every other setting is in the configuration reference.
3. Start and verify
Section titled “3. Start and verify”# Bloom runs its own migrations on start (see the compose command)docker compose pulldocker compose up -dcurl http://localhost:8000/api/readyFollow startup and migration logs:
docker compose logs -f bloomOpen http://localhost:8000/setup to create the first administrator. As with Bud, production requires a real ADMIN_EMAIL and a password of at least 16 characters even when AUTO_SEED_ADMIN=false; those values do not create an account with seeding disabled. Keep the database and attachment volumes when upgrading.
Bloom answers three health endpoints:
| Endpoint | What it checks |
|---|---|
/api/health |
Liveness. The web process responds. It does not query PostgreSQL. |
/api/ready |
Readiness. Runs SELECT 1 against PostgreSQL and returns 200 only when the database is reachable, 503 otherwise. The image’s own health check probes it, and it is the right target for load balancer and orchestrator readiness checks. |
/api/version |
Reports the running version. |
Image tags
Section titled “Image tags”- Image:
ghcr.io/mbedlabs/bloom, listed as Bloom on GitHub Packages - Platforms:
linux/amd64,linux/arm64 - Container port:
8080 - Default host port:
8000
Set BLOOM_VERSION in .env:
| Tag | Moves? | Use for |
|---|---|---|
1.2.0 (full version) |
Immutable | Production: pin this |
1.2, 1 |
Moving with stable releases | Tracking a minor or major line |
stable |
Moving to the newest stable release | Low-maintenance production |
sha-<commit> |
Immutable | Exact source and image revision |
latest |
Rolling build | Never in production |
Pin a complete version such as 1.2.0 for production.
Cloudron
Section titled “Cloudron”Cloudron uses the same Bloom container image. Follow the repository’s Cloudron installation instructions for the manifest and install command. Cloudron provides the database, runs migrations, and keeps persistent files under /app/data. Create the first administrator at /setup after installation.
On Cloudron, the image’s normal entrypoint:
- maps Cloudron’s PostgreSQL and optional sendmail variables to Bloom;
- runs database migrations before starting the service;
- stores attachments and generated application secrets under
/app/data; - disables automatic admin seeding, so the first administrator is created at
/setup; - exposes Bud through
BUD_APP_URLwhen that variable is configured.
Bloom generates and persists its own application secrets. Before you install, have ready:
- the Cloudron dashboard domain and a Cloudron login or API token;
- the Bloom app location (its domain);
- an immutable image tag or digest;
- the Bud app domain, for the link between the two apps.
Install the manifest with the Bloom container image:
export CLOUDRON_SERVER='<cloudron-dashboard-domain>'export BLOOM_LOCATION='<bloom-app-location>'export IMAGE_TAG='<immutable-version-or-sha>'
cloudron login "$CLOUDRON_SERVER"cloudron install --location "$BLOOM_LOCATION" \ --image "ghcr.io/mbedlabs/bloom:$IMAGE_TAG"Set Bloom’s link to Bud once both locations are known:
cloudron env set --app "$BLOOM_LOCATION" \ BUD_APP_URL="https://bud.example.com"The manifest declares the PostgreSQL and local storage addons and an optional Email (sendmail) addon, uses /api/ready as its health check, sets a 1 GiB memory limit and needs Cloudron 10.0.0 or later.
The first visit shows a setup screen that asks for your name, email address and a password. It stops being reachable the moment the account exists, so nobody else can claim the instance.
Platinity / K3s
Section titled “Platinity / K3s”For a Kubernetes or K3s platform on infrastructure you control, review Platinity. Use the same Bloom container image and keep PostgreSQL, attachments, and application secrets on durable storage managed by that platform.
Data and backups
Section titled “Data and backups”Bloom has two durable stores:
bloom-postgres-datafor PostgreSQL recordsbloom-attachmentsfor document evidence under/app/attachments
BLOOM_ATTACHMENT_DIR identifies the attachment directory. The Compose service sets it to /app/attachments and mounts bloom-attachments there, so replacing or upgrading the application container does not erase uploaded evidence. A non-Compose deployment must mount its own durable storage at the configured path.
Back up PostgreSQL and the attachment volume together, at the same point in time, so attachment rows and files stay consistent. Preserve SECRET_KEY, SERVICE_TOKEN_PEPPER, and INTEGRATION_ENCRYPTION_KEY with the deployment configuration so saved credentials remain usable.
Nightly backup (adjust connection details to your deployment):
# databasepg_dump --format=custom --file="bloom-$(date +%F).dump" "$DATABASE_URL"
# document evidence; use your named-volume or host-volume backup toolingtar czf "bloom-attachments-$(date +%F).tgz" -C /path/to/attachments .Restore:
pg_restore --clean --if-exists --no-owner --dbname "$DATABASE_URL" bloom-YYYY-MM-DD.dumptar xzf bloom-attachments-YYYY-MM-DD.tgz -C /path/to/attachmentsAfter a restore, check /api/ready, then sign in and confirm that projects, requirements, traceability views, and a sample attachment download all work. Test restoration on a separate installation before you depend on it.
Keep SECRET_KEY, SERVICE_TOKEN_PEPPER, INTEGRATION_ENCRYPTION_KEY, and the SMTP credentials in your secret store:
- a different
SECRET_KEYinvalidates sessions and pending invite and reset links; - a different
SERVICE_TOKEN_PEPPERinvalidates the Bud result-sync credentials; - a different
INTEGRATION_ENCRYPTION_KEYmakes configured tracker secrets unreadable until they are rotated.
Your recovery point equals your backup cadence: nightly dumps mean up to 24 hours of loss. Copy backups off the host.
Upgrade
Section titled “Upgrade”Pull the chosen image, retain the volumes, and let the Compose startup command apply migrations before checking /api/ready.
-
Read the release notes.
-
Back up
bloom-postgres-data,bloom-attachments, and.env. -
Set the target
BLOOM_VERSION. -
Pull and restart:
docker compose pulldocker compose up -d -
Verify
/api/versionand/api/ready.
To roll back a release that changed the database schema, restore the pre-upgrade backup and start the previous image tag. Database downgrades are not supported. Never run a newer schema against an older application version.



