Bagiyo started with a small setup, but more services were added as the application grew.

It now has an ASP.NET Core API, PostgreSQL, several background workers, a self-hosted ntfy server, Caddy, Docker Compose, Cloudflare, GitHub Actions, and remote database backups.

The old VPS was still working, but it had limited resources and the setup was becoming harder to maintain. I decided to move the Bagiyo backend to a larger VPS and clean up the deployment at the same time.

Preparing the new VPS

I kept the old VPS running while setting up the new one.

Old VPS
  └── Still serving Bagiyo

New VPS
  └── Being configured and tested

This allowed me to install Docker, prepare the directories, configure SSH, start the containers, and test the services without immediately affecting the live application.

One of the first problems I encountered was caused by renaming the default server user. I updated the username but missed an SSH rule that still allowed only the old user.

The SSH configuration passed validation, but the new user could not connect.

Since then, I keep an existing SSH session open while testing changes from another terminal. A valid SSH configuration does not mean the access rules are correct.

Cleaning up the deployment setup

Some parts of the old deployment existed only on the VPS. A few files had been created manually, and the full server setup was not clearly represented in the repository.

On the new server, Docker Compose defines the main services:

Docker Compose
  ├── Caddy
  ├── ASP.NET Core API
  ├── BENECO worker
  ├── PAGASA worker
  ├── Notification worker
  ├── ntfy
  └── PostgreSQL

The Compose files now contain the service relationships, networks, health checks, restart policies, memory limits, persistent storage, and environment configuration.

Secrets are still stored separately, but the deployment structure is now kept in the repository instead of being configured only on the server.

This should make it easier to rebuild the setup on another VPS when needed.

Migrating PostgreSQL

The database needed the most care because it contains the actual application data.

The process was:

Existing PostgreSQL

Create a database dump

Transfer the dump

Restore it into the new PostgreSQL container

Verify the data

I first ran into a version mismatch between the PostgreSQL server and the client tools used for the dump. Using a compatible PostgreSQL version resolved the issue.

After restoring the database, I checked the schemas, tables, and records. I also tested the application against the new connection string to confirm that it could read and write data.

The restore command completing successfully was not enough. I still needed to verify that the correct data was restored into the correct database.

Moving ntfy

Bagiyo uses a self-hosted ntfy server for notifications.

Inside Docker, the API and workers connect to ntfy using its Compose service name:

http://ntfy:80

Subscribers connect through the public domain:

https://ntfy.bagiyo.org

During the migration, the new application was still publishing to the ntfy server on the old VPS.

It appeared to be working because the old server was still online. I only found the issue after disconnecting the old ntfy service.

The new containers were running, but part of the application was still depending on the old server.

I changed the publisher configuration to use the internal Docker address and recreated the required ntfy permissions and tokens on the new VPS.

Separating the background workers

Bagiyo has separate background jobs for collecting BENECO announcements, processing PAGASA bulletins, and sending notifications.

These jobs previously ran together with the API. On the new VPS, I moved them into separate containers.

A problem in one worker can now be handled without restarting the API or the other workers. Each service can also be deployed and restarted separately.

The downside is higher memory usage. Each .NET container starts its own runtime, configuration, logging, dependency injection setup, and database connections.

The new VPS has enough memory for this, so I kept the services separated for better isolation.

Using Aspire for local orchestration

The new structure also made the local setup closer to what runs on the VPS.

I use .NET Aspire to start the same main components during development:

.NET Aspire
  ├── ASP.NET Core API
  ├── BENECO worker
  ├── PAGASA worker
  ├── Notification worker
  ├── PostgreSQL
  └── ntfy

Aspire is used only for local orchestration. Docker Compose is still used for the VPS deployment.

They use different tools, but the services and boundaries are mostly the same.

Local development
.NET Aspire

Same services and dependencies

Production
Docker Compose on the VPS

This lets me run the API, workers, database, and ntfy together on my machine before deploying them.

The Aspire dashboard also gives me one place to check logs, traces, health, and service dependencies while developing.

It is not an exact copy of production, but it is close enough to catch configuration and integration issues earlier.

Updating the deployment workflow

I also replaced the older deployment process with a Docker-based GitHub Actions workflow.

The workflow now builds and tests the application, creates the required container images, pushes them to the registry, uploads the deployment files, and updates only the affected services on the VPS.

Each image is tagged using the Git commit instead of relying only on latest. This makes it easier to check which version is running and to roll back to an earlier image.

I still verify the result directly on the VPS:

docker compose ps
docker compose images
docker inspect <container-name>

I added this check after one workflow completed successfully but did not replace the container I expected it to replace.

The GitHub Actions job had passed, but the old container was still running.

Switching the domains

After the services were working on the new VPS, I updated the Cloudflare DNS records for the API and ntfy domains.

I kept the old server online during the cutover and checked the logs on both servers. This helped confirm where the requests were going and whether anything was still using the old VPS.

I also checked the public endpoints, database connection, notifications, TLS certificates, and health checks before shutting down the old services.

Adding remote backups

Moving PostgreSQL to my own VPS also meant managing the backups myself.

I added a scheduled process that creates a database dump, compresses it, uploads it to Cloudflare R2, and removes older files after the retention limit is reached.

The backup is stored outside the VPS. A local backup can help with an accidental deletion, but it will not help if the whole server or disk is lost.

I tested the backup and restore manually before scheduling the job. Seeing a file in R2 is not enough. It also needs to contain valid data and be restorable.

Problems I encountered

Most of the problems were small, but there were many of them:

  • SSH rules still referenced the old username.
  • Some directories had incorrect ownership.
  • PostgreSQL tool versions did not match.
  • The application still used the old ntfy server.
  • ntfy permissions and tokens had to be recreated.
  • A deployment completed without replacing the expected container.
  • Persistent directories had to be created before some containers could start.
  • Separating the workers increased total memory usage.
  • The old VPS continued hiding configuration mistakes because it was still online.

The migration itself was not mainly about copying files. The harder part was finding every dependency and confirming that the new setup no longer needed anything from the old server.

The current setup

The Bagiyo backend now runs like this:

Cloudflare
  ├── Angular frontend
  ├── API domain
  └── ntfy domain

       New VPS
          ├── Caddy
          ├── ASP.NET Core API
          ├── Background workers
          ├── ntfy
          └── PostgreSQL

          Cloudflare R2 backups

For local development, Aspire runs the same main services. In production, Docker Compose runs them on the VPS. GitHub Actions builds and deploys versioned container images, while R2 stores the database backups outside the server.

Everything still runs on a single VPS, so it is not highly available. If the VPS goes down completely, Bagiyo will remain offline until I restore the services and database somewhere else.

For the current size and budget of the project, this setup is enough. More importantly, it is now easier for me to understand, maintain, and rebuild.