Cheat sheet
This cheat sheet covers various Cloudron-specific considerations, caveats and best practices when packaging apps.
Dockerfile.cloudron
If you already have an existing Dockerfile in your project, you can name the Cloudron-specific Dockerfile as
Dockerfile.cloudron or cloudron/Dockerfile.
Examples
Our existing app packages are tagged by framework/language. Ask for help in our 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
The app container has a readonly file system. Writing to any location at runtime other than the below results in 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 any required packages in the Dockerfile.
- Create static configuration files in the Dockerfile.
- Create symlinks to dynamic configuration files (for e.g a generated config.php) under
/runin the Dockerfile.
One-time init
Apps often require initialization the first time they are installed. Use the database or filesystem to track the app's initialization state. For example, create a /app/data/.initialized file to track the status. Save this file in /app/data as this is the only location persisted 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 the ownership of the files to match the app's user id before the app starts. Ownership information can be "lost" across backup/update/restore. For example, when 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 you might need to change permissions to www-data:www-data instead.
Start script
Many apps do not launch the server directly. Instead, they execute a start.sh script (named by convention, you can use any name) as the app entry point.
At the end of the Dockerfile, add your start script (start.sh) and set it as the default command. Make the start.sh executable in the app package repo using chmod +x start.sh.
ADD start.sh /app/code/start.sh
CMD [ "/app/code/start.sh" ]
Non-root user
Cloudron runs the start.sh as root user. Various commands like chown require this. However, to keep the app and cloudron secure, always run the app with the least required permissions.
The gosu tool lets you run a binary with a specific user/group as follows:
/usr/local/bin/gosu cloudron:cloudron node /app/code/.build/bundle/main.js
Environment variables
The following environment variables are set as part of the 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_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 |
You can set custom environment variables using cloudron env.
Logging
Apps stream their logs to stdout and stderr. Logging to stdout has many advantages:
- App does not need to rotate logs - the platform takes care of managing logs.
- App does not need special mechanism to release log file handles (on a log rotate).
- Integrates better with tooling like the CLI.
Achieving this ideal can be difficult. Some programs like Apache do not log to stdout. In such cases, log to a subdirectory in /run (two levels deep) into files with .log extension. These logs are autorotated.
Multiple processes
Docker supports restarting processes natively. Docker restarts your application automatically when it crashes. Single process applications do not require any process manager.
Use supervisor, pm2 or any other process manager for applications with more than one component. This excludes web servers like apache, nginx as they already manage their children. Pick a process manager that forwards signals to child processes.
Supervisor
Supervisor can be configured to send the app's output to stdout as follows:
[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). This can be changed using the memoryLimit
field in the manifest.
Design your app runtime for concurrent use by 100s of users. The platform is not designed for concurrent access by 1000s of users.
An app can determine its memory limit by reading /sys/fs/cgroup/memory/memory.limit_in_bytes if the system uses groups v1 or /sys/fs/cgroup/memory.max for 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 automatically forward signals to child processes by default. A SIGTERM sent to the parent processes does not reach the children. Use exec as the last line of the start.sh script. Programs like gosu, nginx, apache do proper SIGTERM handling.
For example, start apache using exec as below:
echo "Starting apache"
APACHE_CONFDIR="" source /etc/apache2/envvars
rm -f "${APACHE_PID_FILE}"
exec /usr/sbin/apache2 -DFOREGROUND
Debugging
To inspect the filesystem of a running app, use cloudron exec.
When an application keeps restarting due to a bug, cloudron exec will not work or will keep getting disconnected. Use cloudron debug for these situations. In debug mode, the container's file system is read-write. The app also pauses and does not run the RUN command specified in the Dockerfile.
You can turn off debugging mode using cloudron debug --disable.
Popular stacks
Apache
Apache requires some configuration changes to work properly. The following commands configure Apache in the following way:
- 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
Afterwards, add your site config to Apache:
ADD apache2.conf /etc/apache2/sites-available/app.conf
RUN a2ensite app
In start.sh Apache can be started using these commands:
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 backend programs based on the request route or other characteristics. Run nginx and the application through a process manager like supervisor in this case.
Example nginx supervisor configuration file:
[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 nginx configuration, provided with the base image, can be used by adding 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>
Since the base image nginx configuration is unpatched from the ubuntu package, the app configuration has to ensure nginx is using /run/ instead of /var/lib/nginx/ to support the read-only filesystem nature of an app.
Example nginx app config file:
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;
}
}
PHP
PHP wants to store session data at /var/lib/php/sessions which is read-only. To fix this problem you can move this data to /run/php/sessions with these commands:
RUN rm -rf /var/lib/php/sessions && ln -s /run/php/sessions /var/lib/php/sessions
Don't forget to create this directory and set its ownership in the start.sh:
mkdir -p /run/php/sessions
chown www-data:www-data /run/php/sessions
Java
Java scales its memory usage dynamically according to the available system memory. Due to how Docker works, Java sees the hosts total memory instead of the memory limit of the app. To restrict Java to the apps memory limit it is necessary to add a special parameter to Java calls.
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 ...