Ansible-based homelab infrastructure automation system using Docker Swarm for container orchestration and Traefik for reverse proxy with dynamic routing.
- Docker Swarm Orchestration: Automatic swarm initialization and overlay networking
- Traefik Reverse Proxy: Dynamic file-based routing with automatic service discovery
- Template-driven Deployment: Consistent service deployment using Jinja2 templates
- Hot Reload: Route updates without service restarts
- Automated Maintenance: Log rotation and system cleanup via cron jobs
This system is designed to automate the deployment of services to a Docker Swarm cluster. Here’s a visual overview of the workflow when you add a new service:
graph TD
subgraph "Your Local Machine"
A["<br><b>You</b><br>"] -- "1. Run ansible-playbook" --> B(vars/hello.yaml + playbook command)
end
subgraph "Homelab Server"
C[Ansible `service` Role] -->|Uses| D(Jinja2 Templates)
C -->|2. Generates| E(services/hello.yaml)
C -->|2. Generates| F(dynamic/routers/hello.yaml)
E -->|4. Deploy| G(Docker Swarm)
F -->|3. Hot Reload| H(Traefik)
end
I["<br><b>End User</b><br>"]
B -- SSH --> C
G --> H
H -->|5. Access Service| I
Workflow Steps:
- Run Playbook: You execute the
ansible-playbook
command from your local machine, targeting your homelab server. You pass a service definition file (e.g.,vars/hello.yaml
) containing all the necessary variables for your new service. - Generate Configs: Ansible connects to the server via SSH. The
service
role uses Jinja2 templates to generate two critical files based on your variables:- A Docker Compose file in
/opt/homelab/services/
that defines your application as a Docker Swarm service. - A Traefik routing configuration file in
/opt/homelab/dynamic/routers/
that tells Traefik how to route traffic to your new service.
- A Docker Compose file in
- Hot Reload: Traefik is configured to use Docker for service discovery and a file provider for dynamic routing. It automatically detects the new routing file and reloads its configuration on the fly, without requiring a restart.
- Deploy: The Ansible playbook uses the
docker_stack
module to deploy your service to the Docker Swarm cluster. Docker Swarm ensures the service is running according to your definition (e.g., correct image, replicas, etc.). - Access Service: The service is now live and accessible to end-users through the Traefik reverse proxy at the host and path you defined.
- Ansible installed on control machine
- SSH access to target server (configure in
inventory.ini
) - Docker installed on target server
Copy and configure the inventory file:
cp example.inventory.ini inventory.ini
# Edit inventory.ini with your server details (host, user, SSH key path)
inventory.ini
secure as it contains sensitive connection details.
Bootstrap the entire homelab infrastructure:
ansible-playbook -i inventory.ini playbooks/initialize.yaml -e "main_domain=yourdomain.com"
This will:
- Create necessary directories
- Initialize Docker Swarm
- Set up overlay network
- Deploy Traefik with dynamic routing (dashboard accessible at yourdomain.com/traefik/dashboard)
- Configure log rotation and maintenance cron jobs
Required: Replace yourdomain.com
with your actual domain where Traefik will be accessible.
Create a vars file for your service in the vars/
directory:
# vars/hello.yml
name: hello
image: devmuhfaris/hello-world
port: 80
host: apps.yourdomain.com
path: /hello
replicas: 1
For services with static assets (Next.js, React, etc.), add assets routing:
# vars/webapp.yml
name: webapp
image: yourorg/webapp:latest
port: 3000
host: apps.yourdomain.com
path: /webapp
replicas: 2
priority: 2000
assets_paths:
- "/_next/"
- "/favicon.ico"
- "/robots.txt"
- "/static/"
Deploy the service:
ansible-playbook -i inventory.ini playbooks/add-service.yaml --extra-vars @vars/hello.yml
Required:
name
: Service identifierimage
: Docker image referenceport
: Internal service porthost
: External hostnamepath
: URL path for routing
Optional:
replicas
: Number of service replicas (default: 1)priority
: Traefik routing priority (default: 2000)assets_paths
: List of asset paths for separate routingassets_priority
: Priority for assets router (default: priority + 1000)cpu
: CPU allocation per replica (default: "0.25")memory
: Memory allocation per replica (default: "64M")command
: The full command to run in the container. You are responsible for including any flags needed to read a configuration file.service_config
: A dictionary of structured configuration data to be mounted as a file into the container.service_config_format
: The format of the configuration file (yaml
orjson
). Defaults toyaml
.service_config_target_path
: The full path inside the container where the configuration file will be mounted (e.g.,/app/config.yaml
). Defaults to/<name>.config.<format>
.service_config_name_override
: Allows you to specify a custom name for the Docker Swarm config resource, overriding the auto-generated hashed name.internal_only
: Whentrue
, skips Traefik routing and HTTP labels (for DBs/queues).host
,path
, andport
are not required.env
/env_vars
: Map of environment variables for the container. Avoid usingenvironment
as a var name (reserved by Jinja/Ansible).ports
: List of published ports (e.g.,[{ target: 5432, published: 5432 }]
). Optional fields:protocol
,mode
. Ifmode
is omitted, Swarm defaults toingress
.volumes
: List of named volume mounts (e.g.,[{ name: pgdata, target: /var/lib/postgresql/data }]
).healthcheck
: Compose-style healthcheck fields:test
,interval
,timeout
,retries
,start_period
.
Modify your vars file and redeploy:
ansible-playbook -i inventory.ini playbooks/add-service.yaml --extra-vars @vars/hello.yml
Changes to routing configuration trigger hot reloads. Changes to image or replicas trigger rolling updates.
For services not exposed via Traefik, set internal_only: true
and provide only what the container needs (e.g., env, volumes, optional published ports):
# vars/redis.yml
name: redis
image: redis:7-alpine
internal_only: true
command: "redis-server --appendonly yes"
volumes:
- name: redis-data
target: /data
ports: # optional, if you want LAN access
- target: 6379
published: 6379
mode: host
Postgres example with env variables:
# vars/db_idcards.yaml
name: db_idcards
image: postgres:16-alpine
internal_only: true
env:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: change-me
volumes:
- name: db_idcards_data
target: /var/lib/postgresql/data
For applications requiring a configuration file, you have full control over how it's mounted and used. The system no longer automatically injects any command-line flags, giving you the flexibility to support any application.
When you define service_config
in your service's variable file, the system creates a Docker Swarm config object from your data. You then tell your application how to use it.
- Define the Configuration Content: Add your structured data to the
service_config
variable. - Specify the Mount Path: Use
service_config_target_path
to define exactly where the configuration file should be placed inside your container. This path should match what your application expects. - Provide the Full Command: Use the
command
variable to specify the exact command to run your application, including the flag needed to read the config file (e.g.,-c
,--config-file
, etc.).
Here’s an example of a service that uses a custom flag (-c
) and a custom path (/etc/my-app/settings.yaml
) for its configuration:
# vars/my-app.yml
name: my-app
image: my-org/my-app:latest
port: 8080
host: apps.yourdomain.com
path: /my-app
# 1. Define the config content
service_config:
database:
host: db.internal
port: 5432
api_keys:
- key: "key1"
value: "value1"
# 2. Specify the target path inside the container
service_config_target_path: /etc/my-app/settings.yaml
# 3. Provide the full command, including the custom flag and path
command: "node server.js -c /etc/my-app/settings.yaml"
In this example:
- A configuration file is created from the
service_config
data. - It is mounted inside the container at
/etc/my-app/settings.yaml
. - The service's command is executed exactly as
node server.js -c /etc/my-app/settings.yaml
.
This approach gives you the flexibility to work with any application, regardless of its specific command-line arguments.
/opt/homelab/ # Root directory
├── dynamic # Traefik dynamic routing
│ └── routers
│ └── hello.yaml
├── hl-traefik.yaml
└── services # Docker Compose files
└── hello.yaml
/var/log/traefik/ # Traefik logs
├── access.log # Access logs
├── traefik.log # Application logs
The system uses a modular Ansible role structure:
- common: Infrastructure bootstrap and maintenance
- traefik: Reverse proxy configuration and dynamic routing
- service: Template-driven service deployment
Services are deployed as Docker Swarm services with automatic Traefik integration for routing and load balancing.