Self-Hosting an API on Raspberry Pi: Secure Deployments with Tailscale and Cloudflare

Posted on Feb 12, 2025
Last Updated on Jun 12, 2025

Having worked with embedded platforms like STM32, ESP32, and Raspberry Pi Pico, I’ve learned firsthnad how versatile these small devices can be. But if there’s one board that consistently punches above its weight, it’s the Raspberry Pi. Over the years, it’s evolved from a hobbyist toy into a capable platform for edge computing, home labs, and even lightweight production workloads.

If you’re an engineer/developer or haven’t tried using a Raspberry Pi beyond blinking LEDs, you’re missing out on a powerful, low-cost platform. I recommend every engineer to experiment with it. it’s a fantastic learning tool and a legitimate deployment target (clusters).

I’ve set up a home server running entirely on Docker, hosting a variety of self-hosted services including a NAS, a proxy DNS server, a torrent client, and a media streaming service. While building it out, I thought, why not also host the API I was working on? So I added that too. In this post, We’ll see how to build a system where every git push automatically deploys the app to a Raspberry Pi securely, automatically, with no public ports exposed, and serving APIs you can access from anywhere.

Architecture:


Part 1: Running Docker on Raspberry Pi 5

Thanks to ARM64 support in Docker, the Raspberry Pi 5 can run most containerized workloads that don’t demand huge CPU or RAM resources. This opens the door to using the same Docker workflows you’d use on larger servers.

Install Docker and Docker Compose

First, SSH into the Raspberry Pi 5 and install Docker:

curl -sSL https://get.docker.com | sh
sudo usermod -aG docker $USER

Log out and log back in to apply Docker group permissions.

Next, install Docker Compose (since the Pi uses ARM64, we download the ARM binary):

DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
mkdir -p $DOCKER_CONFIG/cli-plugins
curl -SL https://github.com/docker/compose/releases/download/v2.24.0/docker-compose-linux-aarch64 -o $DOCKER_CONFIG/cli-plugins/docker-compose
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose
docker compose version

At this point, both Docker and docker compose are ready to go.


Part 2: Securing Remote Access with Tailscale

Your Raspberry Pi is likely sitting behind a private home or office network. We want to deploy to it remotely without exposing ports or needing a static public IP.

That’s where Tailscale comes in. it creates a secure VPN mesh, letting you SSH or SCP into the Pi over a private, stable IP address.

What’s Tailscale you ask. If you know wireguard then you know how tailscale works.

Here’s a quick intro to tailscale:
Tailscale is a modern VPN solution that simplifies secure networking by building a mesh network using WireGuard.

  1. WireGuard Foundation: Tailscale leverages the open-source WireGuard protocol to establish lightweight, encrypted tunnels between devices, referred to as “nodes”.​

  2. Mesh Networking: Unlike traditional VPNs that use a central hub, Tailscale creates a mesh network where each node can connect directly to others, enhancing efficiency and reducing latency.​

  3. NAT Traversal with DERP: To handle devices behind firewalls or NATs, Tailscale employs NAT traversal techniques and, when direct connections fail, uses its DERP (Designated Encrypted Relay for Packets) servers to relay traffic securely.​

  4. Control Plane Management: A centralized control plane manages authentication, authorization, and key distribution, integrating with identity providers to enforce access controls based on user identities rather than IP addresses.​

  5. Open Source and Flexible Deployment: Tailscale’s node software is open source, allowing users to inspect, modify, and deploy it according to their needs, with a flexible free plan available for various use cases.​

Install Tailscale on the Pi:

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up

Once authenticated, the Pi will be assigned a 100.x.x.x Tailscale IP. You can now securely SSH and SCP from anywhere, whether you’re on a work laptop, a phone hotspot, or a coffee shop Wi-Fi.


Part 3: Docker Compose Setup

In the project repo, define the app’s Docker Compose configuration. Here’s an example for a Python FastAPI app:

docker-compose.yml:

services:
  api:
    build: .
    ports:
      - "8000:8000"
    restart: always

And a simple Dockerfile:

Dockerfile:

FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Test the app manually:

docker compose up -d --build

Then visit http://<tailscale-pi-ip>:8000 from another Tailscale-connected device to confirm it’s working.


Part 4: Automating Deployment with GitHub Actions

Let’s automate deployment so every push to main triggers a secure deploy to the Raspberry Pi, no manual SSH or file copying needed.

Here’s a complete GitHub Actions workflow:
Create .github/workflows/deploy.yml in the repo:

name: Deploy to Raspberry Pi 5 with Docker Compose and Tailscale

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest 

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Tailscale
        uses: tailscale/github-action@v3
        with:
          authkey: ${{ secrets.TAILSCALE_AUTH_KEY }}

      - name: Check Tailscale
        run: tailscale status

      - name: Set up SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          ssh-keyscan -H ${{ secrets.RPI_IP }} >> ~/.ssh/known_hosts

      - name: Copy files to Raspberry Pi
        run: |
          scp -o StrictHostKeyChecking=no -r ./* ${{ secrets.RPI_USER }}@${{ secrets.RPI_IP }}:/home/${{ secrets.RPI_USER }}/myapp/

      - name: Deploy Docker Compose on Raspberry Pi
        run: |
          ssh -o StrictHostKeyChecking=no ${{ secrets.RPI_USER }}@${{ secrets.RPI_IP }} << 'EOF'
            cd /home/${{ secrets.RPI_USER }}/myapp
            docker compose down
            docker compose up -d --build --quiet-pull
          EOF

      - name: Clean up Tailscale session
        run: sudo tailscale down

Required GitHub Secrets:

Secret Name Purpose
TAILSCALE_AUTH_KEY Tailscale auth key for the GitHub Action
SSH_PRIVATE_KEY SSH private key for connecting to the Pi
RPI_IP Tailscale IP of the Raspberry Pi
RPI_USER Username on the Pi (e.g., pi)

What this workflow does:

  1. Establishes a secure VPN session via Tailscale
  2. Copies the project files to the Raspberry Pi via SCP
  3. Remotely rebuilds and restarts Docker Compose on the Pi
  4. Tears down the Tailscale session after deploy

Part 5: Exposing the API Securely with Cloudflare Tunnel

By default, the API is accessible only inside the Tailscale/private network. But what if you want to make it public, without opening firewall ports or setting up NAT?

That’s where Cloudflare Tunnel (formerly Argo Tunnel) comes in:

  • No open inbound ports
  • Automatic SSL/TLS
  • Maps a public domain (e.g., api.mydomain.com) to the private server

Installing Cloudflared

SSH into the Pi:

sudo apt update
sudo apt install cloudflared

Or manually download the ARM64 binary:

wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64
chmod +x cloudflared-linux-arm64
sudo mv cloudflared-linux-arm64 /usr/local/bin/cloudflared

Check installation:

cloudflared --version

Authenticating Cloudflared

Run:

cloudflared login

A browser window will open. Log into the Cloudflare account and select the domain you want to use. This creates a .cloudflared/cert.pem credential.

Create a Tunnel

cloudflared tunnel create my-api-tunnel

Then map the tunnel to a DNS record:

cloudflared tunnel route dns my-api-tunnel api.mydomain.com

Create a configuration file:

# /home/pi/.cloudflared/config.yml
tunnel: my-api-tunnel
credentials-file: /home/pi/.cloudflared/<TUNNEL-ID>.json

ingress:
  - hostname: api.mydomain.com
    service: http://localhost:8000
  - service: http_status:404

Start the tunnel as a system service:

sudo cloudflared service install
sudo systemctl enable cloudflared
sudo systemctl start cloudflared
sudo systemctl status cloudflared

Your API is now publicly accessible at https://api.mydomain.com, protected by Cloudflare, with no ports exposed on the router.


Part 6: What’s the secret ?

In this implementation, I did not set up a dedicated secrets manager for handling environment variables or other sensitive configuration values. For simplicity and speed, environment variables were defined directly in the .env file on the Raspberry Pi.

However, for app handling sensitive data, it’s highly recommended to use a secure secrets management solution instead of hardcoding credentials or storing them in plaintext repositories. Popular options include:

  1. AWS Secrets Manager
    A fully managed service to store, retrieve, and rotate secrets programmatically. Easily integrates with GitHub Actions via the aws-actions/configure-aws-credentials action and the AWS CLI.

  2. Azure Key Vault
    Microsoft’s solution for managing keys, secrets, and certificates with granular access policies. You can access secrets in the GitHub Actions workflows using the Azure/login and Azure/get-keyvault-secrets actions.

  3. 1Password Secrets Automation
    A developer-friendly way to manage secrets, especially if you already use 1Password. Their GitHub Action enables retrieving secrets securely at runtime.


Optional: Restart Cloudflared on Deploy

If the deployment changes configs or dependencies, you can restart the tunnel as part of the GitHub Actions SSH step:

ssh -o StrictHostKeyChecking=no ${{ secrets.RPI_USER }}@${{ secrets.RPI_IP }} << 'EOF'
  cd /home/${{ secrets.RPI_USER }}/myapp
  docker compose down
  docker compose up -d --build --quiet-pull
  sudo systemctl restart cloudflared
EOF

Why Combine Tailscale and Cloudflare Tunnel?

Tailscale: secure, zero-trust SSH/SCP/CI/CD access
Cloudflare Tunnel: selectively expose only specific HTTP endpoints to the public internet

This combination gives you VPN-like SSH and CI/CD security while publicly exposing only what you choose, without exposing the Raspberry Pi’s SSH port or other private services to the internet.


Final Thoughts

With:

  1. Docker Compose running on Raspberry Pi
  2. CI/CD pipeline using GitHub Actions + Tailscale
  3. Cloudflare Tunnel providing secure public access

We’ve created a secure, automated deployment pipeline for a lightweight edge server or home lab.

Whether you’re hosting an API, internal dashboard, IoT controller, or hobby SaaS project, this stack is great for personal or low-traffic deployments.

It’s a great example of how far small, affordable devices like the Raspberry Pi can go when combined with modern dev tools.