D4.2 Docker Compose Files με Jinja2 🎨
Γιατί Jinja2 για Compose Files;
┌─────────────────────────────────────────────────────────┐
│ Στατικό docker-compose.yml │
│ ❌ Hardcoded values │
│ ❌ Ένα αρχείο για κάθε environment │
│ ❌ Passwords σε plaintext │
├─────────────────────────────────────────────────────────┤
│ docker-compose.yml.j2 (Jinja2 Template) │
│ ✅ Dynamic values από Ansible vars │
│ ✅ Ένα template για όλα τα environments │
│ ✅ Secrets από Ansible Vault │
│ ✅ Προσαρμογή βάσει facts (CPU, RAM) │
└─────────────────────────────────────────────────────────┘
Βασικό Compose Template
mkdir -p ~/ansible/roles/myapp/templates
cat > ~/ansible/roles/myapp/templates/docker-compose.yml.j2 << 'EOF'
{# docker-compose.yml.j2 #}
{# Managed by Ansible — DO NOT EDIT MANUALLY! #}
{# Generated: {{ ansible_facts['date_time']['iso8601'] }} #}
{# Host: {{ inventory_hostname }} #}
version: "3.8"
{# ── Υπολογισμός resources βάσει facts ──────── #}
{% set total_ram_mb = ansible_facts['memtotal_mb'] | int %}
{% set cpu_count = ansible_facts['processor_vcpus'] | int %}
{# ── Memory allocations ───────────────────────── #}
{% set nginx_mem = '64m' %}
{% set app_mem = [256, (total_ram_mb * 0.25) | int] | min ~ 'm' %}
{% set postgres_mem = [512, (total_ram_mb * 0.30) | int] | min ~ 'm' %}
{% set redis_mem = [128, (total_ram_mb * 0.10) | int] | min ~ 'm' %}
services:
# ══════════════════════════════════════════
# NGINX — Reverse Proxy
# ══════════════════════════════════════════
nginx:
image: {{ nginx_image | default('nginx:1.25.3-alpine') }}
container_name: {{ app_name }}_nginx
restart: unless-stopped
ports:
- "{{ nginx_host_port | default(80) }}:80"
{% if nginx_ssl_enabled | default(false) %}
- "{{ nginx_ssl_port | default(443) }}:443"
{% endif %}
volumes:
- {{ app_config_dir }}/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- {{ app_logs_dir }}/nginx:/var/log/nginx
{% if nginx_ssl_enabled | default(false) %}
- {{ app_config_dir }}/ssl:/etc/nginx/ssl:ro
{% endif %}
- /etc/localtime:/etc/localtime:ro
networks:
- frontend
depends_on:
app:
condition: service_healthy
deploy:
resources:
limits:
memory: {{ nginx_mem }}
cpus: "0.25"
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
labels:
app: {{ app_name }}
component: nginx
env: {{ app_env }}
version: "{{ app_version }}"
# ══════════════════════════════════════════
# APPLICATION
# ══════════════════════════════════════════
app:
image: {{ app_image | default('nginx:1.25.3-alpine') }}
container_name: {{ app_name }}_app
restart: unless-stopped
env_file:
- {{ app_config_dir }}/.env
volumes:
- app_data:/app/data
- {{ app_logs_dir }}/app:/app/logs
- /etc/localtime:/etc/localtime:ro
networks:
- frontend
- backend
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
deploy:
resources:
limits:
memory: {{ app_mem }}
cpus: "{{ [1.0, (cpu_count * 0.5)] | min }}"
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:{{ app_port | default(3000) }}/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
labels:
app: {{ app_name }}
component: app
env: {{ app_env }}
version: "{{ app_version }}"
# ══════════════════════════════════════════
# POSTGRESQL
# ══════════════════════════════════════════
postgres:
image: {{ postgres_image | default('postgres:15.4-alpine') }}
container_name: {{ app_name }}_postgres
restart: unless-stopped
environment:
POSTGRES_DB: {{ postgres_db }}
POSTGRES_USER: {{ postgres_user }}
POSTGRES_PASSWORD: {{ vault_postgres_password }}
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- postgres_data:/var/lib/postgresql/data
- /etc/localtime:/etc/localtime:ro
networks:
- backend
deploy:
resources:
limits:
memory: {{ postgres_mem }}
cpus: "{{ [1.0, (cpu_count * 0.5)] | min }}"
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U {{ postgres_user }} -d {{ postgres_db }}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
labels:
app: {{ app_name }}
component: postgres
env: {{ app_env }}
# ══════════════════════════════════════════
# REDIS
# ══════════════════════════════════════════
redis:
image: {{ redis_image | default('redis:7.2-alpine') }}
container_name: {{ app_name }}_redis
restart: unless-stopped
command: >
redis-server
--maxmemory {{ redis_maxmem | default('128mb') }}
--maxmemory-policy allkeys-lru
--appendonly yes
--requirepass {{ vault_redis_password }}
volumes:
- redis_data:/data
- /etc/localtime:/etc/localtime:ro
networks:
- backend
deploy:
resources:
limits:
memory: {{ redis_mem }}
cpus: "0.25"
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD", "redis-cli", "--no-auth-warning", "-a", "{{ vault_redis_password }}", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
labels:
app: {{ app_name }}
component: redis
env: {{ app_env }}
# ══════════════════════════════════════════
# MONITORING (μόνο αν enabled)
# ══════════════════════════════════════════
{% if monitoring_enabled | default(false) %}
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: {{ app_name }}_cadvisor
restart: unless-stopped
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
ports:
- "{{ cadvisor_port | default(8080) }}:8080"
networks:
- frontend
deploy:
resources:
limits:
memory: 128m
cpus: "0.25"
{% endif %}
# ══════════════════════════════════════════
# VOLUMES
# ══════════════════════════════════════════
volumes:
postgres_data:
name: {{ app_name }}_postgres_data
labels:
app: {{ app_name }}
env: {{ app_env }}
redis_data:
name: {{ app_name }}_redis_data
labels:
app: {{ app_name }}
env: {{ app_env }}
app_data:
name: {{ app_name }}_app_data
labels:
app: {{ app_name }}
env: {{ app_env }}
# ══════════════════════════════════════════
# NETWORKS
# ══════════════════════════════════════════
networks:
frontend:
name: {{ app_name }}_frontend
driver: bridge
backend:
name: {{ app_name }}_backend
driver: bridge
internal: true {# ← χωρίς πρόσβαση στο internet #}
EOF
Environment File Template (.env.j2)
cat > ~/ansible/roles/myapp/templates/app.env.j2 << 'EOF'
{# app.env.j2 — Application Environment Variables #}
{# Managed by Ansible Vault — DO NOT EDIT MANUALLY! #}
# ── Application ───────────────────────────────
NODE_ENV={{ app_env }}
APP_VERSION={{ app_version }}
PORT={{ app_port | default(3000) }}
LOG_LEVEL={{ 'debug' if app_env != 'production' else 'info' }}
# ── Database ──────────────────────────────────
DB_HOST=postgres
DB_PORT={{ postgres_port | default(5432) }}
DB_NAME={{ postgres_db }}
DB_USER={{ postgres_user }}
DB_PASSWORD={{ vault_postgres_password }}
DATABASE_URL=postgresql://{{ postgres_user }}:{{ vault_postgres_password }}@postgres:{{ postgres_port | default(5432) }}/{{ postgres_db }}
# ── Redis ─────────────────────────────────────
REDIS_HOST=redis
REDIS_PORT={{ redis_port | default(6379) }}
REDIS_PASSWORD={{ vault_redis_password }}
REDIS_URL=redis://:{{ vault_redis_password }}@redis:{{ redis_port | default(6379) }}/0
# ── Security ──────────────────────────────────
SECRET_KEY={{ vault_app_secret_key }}
JWT_SECRET={{ vault_jwt_secret | default(vault_app_secret_key) }}
# ── Feature flags ─────────────────────────────
{% if app_env == 'production' %}
DEBUG=false
SENTRY_ENABLED=true
CACHE_ENABLED=true
{% else %}
DEBUG=true
SENTRY_ENABLED=false
CACHE_ENABLED=false
{% endif %}
# ── Server info ───────────────────────────────
SERVER_HOSTNAME={{ inventory_hostname }}
SERVER_IP={{ ansible_facts['default_ipv4']['address'] }}
EOF
Nginx Config Template
cat > ~/ansible/roles/myapp/templates/nginx_compose.conf.j2 << 'EOF'
{# nginx_compose.conf.j2 #}
{# Managed by Ansible — DO NOT EDIT! #}
user nginx;
worker_processes {{ ansible_facts['processor_vcpus'] | default(1) }};
pid /var/run/nginx.pid;
events {
worker_connections 1024;
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
server_tokens off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# ── Logging ───────────────────────────────
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
# ── Gzip ──────────────────────────────────
gzip on;
gzip_vary on;
gzip_types text/plain text/css application/json
application/javascript text/xml;
# ── Upstream ──────────────────────────────
upstream app_backend {
least_conn;
server app:{{ app_port | default(3000) }};
keepalive 32;
}
server {
listen 80;
server_name {{ inventory_hostname }} _;
client_max_body_size {{ nginx_client_max_body | default('10M') }};
# ── Health check ──────────────────────
location /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
# ── Static files ──────────────────────
{% if nginx_static_enabled | default(false) %}
location /static/ {
alias /var/www/static/;
expires 1y;
add_header Cache-Control "public, immutable";
gzip_static on;
}
{% endif %}
# ── API rate limiting ─────────────────
{% if nginx_rate_limit | default(true) %}
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://app_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
{% endif %}
# ── Application ───────────────────────
location / {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# ── Security headers ──────────────
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
proxy_connect_timeout 60s;
proxy_read_timeout 60s;
}
}
}
EOF
Tasks που χρησιμοποιούν τα Templates
# roles/myapp/tasks/compose_deploy.yml
---
# ============================================================
# Docker Compose Deployment με Templates
# ============================================================
# ── Δημιουργία directories ────────────────────
- name: Create app directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: '0755'
loop:
- "{{ app_base_dir }}"
- "{{ app_config_dir }}"
- "{{ app_config_dir }}/nginx"
- "{{ app_logs_dir }}"
- "{{ app_logs_dir }}/nginx"
- "{{ app_logs_dir }}/app"
# ── Render templates ──────────────────────────
- name: Render docker-compose.yml
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ app_base_dir }}/docker-compose.yml"
mode: '0644'
owner: root
register: compose_file
- name: Render .env file
ansible.builtin.template:
src: app.env.j2
dest: "{{ app_config_dir }}/.env"
mode: '0600' # ← strict permissions για secrets!
owner: root
no_log: true # ← κρύψε secrets από output
register: env_file
- name: Render nginx config
ansible.builtin.template:
src: nginx_compose.conf.j2
dest: "{{ app_config_dir }}/nginx/nginx.conf"
mode: '0644'
register: nginx_config
# ── Validate compose file ─────────────────────
- name: Validate docker-compose.yml
ansible.builtin.command:
cmd: docker compose -f {{ app_base_dir }}/docker-compose.yml config
chdir: "{{ app_base_dir }}"
register: compose_validate
changed_when: false
- name: Validation result
ansible.builtin.debug:
msg: "✅ docker-compose.yml is valid"
# ── Deploy stack ──────────────────────────────
- name: Deploy Docker Compose stack
community.docker.docker_compose_v2:
project_src: "{{ app_base_dir }}"
project_name: "{{ app_name }}"
state: present
pull: "{{ compose_pull | default('missing') }}"
register: compose_result
no_log: true # ← .env περιέχει secrets
- name: Compose deployment result
ansible.builtin.debug:
msg:
- "Stack : {{ app_name }}"
- "Changed : {{ compose_result.changed }}"
# ── Wait for healthy ──────────────────────────
- name: Wait for stack to be healthy
ansible.builtin.command:
cmd: >
docker compose
-f {{ app_base_dir }}/docker-compose.yml
-p {{ app_name }}
ps --format json
chdir: "{{ app_base_dir }}"
register: stack_status
changed_when: false
retries: 10
delay: 10
until: >
stack_status.stdout | from_json |
selectattr('Health', 'equalto', 'unhealthy') |
list | length == 0
ignore_errors: true
- name: Stack status
ansible.builtin.debug:
msg: "{{ stack_status.stdout_lines }}"
Override Files ανά Environment
# Production override
cat > ~/ansible/roles/myapp/templates/docker-compose.prod.yml.j2 << 'EOF'
{# Production overrides #}
version: "3.8"
services:
app:
deploy:
resources:
limits:
memory: {{ app_memory_prod | default('1g') }}
cpus: "{{ app_cpus_prod | default('2.0') }}"
postgres:
deploy:
resources:
limits:
memory: {{ postgres_memory_prod | default('2g') }}
cpus: "1.0"
# Logging to external service in production
{% if log_aggregator_url is defined %}
x-logging: &default-logging
driver: syslog
options:
syslog-address: "{{ log_aggregator_url }}"
tag: "{{ app_name }}"
{% endif %}
EOF
# Development override
cat > ~/ansible/roles/myapp/templates/docker-compose.dev.yml.j2 << 'EOF'
{# Development overrides #}
version: "3.8"
services:
app:
environment:
DEBUG: "true"
LOG_LEVEL: debug
volumes:
- ./src:/app/src # ← hot reload!
postgres:
ports:
- "5432:5432" # ← expose για local access
redis:
ports:
- "6379:6379" # ← expose για local access
EOF
# Χρήση override files
tasks:
# ── Render override files ─────────────────
- name: Render production override
ansible.builtin.template:
src: docker-compose.prod.yml.j2
dest: "{{ app_base_dir }}/docker-compose.prod.yml"
mode: '0644'
when: app_env == 'production'
# ── Deploy με overrides ───────────────────
- name: Deploy με production overrides
community.docker.docker_compose_v2:
project_src: "{{ app_base_dir }}"
project_name: "{{ app_name }}"
files:
- docker-compose.yml
- "docker-compose.{{ app_env }}.yml"
state: present
when: app_env == 'production'
Diff Mode — Τι θα αλλάξει
# Δες τις αλλαγές στο compose file ΠΡΙΝ deploy
ansible-playbook playbooks/compose-deploy.yml \
--check --diff \
--limit nextcloud
# Output:
# TASK [Render docker-compose.yml]
# --- before: /opt/myapp/docker-compose.yml
# +++ after: /opt/myapp/docker-compose.yml
# @@ -10,7 +10,7 @@
# services:
# app:
# - image: myapp:1.0.0
# + image: myapp:1.1.0 ← αλλαγή version!
Σύνοψη D4.2
Compose Files με Jinja2
├── Template: docker-compose.yml.j2
│ ├── Dynamic values από Ansible vars
│ ├── Resource limits βάσει facts
│ ├── Conditional services (monitoring)
│ └── Environment-specific config
│
├── .env.j2 (secrets template):
│ ├── Vault variables
│ ├── no_log: true
│ └── mode: '0600'
│
├── Override files:
│ ├── docker-compose.prod.yml.j2
│ ├── docker-compose.dev.yml.j2
│ └── docker_compose_v2: files=[...]
│
├── Validation:
│ └── docker compose config (πριν deploy)
│
└── Workflow:
template → render → validate → deploy