Deployment Guide
Summary Overview
A production-grade workflow for deploying the homelab stack using Infrastructure as Code (IaC) and Docker Compose.
Homelab Deployment
This guide details the operational procedure used to deploy and maintain the homelab services. It demonstrates a reproducible, code-driven approach rather than manual configuration, ensuring that the infrastructure can be rebuilt from scratch in minutes.
Prerequisites
- Infrastructure: Azure VPS (Ubuntu 22.04+) with a static public IP.
- Domain: Valid domain (e.g.,
ivanncabardo.tech) configured with A records pointing to the VPS IP. - Tools: Git, Docker Engine, Docker Compose v2.
Deployment Workflow
The deployment is broken down into layers to ensure stability and proper dependency management.
Layer 1: The Edge (Traefik)
The first step is establishing the secure perimeter. We deploy Traefik to handle certificate acquisition and routing. This layer must be healthy before any applications are exposed.
Service Configuration
We use docker-compose to define the Traefik service. Note the specific command flags used to enable the Docker provider and Let's Encrypt.
# traefik/docker-compose.yml
services:
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- traefik_proxy
ports:
- "80:80" # HTTP (Redirects to HTTPS)
- "443:443" # HTTPS
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock:ro # Access to listen for new containers
- ./traefik.yml:/traefik.yml:ro # Static config
- ./acme:/acme # Persistent storage for SSL certificates
Why this matters:
- volumes: We mount
acmeto persist certificates across restarts, preventing us from hitting Let's Encrypt rate limits. - security_opt:
no-new-privileges:trueprevents the container from gaining additional privileges (privilege escalation).
Layer 2: Management (Portainer)
We deploy Portainer to provide a visual interface for Docker management. Crucially, we do not expose Portainer's default port 9000. Instead, we route it through Traefik.
Secure Routing Labels
# webapps/portainer/docker-compose.yml
services:
portainer:
# ... image and volume config ...
networks:
- traefik_proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.portainer.rule=Host(`portainer.homelab.ivanncabardo.tech`)"
- "traefik.http.routers.portainer.entrypoints=websecure"
- "traefik.http.routers.portainer.tls.certresolver=letsencrypt"
- "traefik.http.services.portainer.loadbalancer.server.port=9000"
Breakdown:
traefik.enable=true: Explicitly tells Traefik to expose this container.Host(...): Defines the domain name that routes to this service.server.port=9000: Tells Traefik which internal port the container is listening on (since we aren't mapping it to the host).
Layer 3: Observability (Monitoring Stack)
The final layer brings visibility. We deploy the Prometheus/Grafana stack using a composed service definition.
Component Wiring
Prometheus needs to scrape metrics from other containers. This is achieved by joining them to the shared monitoring network.
# monitoring-stack/docker-compose.yaml
networks:
monitoring:
driver: bridge
traefik_proxy:
external: true
services:
prometheus:
# ... config ...
networks:
- monitoring
- traefik_proxy # Needs to be here to be exposed via URL
node-exporter:
# ... config ...
networks:
- monitoring # Internal only, no Traefik labels needed
Architecture Note: node-exporter is NOT exposed to the internet. It sits on the internal monitoring network, where Prometheus scrapes it securely.
Maintenance & Updates
Maintenance is handled via GitOps principles to ensure the running state always matches the code:
- Update:
git pullthe latest configuration from the repository. - Pull:
docker-compose pullto fetch updated container images. - Redeploy:
docker-compose up -dto recreate containers with new configs/images. Docker only restarts changed containers.