DEV Community

Mustafa ERBAY
Mustafa ERBAY

Posted on • Originally published at mustafaerbay.com.tr

Self-Hosted Application Updates: Balancing Automation and Stability

On the backend server of one of my side projects, APIs stopped responding after a scheduled midnight update; the cause was a simple dependency conflict, but it reminded me once again of the blind spots of automation. Updating self-hosted applications requires a delicate balance between the conveniences automation brings and its potential risks, especially when striving for operational stability. In this post, I will discuss the complexities of automating update processes for self-hosted applications, the trade-offs we face, and the practical lessons I've learned.

Self-hosted environments, lacking the luxury of "managed updates" offered by cloud services, require us to manage the entire update cycle ourselves. This offers both greater flexibility and leaves us more vulnerable to potential problems. In my experience, building a successful update strategy demands not just technical knowledge, but also risk management and a flexible problem-solving approach.

Why Are Self-Hosted Application Updates Complex?

Updates for self-hosted applications are inherently complex because they typically involve multiple layers, dependencies, and infrastructure components. When updating the core modules of a monolithic ERP system, we must consider not only the application's own codebase but also database schemas, system dependencies (e.g., Python version or a specific C library), and even operating system patches. Potential interactions between these layers can pave the way for unexpected errors.

Another reason for complexity is that we often work with limited resources. While large enterprises have dedicated DevOps teams and extensive test environments, as a developer running applications on my own servers, I might not have this luxury. This necessitates more careful planning for each update step and manual analysis of potential impacts. Even a simple operating system package update can break an older version of a library used by the application and crash the system.

⚠️ The Danger of Hidden Dependencies

When performing a PostgreSQL update, you might encounter situations where a specific version of the psycopg2 library used by your application is incompatible with newer PostgreSQL client libraries. Such hidden dependencies often lead to errors that emerge after an update and are difficult to diagnose.

Furthermore, there is often pressure to minimize "downtime" during update processes. Especially in a live production environment, every second can have a cost. This requires us to consider more sophisticated methods like blue/green deployments when choosing update strategies, but these methods also bring their own complexities. For example, updating a night shipment reporting module in a production ERP, deploying a wrong version could disrupt the entire logistics flow, and such situations carry significant risk without comprehensive testing and rollback plans.

Full Automation or Semi-Automation?

While full automation in self-hosted application updates might seem appealing, in my experience, semi-automation often proves to be a more balanced approach. Full automation is a scenario where all update steps, tests, and deployments occur automatically without human intervention. This is ideal for large-scale systems with robust CI/CD pipelines, comprehensive integration and unit tests, and well-defined rollback mechanisms. However, for small or medium-sized self-hosted projects, the setup and maintenance costs of full automation can outweigh its benefits.

Semi-automation, on the other hand, is an approach where critical steps are automated but require human approval or manual verification at specific checkpoints. For example, a new version of an application might be automatically built and deployed to a test environment, but a human operator's approval might be awaited before moving to production. This approach leverages the speed and repeatability advantages of automation while retaining the human eye's ability to catch critical errors. I typically prefer this semi-automation model for updates to my own sites; builds and basic tests are automated, but going live and smoke tests are under my manual control.

ℹ️ Automation's Blind Spots

Automation only covers the scenarios we've coded. Unexpected situations like disk full errors, network outages, or operating system kernel errors are blind spots that automation cannot foresee and require manual intervention. Therefore, even full automation should not be "unsupervised."

Consider a new billing module update in a production ERP. Automated tests can verify basic functionality. However, edge cases like different payment methods or specific tax rules can be secured with additional tests performed with live data after manual approval. This is a critical step to prevent a potential error from affecting financial processes. Automation is great for routine and repetitive tasks, but human intuition and experience remain indispensable for unique or high-risk situations.

What Are the Update Strategies and Choices?

Multiple update strategies are available for self-hosted applications, each with its unique advantages and disadvantages. Choosing the right strategy depends on the application's criticality, existing infrastructure, and acceptable downtime. The main strategies I frequently use and encounter in my projects are:

  1. In-Place Upgrade: The old version is stopped on the existing application server, the new version is installed, and the application is restarted. This is the simplest method and is generally suitable for small applications running on a single server or for development environments. Its biggest drawback is that the application is unavailable during the update (downtime), and the rollback process can often be manual and error-prone.

    # Example in-place upgrade steps (general scenario)
    sudo systemctl stop myapp.service
    cd /opt/myapp/
    git pull origin main # or download the new package
    ./install.sh         # install dependencies, migrate
    sudo systemctl start myapp.service
    
  2. Rolling Updates: Used in clusters consisting of multiple servers. One or more instances of the application are updated simultaneously, while others continue to run. If the update is successful, the next group is updated. This minimizes downtime, but since it requires both old and new versions to run concurrently during updates, ensuring backward compatibility is crucial.

  3. Blue/Green Deployment: Two entirely separate (but functionally identical) production environments are maintained: "blue" (live) and "green" (staging). The new version is deployed to the green environment, tests are performed, and if all goes well, traffic is routed to the green environment. The blue environment is kept as a backup or shut down. This method reduces downtime to almost zero, and rollbacks are very fast (just rerouting traffic back to blue). However, it requires double the infrastructure cost and special planning for situations like database schema changes.

    Diagram

  4. Canary Deployment: Similar to rolling updates, but the new version is released to only a very small percentage of users (e.g., 1-5%). Performance and error metrics are closely monitored on this "canary" group. If no issues are detected, it is gradually rolled out to more users. This is a risk-minimizing approach but requires more sophisticated monitoring and traffic routing mechanisms.

When updating a critical module in a production ERP, I usually opted for the Blue/Green approach. Especially for a change affecting financial transactions, the ability to quickly restore the old version is invaluable in preventing a potential disaster. However, since this approach is costly, I use in-place or simple rolling updates for my less critical side projects.

Dependency Management and Rollback Mechanisms

One of the most critical, yet often overlooked, aspects of update processes is dependency management and robust rollback mechanisms. An application depends on multiple libraries, services, or operating system packages. Ensuring these dependencies are present in the correct versions and are compatible with each other forms the foundation of a stable update. For example, in a Python-based FastAPI application, precisely locking the requirements.txt file with pip freeze > requirements.txt ensures that development environment dependencies remain the same in production. However, operating system-level dependencies (e.g., the libpq-dev package) often require manual attention.

Incorrect dependencies or incompatible versions can cause the application to fail to start or produce unexpected errors. Last month, when updating the backend of my Android spam application, I added a new library. Everything was fine in the development environment, but on the production server, I received a missing shared library error in the ldd output. The reason was that the new library required a C library not present on the system. Such situations highlight the importance of a comprehensive test environment and even the use of containerization (like Docker). Containers greatly reduce such problems by packaging dependencies with the application.

🔥 Database Schema Rollback Challenges

Database schema changes are one of the most challenging topics regarding rollback mechanisms. An ALTER TABLE ADD COLUMN operation is usually easy to roll back, but a DROP COLUMN or ALTER TYPE operation is much more complex to roll back as it can lead to data loss. Therefore, database migrations should always be designed carefully to be forward and backward compatible.

Rollback mechanisms, on the other hand, ensure that the system can be quickly restored to its previous stable state if an update fails. In its simplest form, this might involve redeploying the previous version of the application. However, when database schema changes are involved, the situation becomes complicated. If the new version ran a database migration, and this migration is not backward compatible, rolling back to the old version will be more difficult. My advice is to always design migrations to be "backward compatible" and to always take backups before critical database changes. For example, in a production ERP, I made it a habit to always take a physical pg_basebackup before a major database migration. This increases my chances of recovery in a disaster scenario.

The Importance of Monitoring and Alerting Mechanisms

When balancing automation and stability in self-hosted application updates, monitoring and alerting mechanisms play an indispensable role. Understanding whether the system is functioning as expected after an update doesn't end with just checking if the application has started. It's crucial to continuously monitor how performance is affected under real user traffic, whether error rates are increasing, and if system resources (CPU, memory, disk I/O) are exhibiting abnormal behavior. This allows us to definitively understand if an update was successful.

I typically use a combination of Prometheus and Grafana on my own servers. I monitor my application's API latency values, HTTP error codes (4xx, 5xx), database connection counts, and query durations. When I see an abnormal rise or fall in these metrics after an update, it's an immediate sign of a problem. For example, in a production ERP, after deploying a new reporting module, I suddenly saw WaitCount metrics in the PostgreSQL connection pool spike. This indicated that the new module was mismanaging database connections and allowed me to quickly roll back.

💡 Don't Neglect Health Checks

Add HTTP or TCP health checks that verify not just if your application is "running," but also if it's "healthy." These checks should include critical dependencies like database connectivity and external service access. Load balancers and container orchestrators can use these checks to remove unhealthy services from traffic.

Alerting mechanisms, in turn, automatically notify us when monitored metrics exceed a certain threshold or an unexpected situation occurs. This allows us to intervene immediately in problems and significantly reduce potential downtime. Just as I automatically block SSH brute-force attempts with tools like fail2ban, I also ensure that I receive email or Telegram notifications if Nginx's 5xx error rate exceeds a certain percentage. This is vital for catching "application is up but users are receiving errors" scenarios. Without monitoring and alerts, it could take much longer to realize a problem after an update, posing serious risks to business continuity.

Security Updates and Their Importance

For self-hosted applications, security updates are not limited to the application's own codebase; they also encompass all used infrastructure components, the operating system, and dependencies. In a world where cyber threats are constantly evolving, regularly patching vulnerabilities (CVEs) is vital to protect the integrity of our systems and the privacy of user data. I pay special attention to this when managing my Linux servers.

An operating system kernel vulnerability can compromise the entire server, while a library vulnerability (e.g., a CVE found in a popular library like log4j) can directly target the application. Tracking and quickly patching such security vulnerabilities are among the most effective ways to reduce our attack surface. For example, I've been known to blacklist kernel modules to prevent unnecessary or risky modules from loading. I also use system auditing tools like auditd to monitor important file changes or suspicious activity.

# Example of blacklisting a kernel module (independent of example CVE-2026-31431)
echo "blacklist algif_aead" | sudo tee /etc/modprobe.d/blacklist-algif_aead.conf
sudo update-initramfs -u
Enter fullscreen mode Exit fullscreen mode

The difficulty of security updates often lies in the balance between urgency and potential side effects. When a critical security vulnerability is identified, the patch is desired to be applied as soon as possible. However, this patch might break the application's functionality or cause an unexpected incompatibility. Therefore, it's important to test security updates carefully, just like other updates, and have a rollback plan. For instance, when a security patch requiring a major dependency update arrives, I prefer to test it first in a staging environment, then move it to production.

ℹ️ Risks of Automatic Updates

While fully automating operating system package updates might seem appealing, caution should be exercised, especially in production environments. Automatic updates can break a critical dependency or cause an unexpected system restart. Generally, automatic downloading but manual or scheduled installation for security updates is a safer approach.

Furthermore, while I actively block malicious attempts with tools like fail2ban, this is only one layer of defense. Fundamental security comes from keeping the software itself up-to-date. In my AI integrations for my own site, I ensure that all libraries and AI model APIs I use are current. Tracking a CVE can sometimes require hours of research and testing, but it's an indispensable investment for the overall security of the system.

Lessons from My Own Experience: The Update Process for My Side Project

In my nearly twenty years of experience, I've encountered countless scenarios in self-hosted application updates; some painful, some smooth. For the backend of my financial calculators, a side project, I continuously perform small and medium-sized updates. One of the most important lessons I've learned in this process is to emphasize the rule: "test, test, and test again." Once, after updating a dependency, everything was fine in the test environment, but upon going live, a specific reporting function started throwing a TypeError. The reason was that the new dependency no longer supported an old API call, and this error only appeared with a specific dataset. This situation showed me once again how comprehensive our test scenarios need to be.

Another important lesson is that rollback mechanisms must work not just in theory, but also in practice. Once, after an update involving a database schema change, the new version of the application contained a critical bug. I wanted to quickly roll back to the old version, but I couldn't revert the database schema because the change I made was not backward compatible. That day, I realized that just as important as the deploy command for an update, the rollback command is equally important and should be regularly tested. Since then, I always take a logical backup before critical database changes and visualize the rollback scenario in my mind.

⚠️ Risks of Manual Interventions

Resorting to manual interventions is inevitable when automation falls short. However, these interventions are often error-prone. I recall an instance where, after running apt upgrade on a server, I manually restarted the system without waiting for it to automatically restart, and an open terminal session during this time disrupted processes. Remember that every manual step carries a potential risk.

Finally, transparency and communication are critical in update processes. When developing an ERP for a large manufacturing company, I made it a habit to inform all relevant stakeholders (operators, finance department, logistics) before a module update. This prevents panic in the event of a potential outage and allows for a calmer approach to problem-solving. Even for my own side projects, before performing an update, I ask myself: "What could this update affect? In which scenarios could problems arise? How do I roll back?" This simple checklist has saved me from many potential headaches.

Conclusion

Finding the right balance between automation and stability in self-hosted application updates is an ongoing process of learning and adaptation. While the speed and repeatability advantages of full automation are undeniable, the importance of human intervention and careful planning should not be underestimated. Challenges such as complex dependencies, database schema changes, and security vulnerabilities make each update process a unique struggle.

For me, the key is to use automation for routine and repetitive tasks, but to allow human oversight and experience to come into play during critical steps. An update strategy supported by robust monitoring and alerting mechanisms, thoroughly tested, and with guaranteed rollback capabilities, ensures operational stability while keeping our systems current and secure. Remember, the best update is one that completes smoothly and allows you to sleep soundly afterward.

Top comments (2)

Collapse
 
codewithsehar profile image
Sehar Munawar

Great post@mustafaerbay! As a beginner just starting my coding journey reading about real-world challenges like database rollbacks and automation risks is super eye-opening. Thanks for sharing your experience!

Collapse
 
merbayerp profile image
Mustafa ERBAY

Thank you, Sehar! 😊 I’m happy it was helpful. Production stories can sound intimidating at first, but they’re really just lessons collected over time. Keep focusing on the fundamentals—variables, data types, functions, and problem-solving. A strong foundation will make topics like CI/CD, rollbacks, and deployment strategies much easier when you reach them. Good luck with your #100DaysOfCode journey!