How to install Docassemble on Cloudron as a custom application
-
We did it!
It was a long hard slog, but managed to get Docassemble up and running. https://docassemble.org/Docassemble on Cloudron: Working Custom Package + Packaging Guide
We have successfully packaged docassemble as a custom Cloudron app. It is working: interviews run, documents generate, the Playground is functional, and data persists across restarts. The source is available at [your-repo-url] and we welcome testing, feedback, and contributions.
This post is structured for three audiences:
- Deployers who want to run docassemble on their Cloudron today
- Packagers who might want to make this an officially supported Cloudron app
- Cloudron maintainers who could improve the platform to make apps like this easier
What is docassemble and why would you want it?
Docassemble is a free, open-source platform for building guided interviews that generate documents. You define questions in YAML, write logic in Python, and it produces a mobile-friendly web application that walks users through a process step by step, then assembles customised PDFs, DOCX files, or other outputs.
It is the leading open-source tool in its niche. Courts, legal aid organisations, government agencies, and NGOs use it for everything from benefits applications to tenancy agreements to immigration forms. The commercial alternatives (HotDocs, Documate, Lawyaw) cost thousands per year and lock you into their cloud.
What makes it worth the packaging effort:
- Genuinely smart conditional logic. Not just yes/no branching. The Python engine evaluates complex conditions, queries external APIs mid-interview, and adapts the flow dynamically.
- Document assembly that handles real-world messiness. PDF form filling, DOCX generation from Jinja2 templates, touchscreen signatures, OCR, multilingual support.
- Browser-based development. The Playground lets you write, test, and package interviews without any local tooling.
- Self-hosted and MIT licensed. Your data stays on your server. No per-user fees. No SaaS dependency. Critical for legal, healthcare, and government contexts.
Part 1: Deploying docassemble on Cloudron (for users)
Prerequisites
- A Cloudron instance with at least 12 GB total RAM (docassemble needs ~8 GB allocated to it)
- Cloudron CLI installed and logged in
- A domain or subdomain configured in Cloudron
- About 45 minutes for the first deployment (most of this is waiting for builds and initialization)
Step 1: Get the package files (~2 minutes)
Clone the repository:
git clone [your-repo-url] docassemble-cloudron cd docassemble-cloudronStep 2: Install the app (~10-15 minutes)
cloudron install --location docassemble.yourdomain.comThe build takes 10-15 minutes. The Cloudron build service downloads the ~4 GB upstream Docker image, removes RabbitMQ, installs LavinMQ, and prepares seed directories.
Gotcha: Build timeout. If the build times out on the Cloudron build service, you can build locally instead:
docker build -t your-registry/docassemble-cloudron:0.1 . docker push your-registry/docassemble-cloudron:0.1 cloudron install --image your-registry/docassemble-cloudron:0.1 --location docassemble.yourdomain.comStep 3: Set the memory limit (~1 minute)
This step is critical. Immediately after installation, go to your Cloudron dashboard, find the docassemble app, open its settings, and set the memory limit to at least 8 GB (8192 MiB).
Gotcha: Manifest memory limit is ignored. Cloudron does not honour the
memoryLimitfield inCloudronManifest.jsonfor custom (unpublished) apps. The default is 256 MB, which is not enough to load Python's import chain. You must set it manually in the dashboard. The app will restart automatically after you change it.Step 4: Wait for first boot (~15-20 minutes)
First boot is slow. The app needs to:
- Copy the ~2 GB Python virtual environment to persistent storage (~3-5 minutes)
- Run database migrations and populate package tables (~2-3 minutes)
- Generate nginx and application configuration (~1 minute)
- Start all services (nginx, uWSGI, Celery, WebSockets) (~2-3 minutes)
You can monitor progress:
cloudron logs -f --app docassemble.yourdomain.comLook for
Finished initializingin the logs. After that, services will start one by one. When you see celery, nginx, and uwsgi all running, the app is ready.Gotcha: "Not responding" during init. The Cloudron dashboard may show "not responding" for up to 20 minutes during first boot. This is normal. Do not restart the app during this time.
Step 5: Log in (~1 minute)
Visit
https://docassemble.yourdomain.com. The default admin account is:- Email:
admin@example.com - Password:
password
Change this password immediately after logging in. Go to User List from the menu, click Edit next to the admin account, and change both the email and password.
Step 6: Verify it works (~5 minutes)
- Go to Playground from the menu
- Paste this test interview:
question: | Hello from Cloudron! subquestion: | Docassemble is running at ${ url_of('root') } mandatory: True- Click Save and Run. If the interview loads, the core engine works.
- Go to Configuration from the menu and verify it shows your PostgreSQL and Redis connection details.
- Go to Logs and confirm entries are visible.
Subsequent boots (~2-3 minutes)
After the first boot, restarts are much faster because the database tables already exist and the virtual environment is already copied. Expect 2-3 minutes for the app to become responsive after a restart.
Part 2: How the package works (for packagers)
Docassemble is one of the most complex applications you can package for Cloudron. The upstream Docker image is a monolith running ~10 services under supervisord. Here is how we solved each major challenge.
Architecture
Cloudron reverse proxy (TLS) | v nginx:8080 --> uWSGI (Flask/Python app) | +--> Cloudron PostgreSQL addon (external) +--> Cloudron Redis addon (external) +--> LavinMQ localhost:5672 (internal, replaces RabbitMQ) +--> Celery workers (2 processes, 2 queues) +--> WebSocket server (live help) +--> cron (scheduled tasks)Challenge 1: Read-only filesystem
Cloudron locks everything except
/app/data,/tmp, and/run. Docassemble writes to approximately 25 different filesystem paths during initialization and normal operation.Solution: Two categories of symlinks.
Persistent paths (survive restarts, backed up by Cloudron) are symlinked to
/app/data/:/usr/share/docassemble/config→/app/data/config/usr/share/docassemble/files→/app/data/files/usr/share/docassemble/log→/app/data/log/usr/share/docassemble/backup→/app/data/backup/usr/share/docassemble/certs→/app/data/certs/usr/share/docassemble/cron→/app/data/cron/usr/share/docassemble/local3.12(Python venv) →/app/data/venv
Volatile paths (regenerated every boot) are symlinked to
/run/:/etc/nginx/sites-availableand/etc/nginx/sites-enabled/var/log/nginx,/var/log/supervisor,/var/log/apache2/etc/ssl,/var/www,/var/cache/debconf/etc/localtime,/etc/timezone,/etc/locale.gen,/etc/locale.conf/var/lib/nginx,/var/lib/postgresql/etc/cron.daily,/etc/hasbeeninitialized
The Dockerfile creates all symlinks at build time. The
start.shentrypoint recreates volatile directories and seeds them from saved copies on every boot.Gotcha: Python's shutil.rmtree() refuses symlinks. Docassemble's
install_certs.pycallsshutil.rmtree()on/var/www/.certsand/etc/ssl/docassemble. If these are symlinks, Python raisesOSError: Cannot call rmtree on a symbolic link. The fix is to symlink the entire parent directory (/var/www→/run/www,/etc/ssl→/run/ssl) and seed them as real directories from saved copies at boot time.Gotcha: nginx temp directory. Nginx needs writable temp directories at
/var/lib/nginx/body,/var/lib/nginx/proxy, etc. Symlink/var/lib/nginx→/run/nginx-liband create the subdirectories instart.sh.Challenge 2: Replacing RabbitMQ
Docassemble uses RabbitMQ as the Celery message broker. Cloudron has no RabbitMQ addon, and RabbitMQ uses ~200 MB of RAM at rest (Erlang overhead).
Solution: LavinMQ. LavinMQ is an AMQP 0.9.1 wire-compatible message broker written in Crystal. It is a drop-in replacement: same protocol, same broker URL format (
amqp://guest:guest@localhost:5672), same Celery compatibility. Memory usage is ~40 MB at rest.The Dockerfile removes RabbitMQ and Erlang, then installs LavinMQ from the official apt repository. The
start.shscript starts LavinMQ before supervisord and waits for it to accept connections.Installation in the Dockerfile:
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ apt-get remove -y rabbitmq-server erlang-base erlang-nox || true && \ apt-get autoremove -y && \ curl -fsSL https://packagecloud.io/cloudamqp/lavinmq/gpgkey | \ gpg --dearmor -o /usr/share/keyrings/lavinmq.gpg && \ echo "deb [signed-by=/usr/share/keyrings/lavinmq.gpg] https://packagecloud.io/cloudamqp/lavinmq/ubuntu noble main" \ > /etc/apt/sources.list.d/lavinmq.list && \ apt-get update && \ apt-get install -y lavinmq && \ apt-get clean && \ rm -rf /var/lib/apt/lists/*Challenge 3: External PostgreSQL and Redis
Cloudron provides managed PostgreSQL and Redis as addons. Docassemble supports external services via environment variables and config.yml.
Solution: Map Cloudron env vars to docassemble env vars in start.sh, then generate config.yml on every boot.
Key mappings:
CLOUDRON_POSTGRESQL_HOST→DBHOSTCLOUDRON_POSTGRESQL_DATABASE→DBNAMECLOUDRON_POSTGRESQL_USERNAME→DBUSERCLOUDRON_POSTGRESQL_PASSWORD→DBPASSWORDCLOUDRON_REDIS_*→redis:directive in config.ymlCLOUDRON_APP_DOMAIN→DAHOSTNAMEBEHINDHTTPSLOADBALANCER=true(always)PORT=8080(matches manifest httpPort)
Gotcha: CONTAINERROLE must not be "all". Setting
CONTAINERROLE=alltellsinitialize.shto start internal PostgreSQL and Redis via supervisorctl, regardless of supervisor's autostart setting. SetCONTAINERROLE=web:celery:cron:log:mailto skip the sql, redis, and rabbitmq roles.Gotcha: Cloudron PostgreSQL does not use SSL. Do not set
ssl mode: requirein the database configuration. Cloudron's internal PostgreSQL rejects SSL connections.Gotcha: pg_hba.conf errors are non-fatal. Initialize.sh tries to connect to the
postgressystem database to check if the app database exists. Cloudron only allows connections to the app-specific database, so this check fails. It is harmless: the script falls through and connects to the correct database.Challenge 4: Supervisor overrides
We need to disable services that Cloudron provides or that we have replaced.
Solution: A supervisor conf file that sets
autostart=falsefor postgres, redis, rabbitmq, apache2, and syslogng.Gotcha: Must COPY in Dockerfile, not at runtime.
/etc/supervisor/conf.d/is read-only at runtime. The override must be placed there during the Docker build.Challenge 5: Python virtual environment persistence
Docassemble writes to its Python virtual environment at runtime (Playground package installs, pip config changes). The venv is ~2 GB.
Solution: Save the venv as a seed in the Docker image, then copy it to
/app/data/venvon first boot. The Dockerfile copies the venv to/app/code/seed-venv, then symlinks/usr/share/docassemble/local3.12→/app/data/venv. On first boot,start.shcopies the seed to persistent storage.Gotcha: Build service timeout. The 2 GB venv copy can time out on Cloudron's remote build service. If this happens, build locally with
docker buildand push to a registry.Challenge 6: config.yml regeneration
Cloudron may change environment variables between restarts (domain changes, credential rotation). Config.yml is regenerated from environment variables on every boot.
The
secretkey(used for encrypting session data) is generated once and persisted at/app/data/config/.secretso sessions survive restarts.
Part 3: Suggestions for Cloudron maintainers
Several platform improvements would make packaging complex apps like docassemble significantly easier.
1. Honour memoryLimit in manifests for custom apps
Currently, the
memoryLimitfield inCloudronManifest.jsonis ignored for custom (unpublished) apps. The default is 256 MB, which is insufficient for any substantial Python application. Users must manually set memory in the dashboard after installation, which is an undiscoverable step that causes confusing failures.Suggestion: Apply the manifest's
memoryLimitduring installation, or at minimum, show a warning if the default is being used despite a higher value in the manifest.2. Provide a LavinMQ addon
LavinMQ is a lightweight, AMQP 0.9.1 compatible message broker that could serve any app currently needing RabbitMQ. At ~40 MB RAM and a single binary, it is a natural fit for the addon model. A managed LavinMQ addon would simplify packaging for apps that use Celery, Sidekiq, or any AMQP-based task queue.
There is a separate forum request for LavinMQ on Cloudron.
3. Configurable stop timeout per app
Docassemble's upstream Docker setup uses
--stop-timeout 600for graceful shutdown. Cloudron's default stop timeout is shorter. A per-app configurable stop timeout (in the manifest or dashboard) would help apps that need time to flush state.4. Writable /etc subdirectories as an opt-in manifest option
Many complex apps write to
/etc/subdirectories during initialization (timezone, locale, SSL, cron). Currently, packagers must individually symlink each path. A manifest option like"writableEtcDirs": ["/etc/nginx", "/etc/ssl", "/etc/cron.daily"]would reduce boilerplate significantly.5. Build service timeout for large images
The Cloudron build service can time out when Dockerfile steps involve copying large directories (2 GB+ in our case). A configurable build timeout or a progress-aware timeout (that resets when output is being produced) would help.
Known limitations (v0.1)
- Memory must be set manually to ~8 GB in the Cloudron dashboard after installation
- First boot takes 15-20 minutes (database migration, venv copy)
- Exim4 (email receiving) does not work (read-only /etc/exim4). Outbound email via Cloudron's sendmail addon works fine.
- The
initializesupervisor process hangs after completing. Non-blocking but wastes some memory. - Playground package installs persist across restarts but not across Cloudron updates (the venv is re-seeded from the image on update)
- No OIDC/LDAP integration with Cloudron user management yet
Repository
https://github.com/OrcVole/docassemble-cloudron
Contains:
Dockerfile,start.sh,CloudronManifest.json,supervisor/docassemble-cloudron.conf,DESCRIPTION.md,icon.png, documentation.Tested on Cloudron 9.x with the
jhpyle/docassemble:latestimage (v1.8.17, Ubuntu 24.04, Python 3.12).Feedback, bug reports, and pull requests welcome.
LoudLemur said:
We did it!
It was a long hard slog, but managed to get Docassemble up and running. https://docassemble.org/Docassemble on Cloudron: Working Custom Package + Packaging Guide
We have successfully packaged docassemble as a custom Cloudron app. It is working: interviews run, documents generate, the Playground is functional, and data persists across restarts. The source is available at https://github.com/OrcVole/docassemble-cloudron and we welcome testing, feedback, and contributions.
This post is structured for three audiences:
- Deployers who want to run docassemble on their Cloudron today
- Packagers who might want to make this an officially supported Cloudron app
- Cloudron maintainers who could improve the platform to make apps like this easier
What is docassemble and why would you want it?
Docassemble is a free, open-source platform for building guided interviews that generate documents. You define questions in YAML, write logic in Python, and it produces a mobile-friendly web application that walks users through a process step by step, then assembles customised PDFs, DOCX files, or other outputs.
It is the leading open-source tool in its niche. Courts, legal aid organisations, government agencies, and NGOs use it for everything from benefits applications to tenancy agreements to immigration forms. The commercial alternatives (HotDocs, Documate, Lawyaw) cost thousands per year and lock you into their cloud.
What makes it worth the packaging effort:
- Genuinely smart conditional logic. Not just yes/no branching. The Python engine evaluates complex conditions, queries external APIs mid-interview, and adapts the flow dynamically.
- Document assembly that handles real-world messiness. PDF form filling, DOCX generation from Jinja2 templates, touchscreen signatures, OCR, multilingual support.
- Browser-based development. The Playground lets you write, test, and package interviews without any local tooling.
- Self-hosted and MIT licensed. Your data stays on your server. No per-user fees. No SaaS dependency. Critical for legal, healthcare, and government contexts.
Part 1: Deploying docassemble on Cloudron (for users)
Prerequisites
- A Cloudron instance with at least 12 GB total RAM (docassemble needs ~8 GB allocated to it)
- Cloudron CLI installed and logged in
- A domain or subdomain configured in Cloudron
- About 45 minutes for the first deployment (most of this is waiting for builds and initialization)
Step 1: Get the package files (~2 minutes)
Clone the repository:
git clone https://github.com/OrcVole/docassemble-cloudron cd docassemble-cloudronStep 2: Install the app (~10-15 minutes)
cloudron install --location docassemble.yourdomain.comThe build takes 10-15 minutes. The Cloudron build service downloads the ~4 GB upstream Docker image, removes RabbitMQ, installs LavinMQ, and prepares seed directories.
Gotcha: Build timeout. If the build times out on the Cloudron build service, you can build locally instead:
docker build -t your-registry/docassemble-cloudron:0.1 . docker push your-registry/docassemble-cloudron:0.1 cloudron install --image your-registry/docassemble-cloudron:0.1 --location docassemble.yourdomain.comStep 3: Set the memory limit (~1 minute)
This step is critical. Immediately after installation, go to your Cloudron dashboard, find the docassemble app, open its settings, and set the memory limit to at least 8 GB (8192 MiB).
Gotcha: Manifest memory limit is ignored. Cloudron does not honour the
memoryLimitfield inCloudronManifest.jsonfor custom (unpublished) apps. The default is 256 MB, which is not enough to load Python's import chain. You must set it manually in the dashboard. The app will restart automatically after you change it.Step 4: Wait for first boot (~15-20 minutes)
First boot is slow. The app needs to:
- Copy the ~2 GB Python virtual environment to persistent storage (~3-5 minutes)
- Run database migrations and populate package tables (~2-3 minutes)
- Generate nginx and application configuration (~1 minute)
- Start all services (nginx, uWSGI, Celery, WebSockets) (~2-3 minutes)
You can monitor progress:
cloudron logs -f --app docassemble.yourdomain.comLook for
Finished initializingin the logs. After that, services will start one by one. When you see celery, nginx, and uwsgi all running, the app is ready.Gotcha: "Not responding" during init. The Cloudron dashboard may show "not responding" for up to 20 minutes during first boot. This is normal. Do not restart the app during this time.
Step 5: Log in (~1 minute)
Visit
https://docassemble.yourdomain.com. The default admin account is:- Email:
admin@example.com - Password:
password
Change this password immediately after logging in. Go to User List from the menu, click Edit next to the admin account, and change both the email and password.
Step 6: Verify it works (~5 minutes)
- Go to Playground from the menu
- Paste this test interview:
question: | Hello from Cloudron! subquestion: | Docassemble is running at ${ url_of('root') } mandatory: True- Click Save and Run. If the interview loads, the core engine works.
- Go to Configuration from the menu and verify it shows your PostgreSQL and Redis connection details.
- Go to Logs and confirm entries are visible.
Subsequent boots (~2-3 minutes)
After the first boot, restarts are much faster because the database tables already exist and the virtual environment is already copied. Expect 2-3 minutes for the app to become responsive after a restart.
Part 2: How the package works (for packagers)
Docassemble is one of the most complex applications you can package for Cloudron. The upstream Docker image is a monolith running ~10 services under supervisord. Here is how we solved each major challenge.
Architecture
Cloudron reverse proxy (TLS) | v nginx:8080 --> uWSGI (Flask/Python app) | +--> Cloudron PostgreSQL addon (external) +--> Cloudron Redis addon (external) +--> LavinMQ localhost:5672 (internal, replaces RabbitMQ) +--> Celery workers (2 processes, 2 queues) +--> WebSocket server (live help) +--> cron (scheduled tasks)Challenge 1: Read-only filesystem
Cloudron locks everything except
/app/data,/tmp, and/run. Docassemble writes to approximately 25 different filesystem paths during initialization and normal operation.Solution: Two categories of symlinks.
Persistent paths (survive restarts, backed up by Cloudron) are symlinked to
/app/data/:/usr/share/docassemble/config→/app/data/config/usr/share/docassemble/files→/app/data/files/usr/share/docassemble/log→/app/data/log/usr/share/docassemble/backup→/app/data/backup/usr/share/docassemble/certs→/app/data/certs/usr/share/docassemble/cron→/app/data/cron/usr/share/docassemble/local3.12(Python venv) →/app/data/venv
Volatile paths (regenerated every boot) are symlinked to
/run/:/etc/nginx/sites-availableand/etc/nginx/sites-enabled/var/log/nginx,/var/log/supervisor,/var/log/apache2/etc/ssl,/var/www,/var/cache/debconf/etc/localtime,/etc/timezone,/etc/locale.gen,/etc/locale.conf/var/lib/nginx,/var/lib/postgresql/etc/cron.daily,/etc/hasbeeninitialized
The Dockerfile creates all symlinks at build time. The
start.shentrypoint recreates volatile directories and seeds them from saved copies on every boot.Gotcha: Python's shutil.rmtree() refuses symlinks. Docassemble's
install_certs.pycallsshutil.rmtree()on/var/www/.certsand/etc/ssl/docassemble. If these are symlinks, Python raisesOSError: Cannot call rmtree on a symbolic link. The fix is to symlink the entire parent directory (/var/www→/run/www,/etc/ssl→/run/ssl) and seed them as real directories from saved copies at boot time.Gotcha: nginx temp directory. Nginx needs writable temp directories at
/var/lib/nginx/body,/var/lib/nginx/proxy, etc. Symlink/var/lib/nginx→/run/nginx-liband create the subdirectories instart.sh.Challenge 2: Replacing RabbitMQ
Docassemble uses RabbitMQ as the Celery message broker. Cloudron has no RabbitMQ addon, and RabbitMQ uses ~200 MB of RAM at rest (Erlang overhead).
Solution: LavinMQ. LavinMQ is an AMQP 0.9.1 wire-compatible message broker written in Crystal. It is a drop-in replacement: same protocol, same broker URL format (
amqp://guest:guest@localhost:5672), same Celery compatibility. Memory usage is ~40 MB at rest.The Dockerfile removes RabbitMQ and Erlang, then installs LavinMQ from the official apt repository. The
start.shscript starts LavinMQ before supervisord and waits for it to accept connections.Installation in the Dockerfile:
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ apt-get remove -y rabbitmq-server erlang-base erlang-nox || true && \ apt-get autoremove -y && \ curl -fsSL https://packagecloud.io/cloudamqp/lavinmq/gpgkey | \ gpg --dearmor -o /usr/share/keyrings/lavinmq.gpg && \ echo "deb [signed-by=/usr/share/keyrings/lavinmq.gpg] https://packagecloud.io/cloudamqp/lavinmq/ubuntu noble main" \ > /etc/apt/sources.list.d/lavinmq.list && \ apt-get update && \ apt-get install -y lavinmq && \ apt-get clean && \ rm -rf /var/lib/apt/lists/*Challenge 3: External PostgreSQL and Redis
Cloudron provides managed PostgreSQL and Redis as addons. Docassemble supports external services via environment variables and config.yml.
Solution: Map Cloudron env vars to docassemble env vars in start.sh, then generate config.yml on every boot.
Key mappings:
CLOUDRON_POSTGRESQL_HOST→DBHOSTCLOUDRON_POSTGRESQL_DATABASE→DBNAMECLOUDRON_POSTGRESQL_USERNAME→DBUSERCLOUDRON_POSTGRESQL_PASSWORD→DBPASSWORDCLOUDRON_REDIS_*→redis:directive in config.ymlCLOUDRON_APP_DOMAIN→DAHOSTNAMEBEHINDHTTPSLOADBALANCER=true(always)PORT=8080(matches manifest httpPort)
Gotcha: CONTAINERROLE must not be "all". Setting
CONTAINERROLE=alltellsinitialize.shto start internal PostgreSQL and Redis via supervisorctl, regardless of supervisor's autostart setting. SetCONTAINERROLE=web:celery:cron:log:mailto skip the sql, redis, and rabbitmq roles.Gotcha: Cloudron PostgreSQL does not use SSL. Do not set
ssl mode: requirein the database configuration. Cloudron's internal PostgreSQL rejects SSL connections.Gotcha: pg_hba.conf errors are non-fatal. Initialize.sh tries to connect to the
postgressystem database to check if the app database exists. Cloudron only allows connections to the app-specific database, so this check fails. It is harmless: the script falls through and connects to the correct database.Challenge 4: Supervisor overrides
We need to disable services that Cloudron provides or that we have replaced.
Solution: A supervisor conf file that sets
autostart=falsefor postgres, redis, rabbitmq, apache2, and syslogng.Gotcha: Must COPY in Dockerfile, not at runtime.
/etc/supervisor/conf.d/is read-only at runtime. The override must be placed there during the Docker build.Challenge 5: Python virtual environment persistence
Docassemble writes to its Python virtual environment at runtime (Playground package installs, pip config changes). The venv is ~2 GB.
Solution: Save the venv as a seed in the Docker image, then copy it to
/app/data/venvon first boot. The Dockerfile copies the venv to/app/code/seed-venv, then symlinks/usr/share/docassemble/local3.12→/app/data/venv. On first boot,start.shcopies the seed to persistent storage.Gotcha: Build service timeout. The 2 GB venv copy can time out on Cloudron's remote build service. If this happens, build locally with
docker buildand push to a registry.Challenge 6: config.yml regeneration
Cloudron may change environment variables between restarts (domain changes, credential rotation). Config.yml is regenerated from environment variables on every boot.
The
secretkey(used for encrypting session data) is generated once and persisted at/app/data/config/.secretso sessions survive restarts.
Part 3: Suggestions for Cloudron maintainers
Several platform improvements would make packaging complex apps like docassemble significantly easier.
1. Honour memoryLimit in manifests for custom apps
Currently, the
memoryLimitfield inCloudronManifest.jsonis ignored for custom (unpublished) apps. The default is 256 MB, which is insufficient for any substantial Python application. Users must manually set memory in the dashboard after installation, which is an undiscoverable step that causes confusing failures.Suggestion: Apply the manifest's
memoryLimitduring installation, or at minimum, show a warning if the default is being used despite a higher value in the manifest.2. Provide a LavinMQ addon
LavinMQ is a lightweight, AMQP 0.9.1 compatible message broker that could serve any app currently needing RabbitMQ. At ~40 MB RAM and a single binary, it is a natural fit for the addon model. A managed LavinMQ addon would simplify packaging for apps that use Celery, Sidekiq, or any AMQP-based task queue.
There is a separate forum request for LavinMQ on Cloudron.
3. Configurable stop timeout per app
Docassemble's upstream Docker setup uses
--stop-timeout 600for graceful shutdown. Cloudron's default stop timeout is shorter. A per-app configurable stop timeout (in the manifest or dashboard) would help apps that need time to flush state.4. Writable /etc subdirectories as an opt-in manifest option
Many complex apps write to
/etc/subdirectories during initialization (timezone, locale, SSL, cron). Currently, packagers must individually symlink each path. A manifest option like"writableEtcDirs": ["/etc/nginx", "/etc/ssl", "/etc/cron.daily"]would reduce boilerplate significantly.5. Build service timeout for large images
The Cloudron build service can time out when Dockerfile steps involve copying large directories (2 GB+ in our case). A configurable build timeout or a progress-aware timeout (that resets when output is being produced) would help.
Known limitations (v0.1)
- Memory must be set manually to ~8 GB in the Cloudron dashboard after installation
- First boot takes 15-20 minutes (database migration, venv copy)
- Exim4 (email receiving) does not work (read-only /etc/exim4). Outbound email via Cloudron's sendmail addon works fine.
- The
initializesupervisor process hangs after completing. Non-blocking but wastes some memory. - Playground package installs persist across restarts but not across Cloudron updates (the venv is re-seeded from the image on update)
- No OIDC/LDAP integration with Cloudron user management yet
Repository
https://github.com/OrcVole/docassemble-cloudron
Contains:
Dockerfile,start.sh,CloudronManifest.json,supervisor/docassemble-cloudron.conf,DESCRIPTION.md,icon.png, documentation.Tested on Cloudron 9.x with the
jhpyle/docassemble:latestimage (v1.8.17, Ubuntu 24.04, Python 3.12).Feedback, bug reports, and pull requests welcome.
-
We did it!
It was a long hard slog, but managed to get Docassemble up and running. https://docassemble.org/Docassemble on Cloudron: Working Custom Package + Packaging Guide
We have successfully packaged docassemble as a custom Cloudron app. It is working: interviews run, documents generate, the Playground is functional, and data persists across restarts. The source is available at [your-repo-url] and we welcome testing, feedback, and contributions.
This post is structured for three audiences:
- Deployers who want to run docassemble on their Cloudron today
- Packagers who might want to make this an officially supported Cloudron app
- Cloudron maintainers who could improve the platform to make apps like this easier
What is docassemble and why would you want it?
Docassemble is a free, open-source platform for building guided interviews that generate documents. You define questions in YAML, write logic in Python, and it produces a mobile-friendly web application that walks users through a process step by step, then assembles customised PDFs, DOCX files, or other outputs.
It is the leading open-source tool in its niche. Courts, legal aid organisations, government agencies, and NGOs use it for everything from benefits applications to tenancy agreements to immigration forms. The commercial alternatives (HotDocs, Documate, Lawyaw) cost thousands per year and lock you into their cloud.
What makes it worth the packaging effort:
- Genuinely smart conditional logic. Not just yes/no branching. The Python engine evaluates complex conditions, queries external APIs mid-interview, and adapts the flow dynamically.
- Document assembly that handles real-world messiness. PDF form filling, DOCX generation from Jinja2 templates, touchscreen signatures, OCR, multilingual support.
- Browser-based development. The Playground lets you write, test, and package interviews without any local tooling.
- Self-hosted and MIT licensed. Your data stays on your server. No per-user fees. No SaaS dependency. Critical for legal, healthcare, and government contexts.
Part 1: Deploying docassemble on Cloudron (for users)
Prerequisites
- A Cloudron instance with at least 12 GB total RAM (docassemble needs ~8 GB allocated to it)
- Cloudron CLI installed and logged in
- A domain or subdomain configured in Cloudron
- About 45 minutes for the first deployment (most of this is waiting for builds and initialization)
Step 1: Get the package files (~2 minutes)
Clone the repository:
git clone [your-repo-url] docassemble-cloudron cd docassemble-cloudronStep 2: Install the app (~10-15 minutes)
cloudron install --location docassemble.yourdomain.comThe build takes 10-15 minutes. The Cloudron build service downloads the ~4 GB upstream Docker image, removes RabbitMQ, installs LavinMQ, and prepares seed directories.
Gotcha: Build timeout. If the build times out on the Cloudron build service, you can build locally instead:
docker build -t your-registry/docassemble-cloudron:0.1 . docker push your-registry/docassemble-cloudron:0.1 cloudron install --image your-registry/docassemble-cloudron:0.1 --location docassemble.yourdomain.comStep 3: Set the memory limit (~1 minute)
This step is critical. Immediately after installation, go to your Cloudron dashboard, find the docassemble app, open its settings, and set the memory limit to at least 8 GB (8192 MiB).
Gotcha: Manifest memory limit is ignored. Cloudron does not honour the
memoryLimitfield inCloudronManifest.jsonfor custom (unpublished) apps. The default is 256 MB, which is not enough to load Python's import chain. You must set it manually in the dashboard. The app will restart automatically after you change it.Step 4: Wait for first boot (~15-20 minutes)
First boot is slow. The app needs to:
- Copy the ~2 GB Python virtual environment to persistent storage (~3-5 minutes)
- Run database migrations and populate package tables (~2-3 minutes)
- Generate nginx and application configuration (~1 minute)
- Start all services (nginx, uWSGI, Celery, WebSockets) (~2-3 minutes)
You can monitor progress:
cloudron logs -f --app docassemble.yourdomain.comLook for
Finished initializingin the logs. After that, services will start one by one. When you see celery, nginx, and uwsgi all running, the app is ready.Gotcha: "Not responding" during init. The Cloudron dashboard may show "not responding" for up to 20 minutes during first boot. This is normal. Do not restart the app during this time.
Step 5: Log in (~1 minute)
Visit
https://docassemble.yourdomain.com. The default admin account is:- Email:
admin@example.com - Password:
password
Change this password immediately after logging in. Go to User List from the menu, click Edit next to the admin account, and change both the email and password.
Step 6: Verify it works (~5 minutes)
- Go to Playground from the menu
- Paste this test interview:
question: | Hello from Cloudron! subquestion: | Docassemble is running at ${ url_of('root') } mandatory: True- Click Save and Run. If the interview loads, the core engine works.
- Go to Configuration from the menu and verify it shows your PostgreSQL and Redis connection details.
- Go to Logs and confirm entries are visible.
Subsequent boots (~2-3 minutes)
After the first boot, restarts are much faster because the database tables already exist and the virtual environment is already copied. Expect 2-3 minutes for the app to become responsive after a restart.
Part 2: How the package works (for packagers)
Docassemble is one of the most complex applications you can package for Cloudron. The upstream Docker image is a monolith running ~10 services under supervisord. Here is how we solved each major challenge.
Architecture
Cloudron reverse proxy (TLS) | v nginx:8080 --> uWSGI (Flask/Python app) | +--> Cloudron PostgreSQL addon (external) +--> Cloudron Redis addon (external) +--> LavinMQ localhost:5672 (internal, replaces RabbitMQ) +--> Celery workers (2 processes, 2 queues) +--> WebSocket server (live help) +--> cron (scheduled tasks)Challenge 1: Read-only filesystem
Cloudron locks everything except
/app/data,/tmp, and/run. Docassemble writes to approximately 25 different filesystem paths during initialization and normal operation.Solution: Two categories of symlinks.
Persistent paths (survive restarts, backed up by Cloudron) are symlinked to
/app/data/:/usr/share/docassemble/config→/app/data/config/usr/share/docassemble/files→/app/data/files/usr/share/docassemble/log→/app/data/log/usr/share/docassemble/backup→/app/data/backup/usr/share/docassemble/certs→/app/data/certs/usr/share/docassemble/cron→/app/data/cron/usr/share/docassemble/local3.12(Python venv) →/app/data/venv
Volatile paths (regenerated every boot) are symlinked to
/run/:/etc/nginx/sites-availableand/etc/nginx/sites-enabled/var/log/nginx,/var/log/supervisor,/var/log/apache2/etc/ssl,/var/www,/var/cache/debconf/etc/localtime,/etc/timezone,/etc/locale.gen,/etc/locale.conf/var/lib/nginx,/var/lib/postgresql/etc/cron.daily,/etc/hasbeeninitialized
The Dockerfile creates all symlinks at build time. The
start.shentrypoint recreates volatile directories and seeds them from saved copies on every boot.Gotcha: Python's shutil.rmtree() refuses symlinks. Docassemble's
install_certs.pycallsshutil.rmtree()on/var/www/.certsand/etc/ssl/docassemble. If these are symlinks, Python raisesOSError: Cannot call rmtree on a symbolic link. The fix is to symlink the entire parent directory (/var/www→/run/www,/etc/ssl→/run/ssl) and seed them as real directories from saved copies at boot time.Gotcha: nginx temp directory. Nginx needs writable temp directories at
/var/lib/nginx/body,/var/lib/nginx/proxy, etc. Symlink/var/lib/nginx→/run/nginx-liband create the subdirectories instart.sh.Challenge 2: Replacing RabbitMQ
Docassemble uses RabbitMQ as the Celery message broker. Cloudron has no RabbitMQ addon, and RabbitMQ uses ~200 MB of RAM at rest (Erlang overhead).
Solution: LavinMQ. LavinMQ is an AMQP 0.9.1 wire-compatible message broker written in Crystal. It is a drop-in replacement: same protocol, same broker URL format (
amqp://guest:guest@localhost:5672), same Celery compatibility. Memory usage is ~40 MB at rest.The Dockerfile removes RabbitMQ and Erlang, then installs LavinMQ from the official apt repository. The
start.shscript starts LavinMQ before supervisord and waits for it to accept connections.Installation in the Dockerfile:
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ apt-get remove -y rabbitmq-server erlang-base erlang-nox || true && \ apt-get autoremove -y && \ curl -fsSL https://packagecloud.io/cloudamqp/lavinmq/gpgkey | \ gpg --dearmor -o /usr/share/keyrings/lavinmq.gpg && \ echo "deb [signed-by=/usr/share/keyrings/lavinmq.gpg] https://packagecloud.io/cloudamqp/lavinmq/ubuntu noble main" \ > /etc/apt/sources.list.d/lavinmq.list && \ apt-get update && \ apt-get install -y lavinmq && \ apt-get clean && \ rm -rf /var/lib/apt/lists/*Challenge 3: External PostgreSQL and Redis
Cloudron provides managed PostgreSQL and Redis as addons. Docassemble supports external services via environment variables and config.yml.
Solution: Map Cloudron env vars to docassemble env vars in start.sh, then generate config.yml on every boot.
Key mappings:
CLOUDRON_POSTGRESQL_HOST→DBHOSTCLOUDRON_POSTGRESQL_DATABASE→DBNAMECLOUDRON_POSTGRESQL_USERNAME→DBUSERCLOUDRON_POSTGRESQL_PASSWORD→DBPASSWORDCLOUDRON_REDIS_*→redis:directive in config.ymlCLOUDRON_APP_DOMAIN→DAHOSTNAMEBEHINDHTTPSLOADBALANCER=true(always)PORT=8080(matches manifest httpPort)
Gotcha: CONTAINERROLE must not be "all". Setting
CONTAINERROLE=alltellsinitialize.shto start internal PostgreSQL and Redis via supervisorctl, regardless of supervisor's autostart setting. SetCONTAINERROLE=web:celery:cron:log:mailto skip the sql, redis, and rabbitmq roles.Gotcha: Cloudron PostgreSQL does not use SSL. Do not set
ssl mode: requirein the database configuration. Cloudron's internal PostgreSQL rejects SSL connections.Gotcha: pg_hba.conf errors are non-fatal. Initialize.sh tries to connect to the
postgressystem database to check if the app database exists. Cloudron only allows connections to the app-specific database, so this check fails. It is harmless: the script falls through and connects to the correct database.Challenge 4: Supervisor overrides
We need to disable services that Cloudron provides or that we have replaced.
Solution: A supervisor conf file that sets
autostart=falsefor postgres, redis, rabbitmq, apache2, and syslogng.Gotcha: Must COPY in Dockerfile, not at runtime.
/etc/supervisor/conf.d/is read-only at runtime. The override must be placed there during the Docker build.Challenge 5: Python virtual environment persistence
Docassemble writes to its Python virtual environment at runtime (Playground package installs, pip config changes). The venv is ~2 GB.
Solution: Save the venv as a seed in the Docker image, then copy it to
/app/data/venvon first boot. The Dockerfile copies the venv to/app/code/seed-venv, then symlinks/usr/share/docassemble/local3.12→/app/data/venv. On first boot,start.shcopies the seed to persistent storage.Gotcha: Build service timeout. The 2 GB venv copy can time out on Cloudron's remote build service. If this happens, build locally with
docker buildand push to a registry.Challenge 6: config.yml regeneration
Cloudron may change environment variables between restarts (domain changes, credential rotation). Config.yml is regenerated from environment variables on every boot.
The
secretkey(used for encrypting session data) is generated once and persisted at/app/data/config/.secretso sessions survive restarts.
Part 3: Suggestions for Cloudron maintainers
Several platform improvements would make packaging complex apps like docassemble significantly easier.
1. Honour memoryLimit in manifests for custom apps
Currently, the
memoryLimitfield inCloudronManifest.jsonis ignored for custom (unpublished) apps. The default is 256 MB, which is insufficient for any substantial Python application. Users must manually set memory in the dashboard after installation, which is an undiscoverable step that causes confusing failures.Suggestion: Apply the manifest's
memoryLimitduring installation, or at minimum, show a warning if the default is being used despite a higher value in the manifest.2. Provide a LavinMQ addon
LavinMQ is a lightweight, AMQP 0.9.1 compatible message broker that could serve any app currently needing RabbitMQ. At ~40 MB RAM and a single binary, it is a natural fit for the addon model. A managed LavinMQ addon would simplify packaging for apps that use Celery, Sidekiq, or any AMQP-based task queue.
There is a separate forum request for LavinMQ on Cloudron.
3. Configurable stop timeout per app
Docassemble's upstream Docker setup uses
--stop-timeout 600for graceful shutdown. Cloudron's default stop timeout is shorter. A per-app configurable stop timeout (in the manifest or dashboard) would help apps that need time to flush state.4. Writable /etc subdirectories as an opt-in manifest option
Many complex apps write to
/etc/subdirectories during initialization (timezone, locale, SSL, cron). Currently, packagers must individually symlink each path. A manifest option like"writableEtcDirs": ["/etc/nginx", "/etc/ssl", "/etc/cron.daily"]would reduce boilerplate significantly.5. Build service timeout for large images
The Cloudron build service can time out when Dockerfile steps involve copying large directories (2 GB+ in our case). A configurable build timeout or a progress-aware timeout (that resets when output is being produced) would help.
Known limitations (v0.1)
- Memory must be set manually to ~8 GB in the Cloudron dashboard after installation
- First boot takes 15-20 minutes (database migration, venv copy)
- Exim4 (email receiving) does not work (read-only /etc/exim4). Outbound email via Cloudron's sendmail addon works fine.
- The
initializesupervisor process hangs after completing. Non-blocking but wastes some memory. - Playground package installs persist across restarts but not across Cloudron updates (the venv is re-seeded from the image on update)
- No OIDC/LDAP integration with Cloudron user management yet
Repository
https://github.com/OrcVole/docassemble-cloudron
Contains:
Dockerfile,start.sh,CloudronManifest.json,supervisor/docassemble-cloudron.conf,DESCRIPTION.md,icon.png, documentation.Tested on Cloudron 9.x with the
jhpyle/docassemble:latestimage (v1.8.17, Ubuntu 24.04, Python 3.12).Feedback, bug reports, and pull requests welcome.
Well done for building it
go to your Cloudron dashboard, find the docassemble app, open its settings, and set the memory limit to at least 8 GB (8192 MiB)
odd, my deploy does set a custom memory limit during publication
how are you expressing the number ? -
We did it!
It was a long hard slog, but managed to get Docassemble up and running. https://docassemble.org/Docassemble on Cloudron: Working Custom Package + Packaging Guide
We have successfully packaged docassemble as a custom Cloudron app. It is working: interviews run, documents generate, the Playground is functional, and data persists across restarts. The source is available at [your-repo-url] and we welcome testing, feedback, and contributions.
This post is structured for three audiences:
- Deployers who want to run docassemble on their Cloudron today
- Packagers who might want to make this an officially supported Cloudron app
- Cloudron maintainers who could improve the platform to make apps like this easier
What is docassemble and why would you want it?
Docassemble is a free, open-source platform for building guided interviews that generate documents. You define questions in YAML, write logic in Python, and it produces a mobile-friendly web application that walks users through a process step by step, then assembles customised PDFs, DOCX files, or other outputs.
It is the leading open-source tool in its niche. Courts, legal aid organisations, government agencies, and NGOs use it for everything from benefits applications to tenancy agreements to immigration forms. The commercial alternatives (HotDocs, Documate, Lawyaw) cost thousands per year and lock you into their cloud.
What makes it worth the packaging effort:
- Genuinely smart conditional logic. Not just yes/no branching. The Python engine evaluates complex conditions, queries external APIs mid-interview, and adapts the flow dynamically.
- Document assembly that handles real-world messiness. PDF form filling, DOCX generation from Jinja2 templates, touchscreen signatures, OCR, multilingual support.
- Browser-based development. The Playground lets you write, test, and package interviews without any local tooling.
- Self-hosted and MIT licensed. Your data stays on your server. No per-user fees. No SaaS dependency. Critical for legal, healthcare, and government contexts.
Part 1: Deploying docassemble on Cloudron (for users)
Prerequisites
- A Cloudron instance with at least 12 GB total RAM (docassemble needs ~8 GB allocated to it)
- Cloudron CLI installed and logged in
- A domain or subdomain configured in Cloudron
- About 45 minutes for the first deployment (most of this is waiting for builds and initialization)
Step 1: Get the package files (~2 minutes)
Clone the repository:
git clone [your-repo-url] docassemble-cloudron cd docassemble-cloudronStep 2: Install the app (~10-15 minutes)
cloudron install --location docassemble.yourdomain.comThe build takes 10-15 minutes. The Cloudron build service downloads the ~4 GB upstream Docker image, removes RabbitMQ, installs LavinMQ, and prepares seed directories.
Gotcha: Build timeout. If the build times out on the Cloudron build service, you can build locally instead:
docker build -t your-registry/docassemble-cloudron:0.1 . docker push your-registry/docassemble-cloudron:0.1 cloudron install --image your-registry/docassemble-cloudron:0.1 --location docassemble.yourdomain.comStep 3: Set the memory limit (~1 minute)
This step is critical. Immediately after installation, go to your Cloudron dashboard, find the docassemble app, open its settings, and set the memory limit to at least 8 GB (8192 MiB).
Gotcha: Manifest memory limit is ignored. Cloudron does not honour the
memoryLimitfield inCloudronManifest.jsonfor custom (unpublished) apps. The default is 256 MB, which is not enough to load Python's import chain. You must set it manually in the dashboard. The app will restart automatically after you change it.Step 4: Wait for first boot (~15-20 minutes)
First boot is slow. The app needs to:
- Copy the ~2 GB Python virtual environment to persistent storage (~3-5 minutes)
- Run database migrations and populate package tables (~2-3 minutes)
- Generate nginx and application configuration (~1 minute)
- Start all services (nginx, uWSGI, Celery, WebSockets) (~2-3 minutes)
You can monitor progress:
cloudron logs -f --app docassemble.yourdomain.comLook for
Finished initializingin the logs. After that, services will start one by one. When you see celery, nginx, and uwsgi all running, the app is ready.Gotcha: "Not responding" during init. The Cloudron dashboard may show "not responding" for up to 20 minutes during first boot. This is normal. Do not restart the app during this time.
Step 5: Log in (~1 minute)
Visit
https://docassemble.yourdomain.com. The default admin account is:- Email:
admin@example.com - Password:
password
Change this password immediately after logging in. Go to User List from the menu, click Edit next to the admin account, and change both the email and password.
Step 6: Verify it works (~5 minutes)
- Go to Playground from the menu
- Paste this test interview:
question: | Hello from Cloudron! subquestion: | Docassemble is running at ${ url_of('root') } mandatory: True- Click Save and Run. If the interview loads, the core engine works.
- Go to Configuration from the menu and verify it shows your PostgreSQL and Redis connection details.
- Go to Logs and confirm entries are visible.
Subsequent boots (~2-3 minutes)
After the first boot, restarts are much faster because the database tables already exist and the virtual environment is already copied. Expect 2-3 minutes for the app to become responsive after a restart.
Part 2: How the package works (for packagers)
Docassemble is one of the most complex applications you can package for Cloudron. The upstream Docker image is a monolith running ~10 services under supervisord. Here is how we solved each major challenge.
Architecture
Cloudron reverse proxy (TLS) | v nginx:8080 --> uWSGI (Flask/Python app) | +--> Cloudron PostgreSQL addon (external) +--> Cloudron Redis addon (external) +--> LavinMQ localhost:5672 (internal, replaces RabbitMQ) +--> Celery workers (2 processes, 2 queues) +--> WebSocket server (live help) +--> cron (scheduled tasks)Challenge 1: Read-only filesystem
Cloudron locks everything except
/app/data,/tmp, and/run. Docassemble writes to approximately 25 different filesystem paths during initialization and normal operation.Solution: Two categories of symlinks.
Persistent paths (survive restarts, backed up by Cloudron) are symlinked to
/app/data/:/usr/share/docassemble/config→/app/data/config/usr/share/docassemble/files→/app/data/files/usr/share/docassemble/log→/app/data/log/usr/share/docassemble/backup→/app/data/backup/usr/share/docassemble/certs→/app/data/certs/usr/share/docassemble/cron→/app/data/cron/usr/share/docassemble/local3.12(Python venv) →/app/data/venv
Volatile paths (regenerated every boot) are symlinked to
/run/:/etc/nginx/sites-availableand/etc/nginx/sites-enabled/var/log/nginx,/var/log/supervisor,/var/log/apache2/etc/ssl,/var/www,/var/cache/debconf/etc/localtime,/etc/timezone,/etc/locale.gen,/etc/locale.conf/var/lib/nginx,/var/lib/postgresql/etc/cron.daily,/etc/hasbeeninitialized
The Dockerfile creates all symlinks at build time. The
start.shentrypoint recreates volatile directories and seeds them from saved copies on every boot.Gotcha: Python's shutil.rmtree() refuses symlinks. Docassemble's
install_certs.pycallsshutil.rmtree()on/var/www/.certsand/etc/ssl/docassemble. If these are symlinks, Python raisesOSError: Cannot call rmtree on a symbolic link. The fix is to symlink the entire parent directory (/var/www→/run/www,/etc/ssl→/run/ssl) and seed them as real directories from saved copies at boot time.Gotcha: nginx temp directory. Nginx needs writable temp directories at
/var/lib/nginx/body,/var/lib/nginx/proxy, etc. Symlink/var/lib/nginx→/run/nginx-liband create the subdirectories instart.sh.Challenge 2: Replacing RabbitMQ
Docassemble uses RabbitMQ as the Celery message broker. Cloudron has no RabbitMQ addon, and RabbitMQ uses ~200 MB of RAM at rest (Erlang overhead).
Solution: LavinMQ. LavinMQ is an AMQP 0.9.1 wire-compatible message broker written in Crystal. It is a drop-in replacement: same protocol, same broker URL format (
amqp://guest:guest@localhost:5672), same Celery compatibility. Memory usage is ~40 MB at rest.The Dockerfile removes RabbitMQ and Erlang, then installs LavinMQ from the official apt repository. The
start.shscript starts LavinMQ before supervisord and waits for it to accept connections.Installation in the Dockerfile:
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ apt-get remove -y rabbitmq-server erlang-base erlang-nox || true && \ apt-get autoremove -y && \ curl -fsSL https://packagecloud.io/cloudamqp/lavinmq/gpgkey | \ gpg --dearmor -o /usr/share/keyrings/lavinmq.gpg && \ echo "deb [signed-by=/usr/share/keyrings/lavinmq.gpg] https://packagecloud.io/cloudamqp/lavinmq/ubuntu noble main" \ > /etc/apt/sources.list.d/lavinmq.list && \ apt-get update && \ apt-get install -y lavinmq && \ apt-get clean && \ rm -rf /var/lib/apt/lists/*Challenge 3: External PostgreSQL and Redis
Cloudron provides managed PostgreSQL and Redis as addons. Docassemble supports external services via environment variables and config.yml.
Solution: Map Cloudron env vars to docassemble env vars in start.sh, then generate config.yml on every boot.
Key mappings:
CLOUDRON_POSTGRESQL_HOST→DBHOSTCLOUDRON_POSTGRESQL_DATABASE→DBNAMECLOUDRON_POSTGRESQL_USERNAME→DBUSERCLOUDRON_POSTGRESQL_PASSWORD→DBPASSWORDCLOUDRON_REDIS_*→redis:directive in config.ymlCLOUDRON_APP_DOMAIN→DAHOSTNAMEBEHINDHTTPSLOADBALANCER=true(always)PORT=8080(matches manifest httpPort)
Gotcha: CONTAINERROLE must not be "all". Setting
CONTAINERROLE=alltellsinitialize.shto start internal PostgreSQL and Redis via supervisorctl, regardless of supervisor's autostart setting. SetCONTAINERROLE=web:celery:cron:log:mailto skip the sql, redis, and rabbitmq roles.Gotcha: Cloudron PostgreSQL does not use SSL. Do not set
ssl mode: requirein the database configuration. Cloudron's internal PostgreSQL rejects SSL connections.Gotcha: pg_hba.conf errors are non-fatal. Initialize.sh tries to connect to the
postgressystem database to check if the app database exists. Cloudron only allows connections to the app-specific database, so this check fails. It is harmless: the script falls through and connects to the correct database.Challenge 4: Supervisor overrides
We need to disable services that Cloudron provides or that we have replaced.
Solution: A supervisor conf file that sets
autostart=falsefor postgres, redis, rabbitmq, apache2, and syslogng.Gotcha: Must COPY in Dockerfile, not at runtime.
/etc/supervisor/conf.d/is read-only at runtime. The override must be placed there during the Docker build.Challenge 5: Python virtual environment persistence
Docassemble writes to its Python virtual environment at runtime (Playground package installs, pip config changes). The venv is ~2 GB.
Solution: Save the venv as a seed in the Docker image, then copy it to
/app/data/venvon first boot. The Dockerfile copies the venv to/app/code/seed-venv, then symlinks/usr/share/docassemble/local3.12→/app/data/venv. On first boot,start.shcopies the seed to persistent storage.Gotcha: Build service timeout. The 2 GB venv copy can time out on Cloudron's remote build service. If this happens, build locally with
docker buildand push to a registry.Challenge 6: config.yml regeneration
Cloudron may change environment variables between restarts (domain changes, credential rotation). Config.yml is regenerated from environment variables on every boot.
The
secretkey(used for encrypting session data) is generated once and persisted at/app/data/config/.secretso sessions survive restarts.
Part 3: Suggestions for Cloudron maintainers
Several platform improvements would make packaging complex apps like docassemble significantly easier.
1. Honour memoryLimit in manifests for custom apps
Currently, the
memoryLimitfield inCloudronManifest.jsonis ignored for custom (unpublished) apps. The default is 256 MB, which is insufficient for any substantial Python application. Users must manually set memory in the dashboard after installation, which is an undiscoverable step that causes confusing failures.Suggestion: Apply the manifest's
memoryLimitduring installation, or at minimum, show a warning if the default is being used despite a higher value in the manifest.2. Provide a LavinMQ addon
LavinMQ is a lightweight, AMQP 0.9.1 compatible message broker that could serve any app currently needing RabbitMQ. At ~40 MB RAM and a single binary, it is a natural fit for the addon model. A managed LavinMQ addon would simplify packaging for apps that use Celery, Sidekiq, or any AMQP-based task queue.
There is a separate forum request for LavinMQ on Cloudron.
3. Configurable stop timeout per app
Docassemble's upstream Docker setup uses
--stop-timeout 600for graceful shutdown. Cloudron's default stop timeout is shorter. A per-app configurable stop timeout (in the manifest or dashboard) would help apps that need time to flush state.4. Writable /etc subdirectories as an opt-in manifest option
Many complex apps write to
/etc/subdirectories during initialization (timezone, locale, SSL, cron). Currently, packagers must individually symlink each path. A manifest option like"writableEtcDirs": ["/etc/nginx", "/etc/ssl", "/etc/cron.daily"]would reduce boilerplate significantly.5. Build service timeout for large images
The Cloudron build service can time out when Dockerfile steps involve copying large directories (2 GB+ in our case). A configurable build timeout or a progress-aware timeout (that resets when output is being produced) would help.
Known limitations (v0.1)
- Memory must be set manually to ~8 GB in the Cloudron dashboard after installation
- First boot takes 15-20 minutes (database migration, venv copy)
- Exim4 (email receiving) does not work (read-only /etc/exim4). Outbound email via Cloudron's sendmail addon works fine.
- The
initializesupervisor process hangs after completing. Non-blocking but wastes some memory. - Playground package installs persist across restarts but not across Cloudron updates (the venv is re-seeded from the image on update)
- No OIDC/LDAP integration with Cloudron user management yet
Repository
https://github.com/OrcVole/docassemble-cloudron
Contains:
Dockerfile,start.sh,CloudronManifest.json,supervisor/docassemble-cloudron.conf,DESCRIPTION.md,icon.png, documentation.Tested on Cloudron 9.x with the
jhpyle/docassemble:latestimage (v1.8.17, Ubuntu 24.04, Python 3.12).Feedback, bug reports, and pull requests welcome.
-
LoudLemur said:
We did it!
It was a long hard slog, but managed to get Docassemble up and running. https://docassemble.org/Docassemble on Cloudron: Working Custom Package + Packaging Guide
We have successfully packaged docassemble as a custom Cloudron app. It is working: interviews run, documents generate, the Playground is functional, and data persists across restarts. The source is available at https://github.com/OrcVole/docassemble-cloudron and we welcome testing, feedback, and contributions.
This post is structured for three audiences:
- Deployers who want to run docassemble on their Cloudron today
- Packagers who might want to make this an officially supported Cloudron app
- Cloudron maintainers who could improve the platform to make apps like this easier
What is docassemble and why would you want it?
Docassemble is a free, open-source platform for building guided interviews that generate documents. You define questions in YAML, write logic in Python, and it produces a mobile-friendly web application that walks users through a process step by step, then assembles customised PDFs, DOCX files, or other outputs.
It is the leading open-source tool in its niche. Courts, legal aid organisations, government agencies, and NGOs use it for everything from benefits applications to tenancy agreements to immigration forms. The commercial alternatives (HotDocs, Documate, Lawyaw) cost thousands per year and lock you into their cloud.
What makes it worth the packaging effort:
- Genuinely smart conditional logic. Not just yes/no branching. The Python engine evaluates complex conditions, queries external APIs mid-interview, and adapts the flow dynamically.
- Document assembly that handles real-world messiness. PDF form filling, DOCX generation from Jinja2 templates, touchscreen signatures, OCR, multilingual support.
- Browser-based development. The Playground lets you write, test, and package interviews without any local tooling.
- Self-hosted and MIT licensed. Your data stays on your server. No per-user fees. No SaaS dependency. Critical for legal, healthcare, and government contexts.
Part 1: Deploying docassemble on Cloudron (for users)
Prerequisites
- A Cloudron instance with at least 12 GB total RAM (docassemble needs ~8 GB allocated to it)
- Cloudron CLI installed and logged in
- A domain or subdomain configured in Cloudron
- About 45 minutes for the first deployment (most of this is waiting for builds and initialization)
Step 1: Get the package files (~2 minutes)
Clone the repository:
git clone https://github.com/OrcVole/docassemble-cloudron cd docassemble-cloudronStep 2: Install the app (~10-15 minutes)
cloudron install --location docassemble.yourdomain.comThe build takes 10-15 minutes. The Cloudron build service downloads the ~4 GB upstream Docker image, removes RabbitMQ, installs LavinMQ, and prepares seed directories.
Gotcha: Build timeout. If the build times out on the Cloudron build service, you can build locally instead:
docker build -t your-registry/docassemble-cloudron:0.1 . docker push your-registry/docassemble-cloudron:0.1 cloudron install --image your-registry/docassemble-cloudron:0.1 --location docassemble.yourdomain.comStep 3: Set the memory limit (~1 minute)
This step is critical. Immediately after installation, go to your Cloudron dashboard, find the docassemble app, open its settings, and set the memory limit to at least 8 GB (8192 MiB).
Gotcha: Manifest memory limit is ignored. Cloudron does not honour the
memoryLimitfield inCloudronManifest.jsonfor custom (unpublished) apps. The default is 256 MB, which is not enough to load Python's import chain. You must set it manually in the dashboard. The app will restart automatically after you change it.Step 4: Wait for first boot (~15-20 minutes)
First boot is slow. The app needs to:
- Copy the ~2 GB Python virtual environment to persistent storage (~3-5 minutes)
- Run database migrations and populate package tables (~2-3 minutes)
- Generate nginx and application configuration (~1 minute)
- Start all services (nginx, uWSGI, Celery, WebSockets) (~2-3 minutes)
You can monitor progress:
cloudron logs -f --app docassemble.yourdomain.comLook for
Finished initializingin the logs. After that, services will start one by one. When you see celery, nginx, and uwsgi all running, the app is ready.Gotcha: "Not responding" during init. The Cloudron dashboard may show "not responding" for up to 20 minutes during first boot. This is normal. Do not restart the app during this time.
Step 5: Log in (~1 minute)
Visit
https://docassemble.yourdomain.com. The default admin account is:- Email:
admin@example.com - Password:
password
Change this password immediately after logging in. Go to User List from the menu, click Edit next to the admin account, and change both the email and password.
Step 6: Verify it works (~5 minutes)
- Go to Playground from the menu
- Paste this test interview:
question: | Hello from Cloudron! subquestion: | Docassemble is running at ${ url_of('root') } mandatory: True- Click Save and Run. If the interview loads, the core engine works.
- Go to Configuration from the menu and verify it shows your PostgreSQL and Redis connection details.
- Go to Logs and confirm entries are visible.
Subsequent boots (~2-3 minutes)
After the first boot, restarts are much faster because the database tables already exist and the virtual environment is already copied. Expect 2-3 minutes for the app to become responsive after a restart.
Part 2: How the package works (for packagers)
Docassemble is one of the most complex applications you can package for Cloudron. The upstream Docker image is a monolith running ~10 services under supervisord. Here is how we solved each major challenge.
Architecture
Cloudron reverse proxy (TLS) | v nginx:8080 --> uWSGI (Flask/Python app) | +--> Cloudron PostgreSQL addon (external) +--> Cloudron Redis addon (external) +--> LavinMQ localhost:5672 (internal, replaces RabbitMQ) +--> Celery workers (2 processes, 2 queues) +--> WebSocket server (live help) +--> cron (scheduled tasks)Challenge 1: Read-only filesystem
Cloudron locks everything except
/app/data,/tmp, and/run. Docassemble writes to approximately 25 different filesystem paths during initialization and normal operation.Solution: Two categories of symlinks.
Persistent paths (survive restarts, backed up by Cloudron) are symlinked to
/app/data/:/usr/share/docassemble/config→/app/data/config/usr/share/docassemble/files→/app/data/files/usr/share/docassemble/log→/app/data/log/usr/share/docassemble/backup→/app/data/backup/usr/share/docassemble/certs→/app/data/certs/usr/share/docassemble/cron→/app/data/cron/usr/share/docassemble/local3.12(Python venv) →/app/data/venv
Volatile paths (regenerated every boot) are symlinked to
/run/:/etc/nginx/sites-availableand/etc/nginx/sites-enabled/var/log/nginx,/var/log/supervisor,/var/log/apache2/etc/ssl,/var/www,/var/cache/debconf/etc/localtime,/etc/timezone,/etc/locale.gen,/etc/locale.conf/var/lib/nginx,/var/lib/postgresql/etc/cron.daily,/etc/hasbeeninitialized
The Dockerfile creates all symlinks at build time. The
start.shentrypoint recreates volatile directories and seeds them from saved copies on every boot.Gotcha: Python's shutil.rmtree() refuses symlinks. Docassemble's
install_certs.pycallsshutil.rmtree()on/var/www/.certsand/etc/ssl/docassemble. If these are symlinks, Python raisesOSError: Cannot call rmtree on a symbolic link. The fix is to symlink the entire parent directory (/var/www→/run/www,/etc/ssl→/run/ssl) and seed them as real directories from saved copies at boot time.Gotcha: nginx temp directory. Nginx needs writable temp directories at
/var/lib/nginx/body,/var/lib/nginx/proxy, etc. Symlink/var/lib/nginx→/run/nginx-liband create the subdirectories instart.sh.Challenge 2: Replacing RabbitMQ
Docassemble uses RabbitMQ as the Celery message broker. Cloudron has no RabbitMQ addon, and RabbitMQ uses ~200 MB of RAM at rest (Erlang overhead).
Solution: LavinMQ. LavinMQ is an AMQP 0.9.1 wire-compatible message broker written in Crystal. It is a drop-in replacement: same protocol, same broker URL format (
amqp://guest:guest@localhost:5672), same Celery compatibility. Memory usage is ~40 MB at rest.The Dockerfile removes RabbitMQ and Erlang, then installs LavinMQ from the official apt repository. The
start.shscript starts LavinMQ before supervisord and waits for it to accept connections.Installation in the Dockerfile:
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ apt-get remove -y rabbitmq-server erlang-base erlang-nox || true && \ apt-get autoremove -y && \ curl -fsSL https://packagecloud.io/cloudamqp/lavinmq/gpgkey | \ gpg --dearmor -o /usr/share/keyrings/lavinmq.gpg && \ echo "deb [signed-by=/usr/share/keyrings/lavinmq.gpg] https://packagecloud.io/cloudamqp/lavinmq/ubuntu noble main" \ > /etc/apt/sources.list.d/lavinmq.list && \ apt-get update && \ apt-get install -y lavinmq && \ apt-get clean && \ rm -rf /var/lib/apt/lists/*Challenge 3: External PostgreSQL and Redis
Cloudron provides managed PostgreSQL and Redis as addons. Docassemble supports external services via environment variables and config.yml.
Solution: Map Cloudron env vars to docassemble env vars in start.sh, then generate config.yml on every boot.
Key mappings:
CLOUDRON_POSTGRESQL_HOST→DBHOSTCLOUDRON_POSTGRESQL_DATABASE→DBNAMECLOUDRON_POSTGRESQL_USERNAME→DBUSERCLOUDRON_POSTGRESQL_PASSWORD→DBPASSWORDCLOUDRON_REDIS_*→redis:directive in config.ymlCLOUDRON_APP_DOMAIN→DAHOSTNAMEBEHINDHTTPSLOADBALANCER=true(always)PORT=8080(matches manifest httpPort)
Gotcha: CONTAINERROLE must not be "all". Setting
CONTAINERROLE=alltellsinitialize.shto start internal PostgreSQL and Redis via supervisorctl, regardless of supervisor's autostart setting. SetCONTAINERROLE=web:celery:cron:log:mailto skip the sql, redis, and rabbitmq roles.Gotcha: Cloudron PostgreSQL does not use SSL. Do not set
ssl mode: requirein the database configuration. Cloudron's internal PostgreSQL rejects SSL connections.Gotcha: pg_hba.conf errors are non-fatal. Initialize.sh tries to connect to the
postgressystem database to check if the app database exists. Cloudron only allows connections to the app-specific database, so this check fails. It is harmless: the script falls through and connects to the correct database.Challenge 4: Supervisor overrides
We need to disable services that Cloudron provides or that we have replaced.
Solution: A supervisor conf file that sets
autostart=falsefor postgres, redis, rabbitmq, apache2, and syslogng.Gotcha: Must COPY in Dockerfile, not at runtime.
/etc/supervisor/conf.d/is read-only at runtime. The override must be placed there during the Docker build.Challenge 5: Python virtual environment persistence
Docassemble writes to its Python virtual environment at runtime (Playground package installs, pip config changes). The venv is ~2 GB.
Solution: Save the venv as a seed in the Docker image, then copy it to
/app/data/venvon first boot. The Dockerfile copies the venv to/app/code/seed-venv, then symlinks/usr/share/docassemble/local3.12→/app/data/venv. On first boot,start.shcopies the seed to persistent storage.Gotcha: Build service timeout. The 2 GB venv copy can time out on Cloudron's remote build service. If this happens, build locally with
docker buildand push to a registry.Challenge 6: config.yml regeneration
Cloudron may change environment variables between restarts (domain changes, credential rotation). Config.yml is regenerated from environment variables on every boot.
The
secretkey(used for encrypting session data) is generated once and persisted at/app/data/config/.secretso sessions survive restarts.
Part 3: Suggestions for Cloudron maintainers
Several platform improvements would make packaging complex apps like docassemble significantly easier.
1. Honour memoryLimit in manifests for custom apps
Currently, the
memoryLimitfield inCloudronManifest.jsonis ignored for custom (unpublished) apps. The default is 256 MB, which is insufficient for any substantial Python application. Users must manually set memory in the dashboard after installation, which is an undiscoverable step that causes confusing failures.Suggestion: Apply the manifest's
memoryLimitduring installation, or at minimum, show a warning if the default is being used despite a higher value in the manifest.2. Provide a LavinMQ addon
LavinMQ is a lightweight, AMQP 0.9.1 compatible message broker that could serve any app currently needing RabbitMQ. At ~40 MB RAM and a single binary, it is a natural fit for the addon model. A managed LavinMQ addon would simplify packaging for apps that use Celery, Sidekiq, or any AMQP-based task queue.
There is a separate forum request for LavinMQ on Cloudron.
3. Configurable stop timeout per app
Docassemble's upstream Docker setup uses
--stop-timeout 600for graceful shutdown. Cloudron's default stop timeout is shorter. A per-app configurable stop timeout (in the manifest or dashboard) would help apps that need time to flush state.4. Writable /etc subdirectories as an opt-in manifest option
Many complex apps write to
/etc/subdirectories during initialization (timezone, locale, SSL, cron). Currently, packagers must individually symlink each path. A manifest option like"writableEtcDirs": ["/etc/nginx", "/etc/ssl", "/etc/cron.daily"]would reduce boilerplate significantly.5. Build service timeout for large images
The Cloudron build service can time out when Dockerfile steps involve copying large directories (2 GB+ in our case). A configurable build timeout or a progress-aware timeout (that resets when output is being produced) would help.
Known limitations (v0.1)
- Memory must be set manually to ~8 GB in the Cloudron dashboard after installation
- First boot takes 15-20 minutes (database migration, venv copy)
- Exim4 (email receiving) does not work (read-only /etc/exim4). Outbound email via Cloudron's sendmail addon works fine.
- The
initializesupervisor process hangs after completing. Non-blocking but wastes some memory. - Playground package installs persist across restarts but not across Cloudron updates (the venv is re-seeded from the image on update)
- No OIDC/LDAP integration with Cloudron user management yet
Repository
https://github.com/OrcVole/docassemble-cloudron
Contains:
Dockerfile,start.sh,CloudronManifest.json,supervisor/docassemble-cloudron.conf,DESCRIPTION.md,icon.png, documentation.Tested on Cloudron 9.x with the
jhpyle/docassemble:latestimage (v1.8.17, Ubuntu 24.04, Python 3.12).Feedback, bug reports, and pull requests welcome.
Copy the ~2 GB Python virtual environment to persistent storage
I'm not a python expert, well, actually I'm not any kind of expert, but in the content of a single isolated container, I am not sure that
venvis needed.
Maybe withoutvenvit might be a tad quicker ? -
Well done for building it
go to your Cloudron dashboard, find the docassemble app, open its settings, and set the memory limit to at least 8 GB (8192 MiB)
odd, my deploy does set a custom memory limit during publication
how are you expressing the number ?@timconsidine Good question. The manifest has
"memoryLimit": 6144which should work, but during our testing Cloudron applied the default 256 MB regardless. We confirmed this by checking the cgroup limit from inside the container:cat /sys/fs/cgroup/memory.maxIt returned 268435456 (256 MB) despite the manifest specifying 6144. This was with
cloudron installfrom the CLI for an unpublished custom app. Once we set it manually in the dashboard it applied correctly and the app started working.It is possible this behaves differently for published apps versus custom installs. If your deploys do honour the manifest value, that is useful to know and we may have hit a bug specific to unpublished packages. Would be worth a question to the Cloudron team.
-
git clone [your-repo-url] docassemble-cloudron
you didn't actually post your code. So far you post is just one big ai hallucination without something one can actually test
.Edit: ah it was buried at the very end.
@fbartels Fair point, the repo link should have been more prominent. It is here:
https://github.com/OrcVole/docassemble-cloudron
Five files: Dockerfile, start.sh, CloudronManifest.json, supervisor/docassemble-cloudron.conf, and DESCRIPTION.md. The write-up above documents what each file does and the problems you will hit at each stage, so you are not flying blind.
-
Wow what a RAM beast!
Nice work on saving 150MB RAM with LavinMQ, and proving it's suitable as an AddOn.
Could run a business for access to just this one app..
@robi It is a beast, yes. The 8 GB figure is somewhat misleading though. The app runs comfortably in about 2 GB during normal operation. The high allocation is needed because the first-boot database migration and Python package table population spike memory usage briefly, and the OOM killer is unforgiving. An official package could potentially pre-seed the database schema in the Docker image and bring the runtime requirement down to 3-4 GB.
On LavinMQ: it really is a clean drop-in for RabbitMQ. Same AMQP 0.9.1 protocol, same Celery broker URL format, Celery does not know the difference. There is a separate request for LavinMQ as a Cloudron addon which would simplify this and any other app that needs a message broker.
-
Copy the ~2 GB Python virtual environment to persistent storage
I'm not a python expert, well, actually I'm not any kind of expert, but in the content of a single isolated container, I am not sure that
venvis needed.
Maybe withoutvenvit might be a tad quicker ?@timconsidine You are right that in a normal Docker container you would not need a venv at all, the container IS the isolation. The reason docassemble uses one is that its Playground feature lets users install additional Python packages at runtime through the web interface. Those pip installs need a writable location, and the upstream image puts everything in a venv at
/usr/share/docassemble/local3.12so thatwww-datacan write to it without root.On Cloudron the container filesystem is read-only, so we have to copy that venv to the writable
/app/data/partition on first boot. That is the slow step. On subsequent restarts it is already there and boots in 2-3 minutes.If you did not need the Playground install feature, you could skip the copy and leave the venv read-only in the image. The app would still run, you just could not install extra packages from the browser.
-
@timconsidine Good question. The manifest has
"memoryLimit": 6144which should work, but during our testing Cloudron applied the default 256 MB regardless. We confirmed this by checking the cgroup limit from inside the container:cat /sys/fs/cgroup/memory.maxIt returned 268435456 (256 MB) despite the manifest specifying 6144. This was with
cloudron installfrom the CLI for an unpublished custom app. Once we set it manually in the dashboard it applied correctly and the app started working.It is possible this behaves differently for published apps versus custom installs. If your deploys do honour the manifest value, that is useful to know and we may have hit a bug specific to unpublished packages. Would be worth a question to the Cloudron team.
The manifest has "memoryLimit": 6144 which should work
I don't think this is the correct notation, Cloudron overrides it and puts 256Mb
My Dify package sets this for 4Gb
"memoryLimit": 4294967296,
double it for 8Gb ? -
It has been mentioned over on the Docassemble github and hopefully some of the good people there will visit and help us make this an officially supported application.
Oh, @robi , we just tried resizing the RAM resources to 4.25GB and now that it has been setup, it seems to be running well on that.
-
@timconsidine You are right that in a normal Docker container you would not need a venv at all, the container IS the isolation. The reason docassemble uses one is that its Playground feature lets users install additional Python packages at runtime through the web interface. Those pip installs need a writable location, and the upstream image puts everything in a venv at
/usr/share/docassemble/local3.12so thatwww-datacan write to it without root.On Cloudron the container filesystem is read-only, so we have to copy that venv to the writable
/app/data/partition on first boot. That is the slow step. On subsequent restarts it is already there and boots in 2-3 minutes.If you did not need the Playground install feature, you could skip the copy and leave the venv read-only in the image. The app would still run, you just could not install extra packages from the browser.
On Cloudron the container filesystem is read-only, so we have to copy that venv to the writable /app/data/ partition on first boot.
Understood but it seems a strange way to do it.
My instinct would be to get setup.sh to create the neededvenvon first run, not pre-create and copy.
That way, it would survive upgrades.Maybe there is a technical reason for your approach.
EDIT : My AI friend says my instinct is wrong

-
@loudlemur if you conquered the mammoth build process, it would be easy to make it a community app for people to try out easier ?
-
@loudlemur do I understand correctly that you are not using the Cloudron base image ?
I think this would disqualify it from getting into the AppStore.
But maybe that's not your ambition. -
@loudlemur if you conquered the mammoth build process, it would be easy to make it a community app for people to try out easier ?
@timconsidine Yes, I hope it becomes a community application, but I need to turn my attention elsewhere at the moment. Could you put it on your app, tim?
-
@loudlemur do I understand correctly that you are not using the Cloudron base image ?
I think this would disqualify it from getting into the AppStore.
But maybe that's not your ambition.@timconsidine No, we did not use the Cloudron base image. We built directly FROM jhpyle/docassemble:latest. The reason is pragmatic: docassemble's upstream image contains hundreds of carefully configured components (texlive, pandoc, imagemagick, LibreOffice, a full Python environment with ~200 packages, supervisord configs, nginx templates, dozens of shell scripts). Rebuilding all of that on top of cloudron/base would have been a multi-week project and a permanent maintenance burden, because every upstream docassemble release could change dependencies, scripts, or paths.
By building on top of the upstream image, we inherit all of that and only add our adaptation layer (LavinMQ swap, symlinks, start.sh, supervisor overrides). When docassemble releases a new version, updating is just a rebuild that pulls the new upstream image.The tradeoff is that we lose some Cloudron-native tooling that the base image provides (like gosu and some helper scripts), and our image is larger than it would be with a clean build. For an official Cloudron package, rebuilding on cloudron/base would be the right approach, but for a community custom app, tracking upstream is far more sustainable.
(ATTENTION DOCASSEMBLE DEVELOPERS)
The strongest path would be a hybrid approach, similar to how the Baserow Cloudron package evolved. There are two realistic strategies, and they have different tradeoffs.
Strategy 1: Upstream-first (recommended)
Work with Jonathan Pyle (docassemble maintainer) to improve the upstream Docker image's support for constrained environments. The specific changes that would make official Cloudron packaging dramatically easier:
First, extend DAREADONLYFILESYSTEM to cover all write paths, not just the 25 it currently gates. If the upstream image could run cleanly with that flag set to true and all state under a single configurable data directory, the Cloudron package would be straightforward.
Second, make initialize.sh respect CONTAINERROLE fully. If sql is not in the role, the script should never touch PostgreSQL, not even to check if it is running. Same for Redis and RabbitMQ.
Third, support a DA_DATA_DIR environment variable that relocates all writable state to a single directory. Docassemble already partially supports this with DA_ROOT, but many paths are hardcoded outside of it (/var/www/.certs, /etc/ssl/docassemble, /var/lib/nginx, etc).
If those three changes landed upstream, an official Cloudron package could be built on cloudron/base:5.0 with a clean Dockerfile that installs docassemble from pip into a venv, uses Cloudron addons for PostgreSQL and Redis, and needs very few symlink hacks.
Strategy 2: Clean-room rebuild on cloudron/base (harder, more maintainable long-term)
Build from cloudron/base:5.0 and install docassemble's Python packages directly, bypassing the upstream Docker image entirely. This is how most official Cloudron packages work. The Dockerfile would install the system dependencies (texlive, pandoc, imagemagick, poppler, LibreOffice), create a Python venv, pip install the three docassemble packages (docassemble.base, docassemble.webapp, docassemble.demo), and configure nginx, supervisord, and LavinMQ from scratch.
The advantage is a smaller, cleaner image that follows Cloudron conventions properly. The disadvantage is substantial: docassemble has over 100 system dependencies and the upstream initialize.sh contains years of accumulated logic for database migrations, package management, certificate handling, and configuration templating. Replicating all of that correctly would take weeks, and every upstream release could break it.
What I would actually recommend to someone volunteering for this:
Start from our working v0.1 package and incrementally replace the upstream image. The sequence would be:Fork our repo and get it running on your own Cloudron. Confirm everything works.
Open a conversation with the docassemble maintainer (the GitHub issue we just filed is a good starting point). Propose the DA_DATA_DIR and DAREADONLYFILESYSTEM improvements. Jonathan Pyle has been maintaining this project for years and is responsive.
While waiting for upstream changes, extract the initialize.sh logic you actually need. Most of it (S3 support, Azure blob storage, multi-server clustering, Let's Encrypt, internal PostgreSQL/Redis management) is irrelevant on Cloudron. A stripped-down init script that only handles database migration, package table population, nginx config generation, and service startup would be perhaps 200 lines instead of 1800.
Once you have a minimal init script, rebuild on cloudron/base:5.0. Install system dependencies from a known-good list (extractable from the upstream Dockerfile), pip install the docassemble packages, and use your minimal init script instead of the upstream one.
Add LDAP or OIDC integration for Cloudron single sign-on. Docassemble supports LDAP. This is expected for official Cloudron apps.
Submit to the Cloudron app store via cloudron versions init and the publication workflow. -
Great news! Jonathan Pyle likes this effort and he is going to take a look. He suggested this:
"If your platform supports Docker Compose, you might want to look at https://github.com/jhpyle/docassemble-compose, which shows how you can cut out the jhpyle/docassemble image, supervisord, and any service you are hosting externally."
-
Great news! Jonathan Pyle likes this effort and he is going to take a look. He suggested this:
"If your platform supports Docker Compose, you might want to look at https://github.com/jhpyle/docassemble-compose, which shows how you can cut out the jhpyle/docassemble image, supervisord, and any service you are hosting externally."
@LoudLemur .... but Cloudron does NOT support Docker Compose, so ...
-
for a community custom app ...
You raise an interesting point. Just because it is a community app does not mean it can be any base image. Community apps should be compliant to Cloudron standards. IMHO. If not, you might as well set up a separate VPS and deploy via docker compose, why have the hassle of cloudron compliance.
I do see the other view point, I just don't agree with it.Rebuilding all of that on top of cloudron/base would have been a multi-week project
I started from scratch using a cloudron base image, none of your code, different direction, at 17:16, took 2 hours out for supper & TV, then returned to the app. 1st working version at 21:08. So roughly 2 hours.

I'm not saying that the app is fully built. But to get a Home Screen loaded is significant. I don't know the app, so not sure what to do next. That might take another couple hours.
Not seeking an argument, I just don't agree that your approach to build from upstream image is correct.
-
for a community custom app ...
You raise an interesting point. Just because it is a community app does not mean it can be any base image. Community apps should be compliant to Cloudron standards. IMHO. If not, you might as well set up a separate VPS and deploy via docker compose, why have the hassle of cloudron compliance.
I do see the other view point, I just don't agree with it.Rebuilding all of that on top of cloudron/base would have been a multi-week project
I started from scratch using a cloudron base image, none of your code, different direction, at 17:16, took 2 hours out for supper & TV, then returned to the app. 1st working version at 21:08. So roughly 2 hours.

I'm not saying that the app is fully built. But to get a Home Screen loaded is significant. I don't know the app, so not sure what to do next. That might take another couple hours.
Not seeking an argument, I just don't agree that your approach to build from upstream image is correct.
@timconsidine That is fantastic, Tim!
I was just trying a version 2 using the Docker Compose approach, but if you have managed to do it on the Cloudron base, that would be much better.
Hello! It looks like you're interested in this conversation, but you don't have an account yet.
Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.
With your input, this post could be even better 💗
Register Login