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. Child images (php-base, node-base, python-base) use series-YYYYMMDD tags, not floating :8.5, :24, or :3.14. cloudron/base stays semver.

App typeImageNotes
No Node / PHP / pipcloudron/base:6.0.0@sha256:9bed4c8fa880645f8e669041ee28febe941481d00e9445e3e5a5483cb541d09bUbuntu 26.04. No PHP, Node, pip/venv, Apache, or mongo tools. nginx-full, supervisor, and ImageMagick stay.
Node 24cloudron/node-base:24-20260920@sha256:d984683ec59bf2379130bf41cf3c2b6bc0453f327297d7183525cb05424e7b34Latest 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-20260920@sha256:a59a4334fbf50ebfdf8f6cc5527e099e4586349a1aefc546e85f7401ea366d65Prefer when the app supports PHP 8.5. Apache, Composer, and ghostscript (for Imagick PDF) are here.
PHP 8.4cloudron/php-base:8.4-20260920@sha256:e9352b5fba7ad0a231b8a9e454034400a3a34a7fe1800425f634d32ffcae4a52Use when the app is not ready for 8.5.

Do not put PHP, Node, or pip/venv back into cloudron/base.

Node apps use cloudron/node-base. Node is already on PATH. Do not set ENV PATH=/usr/local/node-…/bin:$PATH. Apps that need a Node major other than 24 stay on cloudron/base and install their own tarball.

Python apps use cloudron/python-base and python3 -m venv. uv / conda apps stay on cloudron/base and install their own interpreter. Node or PHP apps that need a Python sidecar stay on node-base / php-base and apt-get install python3-venv (use install, not reinstall).

Multi-PHP stacks such as LAMP stay on cloudron/base and install Apache and PHP themselves. A static Apache site (no PHP) also installs apache2 — 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​

The app container has a read-only file system. Writing at runtime to any location other than those listed below 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​

Apps often require initialization on first install. Use the database or filesystem to track initialization state. 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​

Many apps use a start.sh script (named by 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​

The start.sh script runs as root. Commands like chown require root privileges. However, always run the app itself with the least required permissions to maintain security.

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​

Apps stream their logs to stdout and stderr. Logging to stdout 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.

Not all programs log to stdout (e.g., Apache). For these, log to a subdirectory in /run (two levels deep) into files with a .log extension. These logs are autorotated.

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​

By default, apps get 256MB RAM (including swap). Adjust this with the memoryLimit field in the manifest.

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​

Apache is included in cloudron/php-base. 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​

Nginx serves as a reverse proxy in front of the application, dispatching 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

The base image provides a default Nginx configuration. 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>

The base image uses the unpatched Ubuntu Nginx configuration. Configure the app to use /run/ instead of /var/lib/nginx/ to support the read-only filesystem.

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/).

PHP stores session data in /var/lib/php/sessions, which is read-only. Move session data to /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​

Java scales memory usage based on available system memory. Inside Docker, Java sees the host's total memory rather than the container's memory limit. Restrict Java to the app's memory limit by adding 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.