Skip to main content

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 typeImageNotes
No Node / PHP / pipcloudron/base:6.0.1@sha256:7b1e729960c3ca312d26e886917ba89483241bba0219ff292858e10d9d5768eeUbuntu 26.04. No PHP, Node, pip/venv, Apache, or mongo tools. nginx-full, supervisor, and ImageMagick stay.
Node 24cloudron/node-base:24-20261021@sha256:7435d8aca138ce01cfca4cd7ec05683223b8f5d7198c9abdaaefa1b6ba14b4c6Latest Node 24.x at /usr/local/node, on PATH.
Python 3.14cloudron/python-base:3.14-20260920@sha256:f61b3964a044414c8dcdcd897aea39629e3d73a21e48c01492e4c4a82753f891Distro Python, venv, pip. Prefer when the app supports it.
Python 3.13cloudron/python-base:3.13-20260920@sha256:32e0d51808c692ab7fe6dd51c98238d6b15b1102462e5d7ade954cf48f5acbf2When upstream pins 3.13.
Python 3.12cloudron/python-base:3.12-20260920@sha256:49c5a00c1110ec76cb5122e48c26dbc26a1c04e020c9ee230be95272c66eb86bWhen upstream pins 3.12.
Python 3.11cloudron/python-base:3.11-20260920@sha256:b667f60283768162b46c84f69619e13edbf6c1e1a4ccde3c1d828d7d631f5f0bWhen upstream pins 3.11.
PHP 8.5cloudron/php-base:8.5-20261021@sha256:5a90e896dfa57058acb1384ad25bfd668c1744737ef56fb0c287cd110feaf99aPrefer when the app supports PHP 8.5. Apache, Composer, and ghostscript (for Imagick PDF) are here.
PHP 8.4cloudron/php-base:8.4-20261021@sha256:476ced2a7fd301a9cdd5aa8e8604b069215fd9cc829fe1a157829741e61ffb76Use 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.

Filesystem​

Read-only​

Treat the container filesystem as read-only at runtime. Write only to the locations below. Writing anywhere else produces an error:

DirDescription
/tmpUse this location for temporary files. Files in this directory are cleaned up periodically.
/runUse 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/dataUse this location to store app data that is to be backed up. To use this location, you must use the localstorage addon.
Other pathsNot 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 /run in 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:

NameDescription
CLOUDRONSet to '1'. This is useful for writing platform-specific code
CLOUDRON_ALIAS_DOMAINSSet to the domain aliases. Only set when multiDomain flag is enabled
CLOUDRON_API_ORIGINSet to the HTTP(S) origin of the API. For example, https://my.example.com
CLOUDRON_APP_DOMAINThe domain name of the app. For example, app.example.com
CLOUDRON_APP_ORIGINThe HTTP(s) origin of the app. For example, https://app.example.com
CLOUDRON_DEBUGSet to '1' when the app is in recovery mode
CLOUDRON_PROXY_IPThe 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_ORIGINThe 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.

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.