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-playbookcommand 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
servicerole 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_stackmodule 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: 1For 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.ymlRequired:
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 (yamlorjson). 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, andportare not required.env/env_vars: Map of environment variables for the container. Avoid usingenvironmentas a var name (reserved by Jinja/Ansible).ports: List of published ports (e.g.,[{ target: 5432, published: 5432 }]). Optional fields:protocol,mode. Ifmodeis 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.ymlChanges 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: hostPostgres 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/dataFor 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_configvariable. - Specify the Mount Path: Use
service_config_target_pathto 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
commandvariable 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_configdata. - 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 logsThe 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.