INITIALIZING
Technical

HomeLab with Docker and Raspberry Pi 5.

HomeLab with Docker and Raspberry Pi 5.

A HomeLab is a personal, often DIY setup of servers and networking gear used for learning, experimentation, and — if you squint hard enough and tilt your head — sometimes actual productivity. Mine currently runs on a couple of Raspberry Pis, which handle quick proof-of-concepts, unit testing, code analysis, and a genuinely alarming number of side projects that will absolutely never see production.

I'll give you my personal favorites for software development today. The fun ones — the ones I can't fully justify to my wife — are coming in a future post.

Let's begin.

Raspberry Pi

I'd recommend a Raspberry Pi 5, though honestly any computer with a pulse and a network card will do.

Raspberry Pi 5

  • CPU: Quad-core ARM Cortex-A76 @ 2.4 GHz (64-bit)
  • RAM: 4GB or 8GB LPDDR4X (there's a 16GB option, but at that price you're better off buying an old laptop off Craigslist from a guy named Gary — I recommend the 8GB)
  • GPU: Broadcom VideoCore VII, dual 4Kp60 output (you can even run Ollama on it — slow, like “watching paint cure” slow, but it works; we'll cover that from Docker in a future post)
  • microSD card slot (128GB is plenty for small workloads and POCs that will haunt you for years)
  • PCIe 2.0 x1 for an NVMe SSD via adapter, if you want a more permanent setup — for our purposes, a microSD is perfectly fine and infinitely easier to lose in a drawer
  • Gigabit Ethernet (preferred — this thing is about to carry a genuinely embarrassing amount of traffic)
  • Wi-Fi 802.11ac (dual band, for when Ethernet feels like too much commitment)
  • Power: USB-C PD, 5V/5A

Setup the Raspberry Pi

Flash the SD card (or NVMe drive) with the OS using the Raspberry Pi Imager. Pick your device, select the 64-bit OS, and — this part matters, underline it — edit the settings before flashing to enable Wi-Fi and SSH. I named my host homelab and used the entirely-not-suspicious username homelabadmin, which fools absolutely no one, least of all me.

Once flashed, pop the card in, power on the Pi, and connect over SSH (cable or Wi-Fi — your call, the Pi has no feelings about it either way). I use Windows Terminal, which is genuinely great, and I'll write a post on customizing it eventually — promise number forty-two on this blog, filed alongside the OData part two and the repository pattern post, in the increasingly crowded “coming soon” folder of my life.

ssh homelabadmin@homelab

We're ready to install everything.

Docker and software

As a rule, always update before you do anything else, because installing software on a stale system is how small problems mature into full weekend-consuming problems with their own subplot:

sudo apt update
sudo apt upgrade -y

Docker

Next, Docker itself — pull the install script straight from Docker and run it, no interpretive dance required:

curl -sSL https://get.docker.com | sh

Add your user to the docker group so you stop typing sudo in front of every single command like you're negotiating with a hostage-taker:

sudo usermod -aG docker $USER

Now type exit and SSH back in — the current shell session needs a fresh login to actually pick up the new group membership. Skipping this step is, by a wide margin, the single most common reason people email me confused about “permission denied,” and also the reason I've typed this exact sentence in probably four different posts now.

Docker Compose

We'll use Docker Compose throughout — it's a much saner way to hand Docker a full stack (containers, networking, volumes) instead of typing an increasingly unhinged docker run command with fifteen flags that reads like a ransom note. Managing everything gets even easier once Portainer's up, so let's start there.

Portainer

Portainer is a Docker management UI — containers, images, logs, networks, all in one place, all without you memorizing a single additional CLI incantation. Since we're not running Docker Desktop here, Portainer fills that gap nicely, and with considerably less RAM overhead.

Here's the repeatable pattern we'll use for every stack in this post, so get comfortable, you'll be typing this roughly a dozen more times:

sudo mkdir -p /opt/stacks/portainer
cd /opt/stacks/portainer
touch docker-compose.yml
sudo nano docker-compose.yml

Paste in:

services:
  portainer:
    image: portainer/portainer-ce:latest
    container_name: portainer
    restart: always
    ports:
      - "8000:8000"
      - "9443:9443"
      - "9000:9000"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - portainer_data:/data
volumes:
  portainer_data:

Save with Ctrl+X, Y, then Enter. Be wary of the spaces — YAML has extremely strong, extremely silent opinions about indentation and will ruin your afternoon without so much as an error message, like a passive-aggressive coworker.

Quick anatomy of a docker-compose file, since we'll see this shape repeatedly and I refuse to explain YAML twelve separate times:

  • services: — the containers you're actually running.
  • portainer: — the name of this particular service.
  • image: portainer/portainer-ce:latest — always pull the latest Community Edition build.
  • container_name: portainer — a real name instead of a random Docker-generated one that sounds like a Star Wars droid having an identity crisis.
  • restart: always — comes back up automatically after a crash or reboot, no babysitting, no 2am phone alerts.
  • ports:8000 for agent communication, 9443 for the secure web UI, 9000 for the classic HTTP UI.
  • volumes: — mounting the Docker socket gives Portainer control over Docker itself (a little terrifying if you think about it too hard, so don't), and portainer_data persists its settings even if the container gets deleted and recreated.

Bring it up:

docker compose up -d

If that errors out (usually a leftover permissions issue from before the group membership refresh), try:

sudo docker compose up -d

Browse to https://homelab:9443, follow the prompts to create an admin user, and you're in — full visibility into every container on the box, like a security camera room but for YAML files.

VS Code

Nano's fine, genuinely, but copy-pasting text between machines is broken often enough to be a personality-altering annoyance, so I run VS Code on the Pi itself via its web UI instead.

sudo mkdir -p /opt/stacks/vscode
cd /opt/stacks/vscode
touch docker-compose.yml
sudo nano docker-compose.yml
services:
  code-server:
    container_name: vscode
    image: lscr.io/linuxserver/code-server:latest
    environment:
      - TZ=America/Los_Angeles
      - DEFAULT_WORKSPACE=/config
    ports:
      - 2443:8443
    volumes:
      - ./config:/config
      - /:/host
    user: "1000:1000"
    restart: unless-stopped
docker compose up -d

Easy — and every remaining stack in this post follows this exact same pattern, so consider yourself trained.

Quick anatomy again: TZ sets your local timezone, DEFAULT_WORKSPACE opens /config on launch, 2443:8443 maps code-server's default secure port onto 2443 on the host, ./config persists your settings and extensions between restarts, and /:/host mounts your entire host filesystem into the container — genuinely powerful, genuinely capable of ruining your day, use it like you'd use a chainsaw: carefully, deliberately, and only when you actually mean it. user: "1000:1000" keeps file permissions sane, and restart: unless-stopped means it survives reboots without becoming clingy about it.

You now have a real editor, terminal, and file browser at https://localhost:2443. I mostly use it for creating and editing files, and reach for the actual terminal for everything else, because old habits and trust issues run deep — but there's a Docker extension if you'd rather run compose commands from inside the editor like a person with better boundaries than me.

I won't walk through every remaining stack in this level of detail, or we'll be here until next Tuesday — here's the rundown and the compose files.

Azurite

Azurite emulates Azure Storage services:

Genuinely great for low-budget Azure-shaped development, i.e. all the Azure without the monthly bill turning up like an uninvited relative.

services:
  azurite:
     image: mcr.microsoft.com/azure-storage/azurite
     container_name: azurite
     hostname: azurite
     command: 'azurite --loose --blobHost 0.0.0.0 --blobPort 10000 --queueHost 0.0.0.0 --queuePort 10001 --tableHost 0.0.0.0 --location /workspace --debug /workspace/debug.log'
     ports:
      - 10000:10000
      - 10001:10001
      - 10012:10002
     volumes:
      - ~/dockervolumes/azurite:/workspace

No UI here, but Azure Storage Explorer and the standard .NET storage libraries work against it exactly like the real thing, which is either impressive engineering or a very convincing bit.

Grafana with Postgres

Grafana turns data into genuinely beautiful dashboards and charts, the kind that make you look competent in meetings you didn't prepare for. We're pairing it with Postgres to persist its own configuration:

services:
  grafana_postgres:
    container_name: grafana_postgres
    image: postgres:18-alpine
    restart: always
    environment:
      POSTGRES_DB: grafana_db
      POSTGRES_USER: grafana_usr
      POSTGRES_PASSWORD: myweirdPassword!!!
    volumes:
      - postgres-storage:/var/lib/postgresql/data
  grafana:
    image: grafana/grafana-enterprise:latest
    container_name: grafana
    restart: unless-stopped
    environment:
      GF_DATABASE_TYPE: postgres
      GF_DATABASE_HOST: grafana_postgres
      GF_DATABASE_NAME: grafana_db
      GF_DATABASE_USER: grafana_usr
      GF_DATABASE_PASSWORD: myweirdPassword!!!
    ports:
      - '3200:3000'
    volumes:
      - grafana-storage:/var/lib/grafana
volumes:
  grafana-storage: {}
  postgres-storage: {}

Log in at http://homelab:3200. Fun Grafana project ideas: a speed-test monitor (run periodic speed tests, store results in Postgres, chart them, discover exactly how much your ISP has been lying to you), a weather dashboard pulling from a public weather API, or ingesting .NET telemetry for analysis because staring at raw log files is a form of self-harm.

Uptime Kuma

Since Grafana's already handling “what does my data look like,” it's the perfect neighbor for something that answers the far more urgent question: “is my stuff actually still alive.” Uptime Kuma is a genuinely lovely self-hosted status page and uptime monitor — point it at your other homelab services (or anything with a URL) and it'll ping them on a schedule, alert you when something goes down, and hand you a clean public or private status page, no duct tape required.

services:
  uptime-kuma:
    image: louislam/uptime-kuma:2
    container_name: uptime-kuma
    restart: unless-stopped
    ports:
      - "3001:3001"
    volumes:
      - uptime-kuma-data:/app/data
volumes:
  uptime-kuma-data:

Log in at http://homelab:3001, run through the (mercifully short) setup wizard, and start adding monitors — HTTP checks, TCP ports, even other Docker containers directly. It's the kind of tool you don't think you need until the one time your Portainer instance quietly faceplants at 2am and something, anything, finally tells you before your spouse does.

Hashicorp Vault

Create secrets and access them securely. I personally prefer Azure Key Vault, but that costs real money, and the entire point of this exercise is building our own HomeLab out of spite and spare Raspberry Pis:

services:
  vault:
    image: hashicorp/vault:2.0
    container_name: vault
    cap_add:
      - IPC_LOCK  # Lock memory to prevent sensitive data from swapping to disk
    environment:
      VAULT_DEV_ROOT_TOKEN_ID: keytouse  # Root token for dev mode
      VAULT_ADDR: http://0.0.0.0:8200  # Set the Vault address
    ports:
      - "8200:8200"  # Expose Vault on port 8200
    command: server -dev  # Start Vault in development mode
    volumes:
      - vault-data:/vault/file  # For future use, persistent storage in production
    healthcheck:
      test: ["CMD", "vault", "status"]
      interval: 30s
      timeout: 10s
      retries: 5
volumes:
  vault-data:

Access it at http://homelab:8200. Note this is Vault 2.0 — a real major-version jump from the 1.x line, so if you're migrating an existing setup rather than starting fresh, skim the upgrade notes before you leap. For a brand-new dev-mode homelab instance like this one, it's a clean start either way, no baggage.

Infisical

If Vault feels like slightly more ceremony than your homelab secrets actually deserve, Infisical is worth a look — open-source, with a genuinely pleasant web UI right out of the box (no CLI-only dev mode required just to get a login screen), and built specifically around “team shares secrets across projects and environments” rather than being a general-purpose secrets engine that also grudgingly does that.

It needs a database and a cache behind it, so it's a three-container affair:

services:
  infisical-db:
    image: postgres:14-alpine
    container_name: infisical-db
    restart: always
    environment:
      POSTGRES_USER: infisical
      POSTGRES_PASSWORD: myInfisicalDbPassword!!
      POSTGRES_DB: infisical
    volumes:
      - infisical-db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U infisical"]
      interval: 5s
      timeout: 10s
      retries: 10

  infisical-redis:
    image: redis:8-alpine
    container_name: infisical-redis
    restart: always
    volumes:
      - infisical-redis:/data

  infisical:
    image: infisical/infisical:latest
    container_name: infisical
    restart: unless-stopped
    depends_on:
      infisical-db:
        condition: service_healthy
      infisical-redis:
        condition: service_started
    environment:
      ENCRYPTION_KEY: f13dbc92aaaf86fa7cb0ed8ac3265f47
      AUTH_SECRET: 5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=
      DB_CONNECTION_URI: postgres://infisical:myInfisicalDbPassword!!@infisical-db:5432/infisical
      REDIS_URL: redis://infisical-redis:6379
      SITE_URL: http://homelab:9200
      NODE_ENV: production
    ports:
      - "9200:8080"

volumes:
  infisical-db:
  infisical-redis:

Log in at http://homelab:9200 and create your first account. That ENCRYPTION_KEY and AUTH_SECRET above are Infisical's own published sample values — genuinely, seriously, never reuse them, generate fresh random ones the instant you deploy this for real. Same rule as every password in this post that isn't already screaming “obviously fake, please don't” at you from the page.

Postgres here is intentionally pinned at 14-alpine rather than the 18 we used elsewhere — that's the version Infisical's own official compose file specifies, and app-paired databases are exactly the place where I do not go freelancing version bumps just to chase a bigger number for bragging rights.

IT-Tools

Ever wanted one site with every random thing a software engineer needs, daily, without fifteen browser tabs spread across fifteen websites that all look like they were designed in 2011?

  • Decoding JWTs
  • Generating random strings
  • Creating public/private key pairs
services:
    it-tools:
        image: 'corentinth/it-tools:latest'
        ports:
            - '2546:80'
        container_name: it-tools
http://homelab:2546

Postgres

The open-source, undefeated database that can basically do everything short of making your coffee, and honestly give it a few more releases:

services:
  db:
    image: postgres:18-alpine
    restart: always
    environment:
      - POSTGRES_USER=myadminuser
      - POSTGRES_PASSWORD=mySuperSecureAdminPassword@#!
    ports:
      - 5432:5432
    volumes:
      - ./data:/var/lib/postgresql/data

No UI on its own, but pgAdmin covers that:

services:
  pgadmin:
    image: dpage/pgadmin4:latest
    environment:
      PGADMIN_DEFAULT_EMAIL: "madeupemail@server.com"
      PGADMIN_DEFAULT_PASSWORD: "mySuperSecurePGAdminPassword9*&%"
    ports:
        - "8095:80"
http://homelab:8095

MQ-Rabbit

Open-source queuing, more capable than Azure Storage Queues, topics included, and a mascot that's frankly more charming than it has any right to be:

services:
  rabbitmq:
    image: rabbitmq:management
    container_name: rabbitmq
    environment:
      - RABBITMQ_DEFAULT_USER=mymquser
      - RABBITMQ_DEFAULT_PASS=rabbitmqpass@#$
    ports:
      - "5672:5672"
      - "15672:15672"

networks:
  default:
    driver: bridge
http://homelab:15672

Redis

Fast cache and queuing, zero drama, exactly the personality trait you want in infrastructure:

services:
  cache:
    image: redis:8-alpine
    restart: always
    ports:
      - '6379:6379'
    command: redis-server --save 20 1 --loglevel warning --requirepass myredispassword&*^
    volumes:
      - cache:/data
volumes:
  cache:
    driver: local

No UI, but AnotherRedisDesktopManager is genuinely one of the better GUI clients out there, and has a name that reads like someone gave up on branding entirely and I respect that.

SonarQube

Yes, there's a community edition, and yes, it's genuinely usable for real code analysis — no, it will not go easy on you, and honestly, it shouldn't:

services:
  sonarqube:
    image: sonarqube:community
    hostname: sonarqube
    container_name: sonarqube
    read_only: true
    depends_on:
      db:
        condition: service_healthy
    environment:
      SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonar
      SONAR_JDBC_USERNAME: sonar
      SONAR_JDBC_PASSWORD: sonar
    volumes:
      - sonarqube_data:/opt/sonarqube/data
      - sonarqube_extensions:/opt/sonarqube/extensions
      - sonarqube_logs:/opt/sonarqube/logs
      - sonarqube_temp:/opt/sonarqube/temp
    ports:
      - "9900:9000"
    networks:
      - ${NETWORK_TYPE:-ipv4}
  db:
    image: postgres:17-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready"]
      interval: 10s
      timeout: 5s
      retries: 5
    hostname: postgresql
    container_name: postgresql
    environment:
      POSTGRES_USER: sonar
      POSTGRES_PASSWORD: sonar
      POSTGRES_DB: sonar
    volumes:
      - postgresql:/var/lib/postgresql
      - postgresql_data:/var/lib/postgresql/data
    networks:
      - ${NETWORK_TYPE:-ipv4}

volumes:
  sonarqube_data:
  sonarqube_temp:
  sonarqube_extensions:
  sonarqube_logs:
  postgresql:
  postgresql_data:

networks:
  ipv4:
    driver: bridge
    enable_ipv6: false
  dual:
    driver: bridge
    enable_ipv6: true
    ipam:
      config:
        - subnet: "192.168.2.0/24"
          gateway: "192.168.2.1"
        - subnet: "2001:db8:2::/64"
          gateway: "2001:db8:2::1"
http://homelab:9900

These are my must-run containers — I have plenty more, but they're tied to specific projects rather than general homelab infrastructure, and this post is long enough without me listing every random tool I've spun up once and never turned off. Next time, I'll show you how to point SonarQube at a real C# project and actually read the results instead of just staring at the dashboard feeling vaguely judged.

Happy coding!!!

Antes de elegir entre qué detalles valorar en una equipación de fútbol, merece la pena comprobar la talla, el material y el uso previsto. Las fotografías y la ficha del producto deberían confirmar el precio total, el plazo de entrega y las condiciones de devolución.