Google Cloud · Docker · a small adventure

From an empty project to a working cloud

How one long evening produced the foundation for a future application: a VM, a network, persistent storage, a domain, HTTPS, backups and an alert that actually sent an email.

1 VMUbuntu · Docker Compose
HTTPSCaddy · custom subdomain
Backupdisk · bucket · restore test

25 October 2026 · A first-person account

We could have started with Terraform and produced a list of cloud resources after a few commands. This time, we went through the web console. I wanted to see where each piece lived, how it connected to the others, and which switches actually mattered. For a small project, doing the first pass by hand turned out to be a good way to learn the shape of the system.

The project is called Cloud VM Docker Starter. It is not yet an application serving users. It is the stage on which one can run: a public HTTPS entry point, a Docker environment, persistent storage for future data, a route for backups and monitoring. Real enough to break; small enough to understand why.

01 / First decisions

The bill comes before the server

We began with a Google Cloud project and a billing account. Before creating any VM, we added a monthly budget with several notification thresholds. The amount was $26.50: an early warning, not a hard cap. Reaching the budget does not automatically stop the services. The cloud will politely keep running if nobody looks at the bill.

Then we opened the pricing calculator. Its 730 hours represent roughly a full month of continuous operation, not a hidden perk. We compared European regions and chose Belgium. Frankfurt was somewhat more expensive, and this first stage gave us no reason to pay the difference. The estimate was only a starting point: a separate data disk, an external address, snapshots, Cloud Storage and traffic can all appear on the real bill.

There was an early puzzle too: why did the calculator show only half a vCPU for e2-small? It is a shared-core machine type. Asking for two full cores selects a different machine and changes the price. We kept e2-small with 2 GB of RAM and the regular provisioning model. It is a modest starting point for a gateway; a real application will tell us whether it is enough.

02 / Defining access

We drew the boundaries first

We created a dedicated VPC and subnet instead of building on the ready-made default network. Then we added two clear firewall rules. One allows public HTTP and HTTPS, because the site must be reachable. The other allows SSH through Google Cloud IAP. That Google-owned address range looked arbitrary until we understood that the SSH tunnel comes through it.

Both rules target a network tag attached to the VM. The rules exist in the VPC, but apply only to instances carrying that tag. That is why we left the VM creation form's extra “Allow HTTP traffic” and “Allow HTTPS traffic” checkboxes alone: our own rule already covered those ports.

We also reserved a static external IP address. The console warned that an unattached reserved address costs more, so we attached it to the VM and gained a stable target for DNS. The network form briefly showed two interfaces: one in default, one in our VPC. We kept only the latter. An empty alias IP field was not a problem to fix; we simply did not need an extra range.

For administration, we enabled OS Login and connected through IAP. We did not open SSH to the entire internet or paste keys into the VM settings. This does not make the machine magically secure, but it keeps the access story legible: Google Cloud permissions decide who may log in, while the network rule controls the traffic path.

03 / The actual VM

One disk for the system, another for data

We created an Ubuntu VM with a 20 GB boot disk and a separate 20 GB data disk. Both use Balanced Persistent Disk. We configured the data disk to remain if the VM is deleted. The distinction matters: a container or even a VM can be replaced, while a future application's database must not vanish with it.

Next we entered through Cloud Shell. This helped clarify a useful boundary: Cloud Shell is Google's separate terminal; only after gcloud compute ssh are we inside our VM. The SSH key asked for a passphrase, adding a very human problem to our infrastructure work: remembering it. A later VM reboot really did disconnect the session. We logged in again and checked that the important pieces were still there.

On the machine, the new disk appeared separately from the system disk. We formatted only the blank data disk, mounted it by UUID, and checked the mount again after a reboot. That verification matters more than a friendly disk name in the console: Docker must write persistent data to the attached disk, not to a directory that happens to exist on the boot volume.

We installed Docker Engine and Compose, placed the repository on the VM and started one container: Caddy. There is no Node or Java app yet, no database with user data and no unnecessary public application ports. We have room to add those when there is an actual app to deploy.

04 / The first public response

One A record, then HTTPS

We added a DNS A record for api.tonymitsev.com, pointing it at the VM's stable address. Once the name resolved correctly, Caddy handled HTTP and HTTPS. A certificate appeared automatically. There was no certificate file to buy, upload or renew by hand. Caddy stores its persistent data on the separate disk, so recreating the container does not start from scratch.

The first homepage answered with 200 and “Cloud VM ready”. Nice for an initial test, but a poor monitoring target: unknown paths also returned 200. How could we simulate a bad request if every path said everything was fine?

We added a dedicated /health endpoint that returns 200 healthy, while unknown paths now return 404. Monitoring gets a stable URL. When a real application arrives, we will keep that public path and connect it to the application's own health check. Today it checks Caddy, not a backend that does not yet exist.

05 / A tested plan B

A backup matters after a restore

We created a private Cloud Storage bucket for future backups. The VM can create objects in it, but cannot read or delete existing ones. That is a useful boundary: the server can send a new backup without gaining full control over the older copies.

That choice produced a helpful error. A trial with gcloud storage cp returned 403 for a read permission. Instead of widening the VM's rights merely to silence the error, we uploaded a probe through the API's object creation operation. Then we made a tiny SQLite database, uploaded it too, and downloaded it in a separate Cloud Shell session using operator access. PRAGMA integrity_check returned ok, and the probe row was readable. That was the point where “we have a backup” became “we can restore this backup”.

We also set up daily disk snapshots. They provide a second recovery path, separate from a logical SQLite copy. A live application will still need scheduled, consistent SQLite backups, uploads, failure alerts and regular restore drills. This was deliberately a small manual test, not a claim that production backup automation is finished.

06 / Wake us if it breaks

The check spoke up — and emailed us

We configured Google Cloud Monitoring to request https://api.tonymitsev.com/health and require both a successful HTTP status and the word healthy. A failure should send email. While we were changing the configuration and recreating the gateway container, an alert email really arrived. Mildly annoying in the moment, but exactly the proof we wanted: the path from a failed check to the inbox works.

We now have a working infrastructure foundation: a domain with HTTPS, restricted admin access, persistent storage, a public gateway, a private backup destination, a tested restore and an alert. We also kept a written record of the setup so that a month from now we do not have to guess which console menu created what.

The next chapter is the application. It will sit behind Caddy, keep its data on the attached disk, provide its own health check and run automatic backups. Until then, the homepage says only what we can honestly claim: the cloud VM is ready.

If you are curious what a small cloud project looks like before it becomes a product, the code and documentation are on GitHub. This is the story so far; we will continue it when a real application moves in.

More projects

My latest news RSS tonymitsev

25.10.2026, Cloud VM Docker Starter: our first cloud foundation

From an empty Google Cloud project, we reached a VM with its own network, Docker, HTTPS, a separate disk, a tested SQLite restore and an email alert. Read the full story...

17.10.2026, RainCheck - a tiny PWA built with AI

With Codex, we built RainCheck, a tiny framework-free weather app using TypeScript, SCSS and Vite. It works offline with saved forecasts; the reusable AI PWA playbook came afterwards, distilled from the working app and its real-world trade-offs. See the project page... and open RainCheck.

22.09.2026, Applied AI Lab: a new project with Codex

With Codex, we created Plumbing Assistant - the pilot project in Applied AI Lab, a specialised AI assistant for plumbing work with a RAG-oriented and provider-agnostic direction. See the project page... and open the project on GitHub.

All news...

I have not made websites for a long time, but some of them have survived the years...

www.elprom-ilep.com www.bluetronic.info

But there is still some new stuff...