These days, what I see most often is people generating an entire app with AI and stopping the moment it runs on localhost. It works on my machine, and then what? This series covers what happens under the hood when an application has to go to a real server, and how I plan to improve that process step by step.
I’ll start from the most honest place possible: my process is raw today, and I’m going to show you what it looks like.
The context
S.O Manager is a service order management system I’m building with a colleague. He built the Android app in Kotlin, and I built the API with Java and Spring Boot, using PostgreSQL as the database and MinIO for file storage. Everything runs on a VPS that I set up from scratch, with Nginx, a firewall, and a domain.
Today, the system has two access levels: Administrator, who manages the creation and progress of service orders (SOs), and Technician, who sees the SOs assigned to them, fills in the relevant information, and collects the client’s signature and feedback. Everything is automated, removing bottlenecks in order management and in filling out manual spreadsheets.
How I deploy today
When I finish a feature, this is the process:
On my machine
- I bump the version in
pom.xmlto mark which version is going to production, and commit it. - I build the Docker image with Google Jib, tagged with the current version.
- I
docker pushthe image to the registry.
On the VPS, via SSH
- I run
git pullon the repository to updatedocker-compose.prod.yml. - I run
docker pullto download the new image. - I take the application down with
docker container kill. - I edit
.envto point to the new version and bring everything up withdocker compose -f docker-compose.prod.yml --env-file .env.prd up -d.
Compose starts three services together: Postgres, MinIO, and the application. Database migrations run with Flyway when the application starts. And if something breaks, the rollback is to edit .env back to the previous image and bring everything up again.
One note: I don’t build on the VPS. It only pulls a ready-made image, which keeps the server simple and guarantees that what ran on my machine is what goes to production.
Two environments
I just set up a staging subdomain. api.mydomain.com.br is production, with real data, and hml.api.mydomain.com.br is where I’ll validate versions before promoting them.
What hurts
The process works, but it has clear problems:
- Too many steps, all manual. There are seven steps across two machines, and any slip becomes a bug in production.
- Downtime. Since I take the container down before bringing the new one up, the API is offline for a few seconds.
- Manual rollback. Reverting a version requires editing a file on the server.
- Almost no automated tests, and none run before the deploy. Today, the only barrier between a bug and production is me.
- I depend on memory. If I forget a step or the order, nobody notices until something goes wrong.
A concrete example: I once hit a conflict with an existing container when bringing up the new version. I thought docker container kill removed the container, but it only stops it. (This deserves its own post.)
What’s next
In the next posts of the series, I’ll evolve this process with GitHub Actions:
- A first pipeline that runs the build and tests on every push.
- Automatic deploy to the VPS, going through the staging environment first.
- How to handle secrets outside the repository.
- Rollback and reducing downtime.
The idea isn’t to show a perfect process, but to record the evolution of one that started raw. If you’ve been through this, tell me in the comments: what was the first step you automated?
