Firstimer - a long way to complete my first app package
I am not a developer, though I have been working with computers for nearly 40 years. I use apps on Cloudron, configure them to suit my specific use cases, explore new technologies, and orchestrate free and open-source tools to move closer to achieving digital sovereignty.
Waiting for new apps to appear in the Cloudron app catalog is one way to address missing features. Last weekend, I decided to explore what it takes to get a new app up and running.
In this forum post, I shared my thoughts on a missing piece for de-Googled Android phones: a FindMyDevice server. To be fair, I’ve never lost a phone or been robbed, but having everything set up just in case gives me peace of mind.
I had heard of Docker before and experimented with Dockerfiles and docker-compose. I also tried out the beginner apps mentioned in the Cloudron documentation and, in the past, set up a private registry and the Cloudron build service. So, the mise en place was already done before getting started. The rest should be easy.
TL;DR: It took me nearly 8 hours to get the app running, ensure it worked properly, and make sure individual configuration changes persisted through app updates.
Basics - where to start
While it’s possible to create an app package using Docker build on your local computer, it’s not the ideal approach. Instead, you’ll want to use the Cloudron Build Service (CBS). The primary reason for choosing CBS is the bandwidth available in the data center where your Cloudron instance is hosted. Building apps involves downloading and uploading large amounts of data, often several gigabytes. This can be especially challenging in Germany, where slow household internet infrastructure makes such tasks cumbersome.
There’s no real need to use a self-hosted Docker registry, but since we can, we go ahead and install the Cloudron Container Registry (CCR) from the Cloudron app store.
You’ll also need a Cloudron instance where you have admin privileges. This allows you to configure the Cloudron instance to use a private registry, install apps, and connect your Cloudron CLI to the instance.
One of the final steps is to install the Cloudron CLI on your personal computer.
Documentation, configuration, first challenge
Follow these links to read about the installation and configuration. Once everything is set up and working, try out one of the ‘Hello World’ examples.
Cloudron CLI - Documentation
Cloudron Build Service - Documentation
Cloudron Container Registry - Documentation
First challenge - hello world app
The moment the ‘Hello World’ app works, it’s time for fireworks! 🎆
Which app to start with?
If you’ve never looked into the complexity of apps, start with a simple one. By “simple”, I mean ideally a Go binary.
Bird’s-eye view of Cloudron app architecture
The typical Cloudron app runs as a Docker container on the Cloudron platform. The app code, which you want to update without affecting user changes, is stored on a read-only partition (/app/code/
). User data and allowed changes are stored on a read-write partition (/app/data/
).
There are many open-source projects out there that scatter user data across the file system. Dealing with such tools can be challenging, as it requires a deep understanding of the software. A symbolic link can sometimes offer a solution, but managing more than five of them starts to feel completely unmanageable.
Many open-source projects require databases or in-memory data structure servers like Redis. In their Docker Compose files, they typically bundle these services together. Cloudron’s approach is to offer these services as so-called add-ons, which you can activate in your Cloudron manifest. The benefit for users: after installing 10 apps that require a PostgreSQL database, you won’t end up with 10 separate PostgreSQL installations on your server.
Why Go (in my use case)?
I came across the FindMyDevice server and decided to try it out as a Docker Compose installation. Upon inspecting the architecture, I found the following components:
- A Go binary
- A SQLite database
- A configuration file
- An Nginx example file
- A complete web UI in a folder
So, it felt feasible as my first personal adventure in app packaging.
Where should it be stored? What questions should you ask?
The Go binary is code and requires no user input, so it goes to /app/code/
The app has no other option for storing data than SQLite, meaning we can’t use a database add-on from the Cloudron platform. Since the app stores user data in the SQLite database, it must be placed in /app/data/
.
Some settings in the configuration file depend on the user’s decisions, which is why it has to be stored in /app/data/
.
Since the Cloudron app platform handles the full reverse proxy and Let’s Encrypt flow, we don’t need the Nginx example file. /dev/null
is your friend.
I decided not to dive into the complexities of web UIs, multilingual support, and all the potential feature requests (especially once users realize what’s possible), so I placed the entire folder in /app/code/.
How to start?
I decided to work with a real-world (and live) app package. In my case, I chose the Vault app. Long story short, it helped, but introduced a lot of unnecessary elements that I didn’t fully understand, so I later handed them over to my friend /dev/null
.
To have the same in mind, check out the app package for the FindMyDevice Server
The file structure of an app package
The essentials
The manifest follows a schema, and most of its structure is self-explanatory. For me, working with the id field was completely new. In the end, it’s simple: normally, you include the app’s domain, but in reverse order.
If you want to package an app that won’t be part of the Cloudron app store, not every property in the manifest is required. However, httpPort
is mandatory because it’s used by the reverse proxy functionality and for the health check.
The Dockerfile is straightforward: start with the base image, define and create the working directory, and use wget to download the package from GitLab. Unzip it into the working directory, remove unnecessary files, copy the relevant files into /app/pkg
, and set up the start command.
Nearly every app requires some configuration. In my case, I found the project’s config.example.yml file.
UnixSocketPath
, UnixSocketChmod
, PortSecure
, and PortInsecure
don’t make sense in a Cloudron context. The rest of the configuration should be left for the user to customize with their own values.
During my hours of packaging the app, I realized that the server wasn’t capable of using the httpPort defined in the manifest. Instead, it defaulted to a random port, which wasn’t ideal for the intended goal. In the end, I had to add the PortInsecure
variable and set it to match the port specified in the manifest.
Since we can, we often tweak defaults. However, to avoid frustrating users, it’s important to address potential support issues—such as the fact that only 8080 works as the PortInsecure
variable in my package.
start.sh
To be fair, the hardest part was feeling like I needed to be a developer to better understand the underlying magic.
set -eu
ensures the script crashes when something is undefined instead of running with unspecified behavior. Since we chose localstorage
as an add-on in the manifest, we need to create the /app/data
folder to store user or app-specific data.
The if-then-fi
loop checks whether config.yml
exists. If it doesn’t, the file is copied to the specified location.
yq eval -i ".PortInsecure=8080" /app/data/config.yml
is part of the magic I hadn’t encountered before. From what I understand, it searches for the PortInsecure
value in the config file and updates it to the correct port. Since every change to the config requires an app restart, and start.sh
runs on each restart, the user can modify and save a different port—but not in a persistent state. (potential support issue solved.)
chown -R cloudron:cloudron /app/data
changes the owner of the files to the correct user.
The exec gosu cloudron:cloudron /app/code/findmydeviceserver serve --db-dir=/app/data --config=/app/data/config.yml --web-dir=/app/code/web
starts the server with some configuration options with the correct user.
Every Cloudron app package is released under an open-source license, which is why we include a License file.
more than nice to have
- DESCRIPTION.md
- README.md
- CHANGELOG
- logo.png
- screenshots
They’re nice to have, but they become necessary once the app is submitted to the Cloudron app store.
Sanitation
- .dockerignore
- .gitignore
Normal in software development.
Challenge accepted and completed!
The moment I took this photo: Unbelievable. The initial package works.
App store
There’s a lot of work to be done after the initial packaging to bring the app to the app store, but that’s a story for another day.
Cloudron
https://www.cloudron.io/get.html
https://forum.cloudron.io/category/4/support