LALOVAR

writing things on the internet

Back to journal

Build Your Own Home Server with a Raspberry Pi, Docker Compose, and Cloudflare Tunnel

Raspberry pi sitting on my tv's cabinet over a book
Raspberry pi sitting on my tv's cabinet over a book

How I turned a Raspberry Pi 4B into a small home server to host my blog, deploy personal apps, monitor my network, and expose selected services securely through Cloudflare Tunnel.

Posted/14 min read/Updated May 20, 2026/ homelab/ raspberry-pi/ docker/ cloudflare/ self-hosting/ devops

Build Your Own Home Server with a Raspberry Pi, Docker Compose, and Cloudflare Tunnel

At some point, every developer ends up with the same question:

Why am I paying for infrastructure when I already have hardware sitting at home?

That question was the beginning of my small home server setup.

I had a Raspberry Pi 4B with 8GB of RAM, a solid internet connection, a few personal projects, and the usual curiosity that makes you want to understand how things actually work under the hood. Instead of treating the Raspberry Pi as a toy or a weekend experiment, I wanted to turn it into something useful: a real home server capable of running my blog, hosting public apps, exposing only the services I wanted, and keeping private tools available only inside my network.

The result is a small but practical setup built around a Raspberry Pi, Docker Compose, Cloudflare Tunnel, public DNS, GitHub Actions, Prometheus, and Grafana.

This is not meant to be a step-by-step tutorial. There are already plenty of those. This is more of a walkthrough of how I approached the architecture, what decisions mattered, what problems appeared along the way, and why I think a cheap computer or Raspberry Pi can be a great place to learn infrastructure.

The idea: use what you already have

The core idea was simple: build a home server using hardware I already owned.

In my case, that hardware was a Raspberry Pi 4B with 8GB of RAM running Raspberry Pi OS. I did not install a desktop environment or anything fancy. The Pi runs headless, and I manage it entirely through the terminal using SSH.

That constraint was intentional. I wanted the machine to feel like a real server, not like a small desktop computer connected to a monitor. Everything happens through the command line: updates, deployments, service management, Docker Compose files, logs, monitoring, and troubleshooting.

The Raspberry Pi is not a replacement for a serious production cluster, and it is not supposed to be. But for personal projects, small public apps, private tools, internal dashboards, and learning how deployment actually works, it is more than enough.

The interesting part is not just the hardware. It is the environment around it.

At home, I have a load balancer and firewall setup with a 1Gb fiber connection as the primary ISP and Starlink as failover. That made the idea even more attractive. If the connection is already there, and the hardware is already there, why not use it?

Instead of deploying every small project to a VPS or a managed platform, I wanted to create my own small infrastructure layer at home.

What runs on the server

The Raspberry Pi runs several services, but I mentally divide them into two categories: public services and private services.

The public side includes my blog, lalovar.com, and a few other public apps from personal projects. These are services I want to expose to the internet through proper domains, HTTPS, and a controlled entry point.

The private side includes tools that only make sense inside my local network. For example, I run Grafana to monitor local metrics: network consumption, traffic separated by VLANs, Raspberry Pi resource usage, inbound and outbound traffic, and other network and performance metrics. Prometheus collects the data, and Grafana gives me a visual layer to understand what is happening.

That separation is important.

Not every service needs to be public. In fact, most services should not be public. A home server becomes much easier to reason about when you decide upfront which apps should be reachable from the internet and which ones should stay inside your network.

For example, a blog or public landing page can be exposed through a domain. A monitoring dashboard, media service, or internal tool can remain local. This reduces risk and keeps the architecture cleaner.

Docker Compose as the deployment unit

For orchestration, I use Docker Compose.

There are many ways to structure a home server. Some people prefer a single massive Compose file for everything. Others prefer Kubernetes, Nomad, systemd services, or plain Docker commands.

I like having one Docker Compose setup per application.

That keeps each project isolated and easy to reason about. If I need to restart the blog, I restart the blog stack. If I need to update another app, I update that app’s Compose file. If one project has its own database, it can live next to that project. If some apps share a database, that can also be handled separately.

There is no single correct answer here. The right structure depends on the type of apps you are running, how much isolation you want, and how much operational complexity you are willing to accept.

For my setup, one Compose file per application gives me a good balance. It is simple enough to manage manually, but organized enough that the server does not turn into a pile of random containers.

This is also one of the reasons I like Docker Compose for a home server. It gives you repeatability without introducing too much abstraction. You can define the service, environment variables, volumes, ports, restart policy, and dependencies in one place. Then the server can recreate that app consistently.

No open ports, no public IP dependency

One of the most important decisions in this setup was how to expose services to the internet.

The traditional way would be to open ports on the router, forward traffic to the Raspberry Pi, configure a reverse proxy like Nginx or Traefik, manage certificates, and point DNS records to the home IP address.

That works, but it has tradeoffs.

You need to expose your network directly. You may need a static public IP or dynamic DNS. You need to be careful with firewall rules. You need to manage TLS certificates locally. You need to trust that every exposed port is intentional and properly secured.

I wanted something simpler and safer.

So I used Cloudflare Tunnel.

With Cloudflare Tunnel, the Raspberry Pi runs a small agent called cloudflared. Instead of opening ports on the router, the agent creates an outbound connection from the Raspberry Pi to Cloudflare. That means the Pi reaches out to Cloudflare, not the other way around.

The flow looks like this:

User → DNS CNAME → Cloudflare Edge → Cloudflare Tunnel → cloudflared on Raspberry Pi → local service

That design has a huge advantage: I do not need to expose ports on my router. I do not need a static public IP. I do not even need my home network to be directly reachable from the outside.

For each public app, I can create a public hostname in Cloudflare Tunnel. That hostname maps a domain or subdomain to a local service running on the Raspberry Pi. For example, a public hostname can point a domain to a service running locally on port 3000, 8080, or whatever the app uses internally.

Cloudflare handles the public edge. The Raspberry Pi only needs to keep the tunnel alive.

This also means HTTPS is handled by Cloudflare. I do not run a local reverse proxy like Nginx, Caddy, or Traefik for this setup. Cloudflare manages the public-facing certificate, DNS, and routing layer.

That keeps the Raspberry Pi focused on running applications, not being the public perimeter of the network.

DNS as the front door

DNS is what makes the whole setup feel clean.

Some of my domains were registered directly through Cloudflare, and others originally lived in Namecheap. I moved the DNS management into Cloudflare so the public hostnames, tunnel records, SSL, and security controls could live in one place.

For a public app, the DNS side is straightforward. A CNAME record points a subdomain to the Cloudflare Tunnel target. When a user visits the domain, the request reaches Cloudflare first. Cloudflare then routes the request through the tunnel into the Raspberry Pi.

From the outside, it feels like a normal hosted application.

From the inside, it is just a service running on a small computer at home.

That is one of the most satisfying parts of this kind of setup. You can take something running locally and give it a proper public domain, HTTPS, and a clean URL without turning your router into a public entry point.

Public services vs private services

A home server should not mean “everything is exposed.”

That is probably the most important mindset shift.

I expose only the services that need to be public. My blog and some public apps are available through public DNS and Cloudflare Tunnel. Other services, like Grafana and local monitoring, stay private.

Grafana is useful to me because it gives me visibility into the network and the Raspberry Pi itself. I can see traffic patterns, local network behavior, VLAN usage, inbound and outbound traffic, and performance metrics from the Pi. Prometheus collects the data, and Grafana turns it into dashboards that are easy to understand.

But there is no reason for that dashboard to be public.

Keeping it private reduces the attack surface and keeps the server easier to maintain. It also forces a healthier architecture: public apps go through Cloudflare, while internal tools stay internal.

The more you self-host, the more you realize that access control is not just a technical feature. It is an architectural habit.

CI/CD was the hardest part

Running an app on a Raspberry Pi was not the hardest part.

Deploying it cleanly was.

The biggest technical challenge was setting up CI/CD with GitHub Actions. I wanted a push to the main branch to deploy the latest version to the Raspberry Pi automatically. The idea was simple:

GitHub Push → GitHub Actions → SSH into Raspberry Pi → pull latest code → build → restart service

In practice, there were a lot of small details to get right.

The Raspberry Pi needs to be reachable from the GitHub Actions runner over SSH. The repository needs deployment secrets. The private SSH key needs to be stored safely. The server needs the right permissions. The app needs a reliable restart strategy. The build process needs to run correctly on the Pi. Environment variables need to exist on the server, not inside the repository.

The final flow is simple, but getting there required debugging.

For some apps, the deployment process is basically: SSH into the Raspberry Pi, enter the project directory, pull the latest code, rebuild the app, and restart the process. For Node-based apps, a process manager like PM2 can keep the app alive. For Dockerized apps, Docker Compose can rebuild and restart the service.

The tunnel does not need to change during deployment. Cloudflare continues routing traffic to the local service. The deployment updates what is running behind that route.

This is the kind of thing that sounds obvious after it works. But when you are setting it up for the first time, you learn a lot about SSH, GitHub secrets, server permissions, environment variables, build artifacts, process managers, and logs.

This was the part where tooling and AI assistance were genuinely helpful. Following documentation, reading errors carefully, and debugging step by step made the final setup much more understandable.

Security choices

Security in a home server starts with restraint.

The first rule is that I do not expose ports from the Raspberry Pi to the internet. Public traffic goes through Cloudflare Tunnel. The Pi maintains an outbound connection, and Cloudflare becomes the public edge.

I also use SSH keys instead of password-based access. That is one of the first things I would recommend for any server, whether it runs at home or in the cloud.

The firewall is configured to keep the server limited to what it actually needs. Public and private services are separated. Secrets live in environment files, not committed into repositories. Backups run through cron jobs and are pushed to the cloud.

Cloudflare also adds a useful layer in front of the public apps: SSL, DNS management, WAF features, bot protection, and other security controls. I do not treat that as magic protection, but it is a good public-facing layer that lets the Raspberry Pi avoid being directly exposed.

The general security model is:

Do not expose what does not need to be exposed.
Use tunnels instead of open ports.
Use SSH keys.
Keep secrets outside Git.
Back up the data.
Separate public services from private tools.
Let Cloudflare handle the edge.

That is enough for my current use case, and it keeps the setup manageable.

Why a Raspberry Pi is enough

A Raspberry Pi is not powerful compared to a dedicated server, but it does not need to be.

Most personal projects do not need massive compute. A blog, a few small apps, monitoring dashboards, APIs, internal tools, and experiments can run comfortably on modest hardware if you are reasonable about resource usage.

The bigger limitation is usually not CPU. It is operational discipline.

Can you keep the server updated?
Can you organize your Docker Compose files?
Can you recover from a bad deployment?
Can you restore from a backup?
Can you separate public and private services?
Can you understand where traffic enters and how it reaches your app?

Those are the real lessons.

A small home server teaches you how infrastructure works because you have to touch every layer: DNS, networking, Linux, SSH, Docker, environment variables, monitoring, deployment, SSL, firewalls, and backups.

Managed platforms are great, but they hide many of those layers. A home server exposes them in a way that makes the learning real.

What this setup is good for

This kind of setup is great for personal projects, internal tools, blogs, experiments, dashboards, private apps, and learning.

It is also a good way to avoid paying for infrastructure you do not really need. If you already have hardware, power, and a decent connection, hosting a few small services at home can make a lot of sense.

That does not mean everything belongs on a Raspberry Pi.

If a project needs high availability, strict uptime guarantees, heavy traffic, compliance requirements, or global low-latency access, a proper cloud environment may still be the right answer.

But not every project needs that.

Sometimes a small server at home is enough. Sometimes it is better because it forces you to understand the system from end to end.

What I would recommend

If you want to build something similar, I would start small.

Do not begin by trying to migrate everything at once. Pick one app. Containerize it. Run it locally. Add a Docker Compose file. Expose it privately first. Then put Cloudflare Tunnel in front of it. Add a public hostname. Confirm HTTPS works. Add deployment automation later.

Once that first app works, the pattern becomes reusable.

You can add another app, another Compose file, another public hostname, another private service, another dashboard, another backup job.

The server grows gradually.

That gradual approach is important because a home server can become messy very quickly. Without structure, it turns into a collection of containers, ports, scripts, and forgotten services. With a simple pattern, it becomes a small but understandable platform.

For me, the pattern is:

One small server.
Docker Compose per app.
Cloudflare Tunnel for public access.
Private services kept private.
GitHub Actions for deployment.
Prometheus and Grafana for visibility.
Backups through cron jobs.
SSH keys and firewall rules for basic security.

That is enough to host useful things without making the setup harder than it needs to be.

Final thoughts

Building a home server is not only about saving money.

It is about ownership.

When you run your own apps on your own hardware, you start seeing the full path from code to user. A commit becomes a deployment. A domain becomes a route. A container becomes a service. A dashboard becomes visibility into the network. A tunnel becomes the bridge between your home and the public internet.

My Raspberry Pi started as a small board connected to my network. Now it runs my blog, public personal apps, private services, monitoring dashboards, and deployment workflows.

It is not a data center. It is not a cloud provider. It is not pretending to be either.

It is a small home server that gives me control, teaches me infrastructure, and makes better use of the hardware and connection I already have.

And for a lot of developers, that is more than enough.

Community

Comments

Approved comments appear here.

1 comment

You can read approved comments without an account, but posting requires signing in first.

Sign in to add a comment

Choose a provider

Fernando Vargas

Fernando Vargas

cool

Newer

The newest note is the one you just read.

Older

This is the first post in the archive.