Google Cloud · Docker · a small adventure

From an empty project to a working cloud

How we built the foundation for a future application: a VM, a network, persistent storage, HTTPS through Cloudflare, backups and an alert that actually sent an email.

1 VMUbuntu · Docker Compose
HTTPSCaddy · Cloudflare
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. Initially, we added two firewall rules. One allowed public HTTP and HTTPS so the site could go live; later we limited it to Cloudflare. 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

First an 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.

At that point we had 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.

07 / Proxying one subdomain

Cloudflare in front, direct IP access closed

Once HTTPS worked, we asked what would happen if someone flooded the small VM with requests. We wanted Cloudflare in front of api.tonymitsev.com while keeping the nameservers for tonymitsev.com at the existing provider. There was no reason to move the main site or email.

Cloudflare's usual partial CNAME setup for that arrangement requires a Business or Enterprise plan. We found a lower-cost route for one subdomain: a separate inexpensive domain on Cloudflare Free with Cloudflare for SaaS. The second domain has registration and renewal costs; Cloudflare for SaaS requires a payment method and is subject to the plan's hostname quota.

  1. Buy a second low-cost domain from a registrar of your choice and activate that domain on Cloudflare Free. In its DNS zone, create a proxied A record to your server's IP and set it as the fallback origin.
  2. Add the desired subdomain of your primary domain as a custom hostname. At the existing DNS provider, add the TXT records Cloudflare supplies for hostname ownership and the HTTPS certificate. Wait until both statuses are Active.
  3. Replace only that subdomain's A record with a CNAME to the proxied hostname on the second domain. The primary domain's nameservers stay where they are. Check that HTTPS now responds through Cloudflare.
  4. In GCP, allow traffic to the VM on ports 80 and 443 only from Cloudflare's published IP ranges. Once the proxied route works, disable the old allow-from-anywhere rule. SSH through IAP remains separate.

In our case, /health still returned 200 healthy with a cf-ray header. Fresh direct connections to the VM's public IP on ports 80 and 443 timed out from an external network. Cloudflare renews the visitor-facing certificate, while Caddy renews the separate origin certificate; we monitor both. This closes the direct HTTP/HTTPS bypass, but it is not a hard spending cap: requests Cloudflare forwards still reach the VM. The commands and limits are in the technical guide.

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...