Cheat sheet
This cheat sheet covers Cloudron-specific considerations, caveats, and best practices for packaging apps.
Dockerfile.cloudron
If your project already has a Dockerfile, name the Cloudron-specific Dockerfile
Dockerfile.cloudron or cloudron/Dockerfile.
Base images
Pin the dated tag and digest in FROM. Pin child images (php-base, node-base, python-base) to a series-YYYYMMDD tag. Do not use a floating :8.5, :24, or :3.14 tag. Keep cloudron/base on its semver tag.
| App type | Image | Notes |
|---|---|---|
| No Node / PHP / pip | cloudron/base:6.0.1@sha256:7b1e729960c3ca312d26e886917ba89483241bba0219ff292858e10d9d5768ee | Ubuntu 26.04. No PHP, Node, pip/venv, Apache, or mongo tools. nginx-full, supervisor, and ImageMagick stay. |
| Node 24 | cloudron/node-base:24-20261021@sha256:7435d8aca138ce01cfca4cd7ec05683223b8f5d7198c9abdaaefa1b6ba14b4c6 | Latest Node 24.x at /usr/local/node, on PATH. |
| Python 3.14 | cloudron/python-base:3.14-20260920@sha256:f61b3964a044414c8dcdcd897aea39629e3d73a21e48c01492e4c4a82753f891 | Distro Python, venv, pip. Prefer when the app supports it. |
| Python 3.13 | cloudron/python-base:3.13-20260920@sha256:32e0d51808c692ab7fe6dd51c98238d6b15b1102462e5d7ade954cf48f5acbf2 | When upstream pins 3.13. |
| Python 3.12 | cloudron/python-base:3.12-20260920@sha256:49c5a00c1110ec76cb5122e48c26dbc26a1c04e020c9ee230be95272c66eb86b | When upstream pins 3.12. |
| Python 3.11 | cloudron/python-base:3.11-20260920@sha256:b667f60283768162b46c84f69619e13edbf6c1e1a4ccde3c1d828d7d631f5f0b | When upstream pins 3.11. |
| PHP 8.5 | cloudron/php-base:8.5-20261021@sha256:5a90e896dfa57058acb1384ad25bfd668c1744737ef56fb0c287cd110feaf99a | Prefer when the app supports PHP 8.5. Apache, Composer, and ghostscript (for Imagick PDF) are here. |
| PHP 8.4 | cloudron/php-base:8.4-20261021@sha256:476ced2a7fd301a9cdd5aa8e8604b069215fd9cc829fe1a157829741e61ffb76 | Use when the app is not ready for 8.5. |
For a Node 24 app, start from cloudron/node-base. Node is already on PATH.
For a Python app, start from cloudron/python-base and create the virtualenv with python3 -m venv. If the app uses uv or conda, start from cloudron/base and install that interpreter in the Dockerfile. If a Node or PHP app only needs Python alongside it, stay on node-base or php-base and run apt-get install python3-venv. Use install, not reinstall.
For a multi-PHP stack such as LAMP, start from cloudron/base and install Apache and PHP in the Dockerfile. For a static Apache site with no PHP, install apache2 the same way. Apache is not in cloudron/base.
Examples
Existing app packages are tagged by framework and language. Ask for help in the forum.
- https://git.cloudron.io/explore/projects?tag=php
- https://git.cloudron.io/explore/projects?tag=java
- https://git.cloudron.io/explore/projects?tag=rails
- https://git.cloudron.io/explore/projects?tag=ruby
- https://git.cloudron.io/explore/projects?tag=node
- https://git.cloudron.io/explore/projects?tag=meteor
- https://git.cloudron.io/explore/projects?tag=python
- https://git.cloudron.io/explore/projects?tag=rust
- https://git.cloudron.io/explore/projects?tag=nginx
- https://git.cloudron.io/explore/projects?tag=go
Filesystem
Read-only
Treat the container filesystem as read-only at runtime. Write only to the locations below. Writing anywhere else produces an error:
| Dir | Description |
|---|---|
/tmp | Use this location for temporary files. Files in this directory are cleaned up periodically. |
/run | Use this location for runtime configuration and dynamic data. These files do not persist across app restarts (for example, after an update or a crash). |
/app/data | Use this location to store app data that is to be backed up. To use this location, you must use the localstorage addon. |
| Other paths | Not writable |
Suggestions for creating the Dockerfile:
- Install required packages in the Dockerfile.
- Create static configuration files in the Dockerfile.
- Create symlinks to dynamic configuration files (e.g., a generated config.php) under
/runin the Dockerfile.
One-time init
Initialize the app on first install. Track that state in the database or on the filesystem. For example, create a /app/data/.initialized file. Only /app/data persists across restarts and updates.
if [[ ! -f /app/data/.initialized ]]; then
echo "Fresh installation, setting up data directory..."
# Setup commands here
touch /app/data/.initialized
echo "Done."
fi
File ownership
When storing files under /app/data, change file ownership to match the app's user ID before the app starts. Backups, updates, and restores can reset ownership. For example, if the app runs as the cloudron user:
# Change ownership of files
chown -R cloudron:cloudron /app/data
# Start the app
gosu cloudron:cloudron npm start
For Apache+PHP apps, use www-data:www-data instead.
Start script
Use a start.sh script (the name is a convention; any name works) as the entry point instead of launching the server directly.
Add your start script (start.sh) at the end of the Dockerfile and set it as the default command. Mark start.sh as executable in the app package repo with chmod +x start.sh.
ADD start.sh /app/code/start.sh
CMD [ "/app/code/start.sh" ]
Non-root user
start.sh runs as root, because commands like chown require it. Run the app itself with the least permissions required.
Use the gosu tool to run a binary with a specific user/group:
/usr/local/bin/gosu cloudron:cloudron node /app/code/.build/bundle/main.js
Environment variables
These environment variables are available at app runtime:
| Name | Description |
|---|---|
CLOUDRON | Set to '1'. This is useful for writing platform-specific code |
CLOUDRON_ALIAS_DOMAINS | Set to the domain aliases. Only set when multiDomain flag is enabled |
CLOUDRON_API_ORIGIN | Set to the HTTP(S) origin of the API. For example, https://my.example.com |
CLOUDRON_APP_DOMAIN | The domain name of the app. For example, app.example.com |
CLOUDRON_APP_ORIGIN | The HTTP(s) origin of the app. For example, https://app.example.com |
CLOUDRON_DEBUG | Set to '1' when the app is in recovery mode |
CLOUDRON_PROXY_IP | The IP address of the reverse proxy. Apps can trust the HTTP headers (like X-Forwarded-For) for requests originating from this IP address. |
CLOUDRON_WEBADMIN_ORIGIN | The HTTP(S) origin of the dashboard. For example, https://my.example.com |
Set custom environment variables with cloudron env.
Logging
Write logs to stdout and stderr. That has several advantages:
- No log rotation needed - the platform manages logs.
- No special mechanism needed to release log file handles on rotation.
- Better integration with tooling like the CLI.
If the program does not log to stdout (for example, Apache), write logs under /run (two levels deep) in files with a .log extension. Those logs are rotated automatically.
Multiple processes
Docker natively restarts crashed processes, so single-process applications do not require a process manager.
Use supervisor, pm2, or any other process manager for applications with more than one component. This excludes web servers like Apache and Nginx, which already manage their child processes. Pick a process manager that forwards signals to child processes.
Supervisor
Configure Supervisor to send app output to stdout:
[program:app]
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
Memory limit
Adjust the memory limit with the memoryLimit field in the manifest. The default is 256MB RAM (including swap).
Design your app runtime for concurrent use by hundreds of users. The platform does not support concurrent access by thousands of users.
Determine the memory limit by reading /sys/fs/cgroup/memory/memory.limit_in_bytes on cgroups v1 or /sys/fs/cgroup/memory.max on cgroups v2.
For example, to spin one worker for every 150M RAM available to the app:
if [[ -f /sys/fs/cgroup/cgroup.controllers ]]; then # cgroup v2
memory_limit=$(cat /sys/fs/cgroup/memory.max)
[[ "${memory_limit}" == "max" ]] && memory_limit=$(( 2 * 1024 * 1024 * 1024 )) # "max" really means unlimited
else
memory_limit=$(cat /sys/fs/cgroup/memory/memory.limit_in_bytes) # this is the RAM. we have equal amount of swap
fi
worker_count=$((memory_limit/1024/1024/150)) # 1 worker for 150M
worker_count=$((worker_count > 8 ? 8 : worker_count )) # max of 8
worker_count=$((worker_count < 1 ? 1 : worker_count )) # min of 1
SIGTERM handling
Bash does not forward signals to child processes by default. A SIGTERM sent to the parent process does not reach the children. Use exec as the last line of start.sh. Programs like gosu, Nginx, and Apache handle SIGTERM correctly.
For example, start Apache with exec:
echo "Starting apache"
APACHE_CONFDIR="" source /etc/apache2/envvars
rm -f "${APACHE_PID_FILE}"
exec /usr/sbin/apache2 -DFOREGROUND
Debugging
Inspect the filesystem of a running app with cloudron exec.
When an application keeps restarting due to a bug, cloudron exec may fail or disconnect repeatedly. Use cloudron debug instead. In debug mode, the container's file system is read-write, and the app pauses without running the RUN command specified in the Dockerfile.
Disable debug mode with cloudron debug --disable.
Popular stacks
Apache
On cloudron/php-base, Apache is already installed. On cloudron/base, install it with apt-get install apache2.
Apache requires configuration changes for the Cloudron environment. The following commands:
- Disable all default sites
- Print errors into the app's log and disable other logs
- Limit server processes to
5(good default value) - Change the port number to the default
8000
RUN rm /etc/apache2/sites-enabled/* \
&& sed -e 's,^ErrorLog.*,ErrorLog "/dev/stderr",' -i /etc/apache2/apache2.conf \
&& sed -e "s,MaxSpareServers[^:].*,MaxSpareServers 5," -i /etc/apache2/mods-available/mpm_prefork.conf \
&& a2disconf other-vhosts-access-log \
&& echo "Listen 8000" > /etc/apache2/ports.conf
Then add your site configuration to Apache:
ADD apache2.conf /etc/apache2/sites-available/app.conf
RUN a2ensite app
Start Apache in start.sh:
echo "Starting apache..."
APACHE_CONFDIR="" source /etc/apache2/envvars
rm -f "${APACHE_PID_FILE}"
exec /usr/sbin/apache2 -DFOREGROUND
Nginx
Use Nginx as a reverse proxy in front of the application, and dispatch to different backends based on the request route or other characteristics. Run Nginx and the application through a process manager like Supervisor.
Example Nginx Supervisor configuration:
[program:nginx]
directory=/tmp
command=/usr/sbin/nginx -g "daemon off;"
user=root
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
Start from the default Nginx configuration in the base image. Add an application-specific config file under /etc/nginx/sites-enabled/ when building the Docker image:
ADD <app config file> /etc/nginx/sites-enabled/<app config file>
Point Nginx temporary paths at /run/ instead of /var/lib/nginx/. The base image ships the stock Ubuntu configuration, which writes under /var/lib/nginx/.
Example Nginx app configuration:
client_body_temp_path /run/client_body;
proxy_temp_path /run/proxy_temp;
fastcgi_temp_path /run/fastcgi_temp;
scgi_temp_path /run/scgi_temp;
uwsgi_temp_path /run/uwsgi_temp;
server {
listen 8000;
root /app/code/dist;
location /api/v1/ {
proxy_pass http://127.0.0.1:8001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
}
Python
Start from cloudron/python-base (not cloudron/base). Create a venv and install with pip:
RUN python3 -m venv /app/code/venv
RUN /app/code/venv/bin/pip install -r /app/code/requirements.txt
Use :3.13-YYYYMMDD / :3.12-YYYYMMDD / :3.11-YYYYMMDD (same date, matching digest from the table) when upstream pins that series.
PHP
Start from cloudron/php-base (not cloudron/base). Pin the dated tag (8.5-YYYYMMDD or 8.4-YYYYMMDD) and digest. Configure Apache/php.ini under /etc/php/<series>/ (for example /etc/php/8.5/).
Move PHP session data out of /var/lib/php/sessions. That directory is read-only. Point it at /run/php/sessions:
RUN rm -rf /var/lib/php/sessions && ln -s /run/php/sessions /var/lib/php/sessions
Create this directory and set its ownership in start.sh:
mkdir -p /run/php/sessions
chown www-data:www-data /run/php/sessions
Java
Cap Java at the app's memory limit. Java sizes its heap from the host's memory, not the container limit. Set a JVM parameter:
if [[ -f /sys/fs/cgroup/cgroup.controllers ]]; then # cgroup v2
ram=$(cat /sys/fs/cgroup/memory.max)
[[ "${ram}" == "max" ]] && ram=$(( 2 * 1024 * 1024 * 1024 )) # "max" means unlimited
else
ram=$(cat /sys/fs/cgroup/memory/memory.limit_in_bytes) # this is the RAM. we have equal amount of swap
fi
ram_mb=$(numfmt --to-unit=1048576 --format "%fm" $ram)
export JAVA_OPTS="-XX:MaxRAM=${ram_mb}M"
java ${JAVA_OPTS} -jar ...
See also
The Tutorial covers the full build-install-update development loop.