D7.6 Πρακτικό παράδειγμα — Multi-container με Network & Volumes 🚀
Τι θα φτιάξουμε
Μια πλήρης multi-container εφαρμογή με proper networking και volume management:
┌─────────────────────────────────────────────────────────┐
│ Production Stack │
│ │
│ Internet :80/:443 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ frontend_net │ │
│ │ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ nginx │ │ myapp │ │ │
│ │ │ (proxy) │──►│ (node/py) │ │ │
│ │ └──────────┘ └──────┬───────┘ │ │
│ └───────────────────────┼───────────────────────-┘ │
│ │ │
│ ┌────────────────────────┼────────────────────────┐ │
│ │ backend_net │ │ │
│ │ ▼ │ │
│ │ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ postgres │ │ redis │ │ │
│ │ │ :5432 │ │ :6379 │ │ │
│ │ └──────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ Volumes: │
│ postgres_data → /var/lib/postgresql/data │
│ redis_data → /data │
│ app_uploads → /app/uploads │
│ nginx_certs → /etc/nginx/certs (bind mount) │
└─────────────────────────────────────────────────────────┘
Inventory
# hosts.ini
[all_managed]
nextcloud ansible_host=192.168.1.10
[all_managed:vars]
ansible_user=ansible
ansible_ssh_private_key_file=~/.ssh/ansible_ed25519
ansible_become=true
Group Variables
# group_vars/all_managed.yml
app_name: myapp
app_version: "{{ new_version | default('latest') }}"
app_env: production
app_domain: myapp.example.com
# Database
postgres_user: appuser
postgres_db: appdb
# Resource limits
nginx_memory: "128m"
app_memory: "512m"
db_memory: "1g"
redis_memory: "256m"
# group_vars/vault.yml (encrypted with Ansible Vault)
vault_postgres_pass: "StrongP@ssw0rd!"
vault_redis_pass: "RedisP@ssw0rd!"
vault_app_secret: "SuperSecretKey!"
Το Playbook
---
# docker-fullstack-networking.yml
# Multi-container stack με Networks & Volumes
- name: Docker Fullstack — Networks & Volumes
hosts: "{{ target | default('all_managed') }}"
become: true
gather_facts: true
vars:
# ── Stack config ───────────────────────────
stack_name: "{{ app_name }}_{{ app_env }}"
cert_dir: /etc/nginx/certs
config_dir: /opt/{{ app_name }}/config
backup_dir: /opt/backups/docker
# ── Images ────────────────────────────────
images:
nginx: "nginx:1.25-alpine"
app: "{{ app_name }}:{{ app_version }}"
postgres: "postgres:15-alpine"
redis: "redis:7.2-alpine"
# ════════════════════════════════════════════
# PHASE 1: PREREQUISITES
# ════════════════════════════════════════════
tasks:
- name: "════ PHASE 1: Prerequisites ════"
ansible.builtin.debug:
msg: "Έλεγχος και προετοιμασία περιβάλλοντος..."
# ── Directories ───────────────────────────
- name: "📁 Δημιουργία directories"
ansible.builtin.file:
path: "{{ item.path }}"
state: directory
owner: "{{ item.owner | default('root') }}"
group: "{{ item.group | default('root') }}"
mode: "{{ item.mode | default('0755') }}"
loop:
- { path: "{{ cert_dir }}", mode: '0700' }
- { path: "{{ config_dir }}", mode: '0755' }
- { path: "{{ backup_dir }}/{{ inventory_hostname }}" }
# ── Nginx config ──────────────────────────
- name: "⚙️ Nginx config"
ansible.builtin.template:
src: templates/nginx-proxy.conf.j2
dest: "{{ config_dir }}/nginx.conf"
mode: '0644'
# ── Pull images ───────────────────────────
- name: "📦 Pull Docker images"
community.docker.docker_image:
name: "{{ item.key }}"
tag: "{{ item.value | split(':') | last }}"
source: pull
loop: "{{ images | dict2items }}"
loop_control:
label: "{{ item.value }}"
async: 300
poll: 10
# ════════════════════════════════════════════
# PHASE 2: NETWORKS
# ════════════════════════════════════════════
- name: "════ PHASE 2: Networks ════"
ansible.builtin.debug:
msg: "Δημιουργία Docker networks..."
- name: "🌐 Frontend network"
community.docker.docker_network:
name: "{{ stack_name }}_frontend"
state: present
driver: bridge
ipam_config:
- subnet: "10.200.1.0/24"
gateway: "10.200.1.1"
labels:
stack: "{{ stack_name }}"
layer: frontend
managed-by: ansible
- name: "🌐 Backend network (internal)"
community.docker.docker_network:
name: "{{ stack_name }}_backend"
state: present
driver: bridge
internal: true # ← ΔΕΝ έχει internet access!
ipam_config:
- subnet: "10.200.2.0/24"
gateway: "10.200.2.1"
labels:
stack: "{{ stack_name }}"
layer: backend
managed-by: ansible
# ════════════════════════════════════════════
# PHASE 3: VOLUMES
# ════════════════════════════════════════════
- name: "════ PHASE 3: Volumes ════"
ansible.builtin.debug:
msg: "Δημιουργία Docker volumes..."
- name: "💾 Δημιουργία named volumes"
community.docker.docker_volume:
name: "{{ item.name }}"
state: present
labels:
stack: "{{ stack_name }}"
purpose: "{{ item.purpose }}"
backup: "{{ item.backup | string }}"
managed-by: ansible
loop:
- { name: "{{ stack_name }}_postgres", purpose: database, backup: true }
- { name: "{{ stack_name }}_redis", purpose: cache, backup: false }
- { name: "{{ stack_name }}_uploads", purpose: media, backup: true }
loop_control:
label: "{{ item.name }}"
# ════════════════════════════════════════════
# PHASE 4: CONTAINERS
# ════════════════════════════════════════════
- name: "════ PHASE 4: Containers ════"
ansible.builtin.debug:
msg: "Deployment containers..."
# ── PostgreSQL ────────────────────────────
- name: "🐘 PostgreSQL container"
community.docker.docker_container:
name: "{{ stack_name }}_postgres"
image: "{{ images.postgres }}"
state: started
restart_policy: unless-stopped
memory: "{{ db_memory }}"
networks:
- name: "{{ stack_name }}_backend" # ← backend only!
volumes:
- "{{ stack_name }}_postgres:/var/lib/postgresql/data:rw"
env:
POSTGRES_DB: "{{ postgres_db }}"
POSTGRES_USER: "{{ postgres_user }}"
POSTGRES_PASSWORD: "{{ vault_postgres_pass }}"
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
no_log: true
healthcheck:
test: ["CMD-SHELL", "pg_isready -U {{ postgres_user }}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
labels:
stack: "{{ stack_name }}"
service: postgres
managed-by: ansible
# ── Redis ─────────────────────────────────
- name: "📮 Redis container"
community.docker.docker_container:
name: "{{ stack_name }}_redis"
image: "{{ images.redis }}"
state: started
restart_policy: unless-stopped
memory: "{{ redis_memory }}"
networks:
- name: "{{ stack_name }}_backend" # ← backend only!
volumes:
- "{{ stack_name }}_redis:/data:rw"
command: >
redis-server
--requirepass {{ vault_redis_pass }}
--maxmemory {{ redis_memory }}
--maxmemory-policy allkeys-lru
--save 60 1000
no_log: true
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
labels:
stack: "{{ stack_name }}"
service: redis
managed-by: ansible
# ── Αναμονή για backends ──────────────────
- name: "⏳ Αναμονή για PostgreSQL"
community.docker.docker_container_info:
name: "{{ stack_name }}_postgres"
register: pg_info
until: >
pg_info.container.State.Health is defined and
pg_info.container.State.Health.Status == 'healthy'
retries: 12
delay: 5
# ── Application ───────────────────────────
- name: "🚀 Application container"
community.docker.docker_container:
name: "{{ stack_name }}_app"
image: "{{ images.app }}"
state: started
restart_policy: unless-stopped
memory: "{{ app_memory }}"
networks:
- name: "{{ stack_name }}_frontend" # ← frontend
- name: "{{ stack_name }}_backend" # ← backend (για db/redis)
volumes:
- "{{ stack_name }}_uploads:/app/uploads:rw"
- "{{ config_dir }}/app.env:/app/.env:ro"
env:
NODE_ENV: "{{ app_env }}"
DATABASE_URL: >
postgresql://{{ postgres_user }}:{{ vault_postgres_pass }}
@{{ stack_name }}_postgres:5432/{{ postgres_db }}
REDIS_URL: "redis://:{{ vault_redis_pass }}@{{ stack_name }}_redis:6379"
SECRET_KEY: "{{ vault_app_secret }}"
APP_VERSION: "{{ app_version }}"
no_log: true
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"]
interval: 15s
timeout: 10s
retries: 5
start_period: 30s
labels:
stack: "{{ stack_name }}"
service: app
version: "{{ app_version }}"
managed-by: ansible
# ── Nginx Reverse Proxy ───────────────────
- name: "🌐 Nginx container"
community.docker.docker_container:
name: "{{ stack_name }}_nginx"
image: "{{ images.nginx }}"
state: started
restart_policy: unless-stopped
memory: "{{ nginx_memory }}"
ports:
- "80:80"
- "443:443"
networks:
- name: "{{ stack_name }}_frontend" # ← frontend only!
volumes:
- "{{ config_dir }}/nginx.conf:/etc/nginx/nginx.conf:ro"
- "{{ cert_dir }}:/etc/nginx/certs:ro"
- "{{ stack_name }}_uploads:/app/uploads:ro" # ← static files
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost/ || exit 1"]
interval: 15s
timeout: 5s
retries: 3
labels:
stack: "{{ stack_name }}"
service: nginx
managed-by: ansible
# ════════════════════════════════════════════
# PHASE 5: VERIFICATION
# ════════════════════════════════════════════
- name: "════ PHASE 5: Verification ════"
ansible.builtin.debug:
msg: "Επαλήθευση stack..."
- name: "🔍 Έλεγχος όλων των containers"
community.docker.docker_container_info:
name: "{{ item }}"
register: stack_info
loop:
- "{{ stack_name }}_postgres"
- "{{ stack_name }}_redis"
- "{{ stack_name }}_app"
- "{{ stack_name }}_nginx"
loop_control:
label: "{{ item }}"
- name: "🔍 HTTP endpoint check"
ansible.builtin.uri:
url: "http://localhost/health"
status_code: 200
register: http_check
retries: 5
delay: 5
until: http_check.status == 200
- name: "📊 Stack Report"
ansible.builtin.debug:
msg:
- "╔══════════════════════════════════════════════╗"
- "║ ✅ DOCKER STACK DEPLOYED! ║"
- "╠══════════════════════════════════════════════╣"
- "║ Stack : {{ stack_name }}"
- "║ Version: {{ app_version }}"
- "╠══════════════════════════════════════════════╣"
- "║ Networks:"
- "║ frontend: {{ stack_name }}_frontend"
- "║ backend: {{ stack_name }}_backend (internal)"
- "╠══════════════════════════════════════════════╣"
- "║ Volumes:"
- "║ postgres: {{ stack_name }}_postgres"
- "║ redis: {{ stack_name }}_redis"
- "║ uploads: {{ stack_name }}_uploads"
- "╠══════════════════════════════════════════════╣"
- "║ Containers:"
- "{% for item in stack_info.results %}\
║ {{ item.item | replace(stack_name + '_', '') }}: {{ item.container.State.Status }}\
{% endfor %}"
- "╠══════════════════════════════════════════════╣"
- "║ HTTP: {{ '✅ OK' if not http_check.failed else '❌ FAILED' }}"
- "║ URL: http://{{ ansible_facts['default_ipv4']['address'] }}"
- "╚══════════════════════════════════════════════╝"
Εκτέλεση
# Deploy
ansible-playbook playbooks/docker-fullstack-networking.yml \
--limit nextcloud \
-e "new_version=2.5.0" \
--ask-vault-pass
# Dry run
ansible-playbook playbooks/docker-fullstack-networking.yml \
--limit nextcloud \
-e "new_version=2.5.0" \
--check --diff
# Verify stack
ansible nextcloud -m ansible.builtin.command \
-a "docker ps --filter label=stack=myapp_production" \
--become
# Network inspection
ansible nextcloud -m ansible.builtin.command \
-a "docker network inspect myapp_production_backend" \
--become
Σύνοψη D7.6
Τι χρησιμοποιήσαμε:
│
├── Networks:
│ ├── frontend_net → nginx + app
│ ├── backend_net → app + postgres + redis
│ └── internal: true → backend ΔΕΝ έχει internet
│
├── Volumes:
│ ├── Named: postgres, redis, uploads
│ └── Bind: config, certs (read-only)
│
├── DNS:
│ └── Containers "μιλάνε" με ονόματα:
│ DATABASE_URL → @postgres_container:5432
│ REDIS_URL → @redis_container:6379
│
├── Security:
│ ├── Secrets: no_log, vault
│ ├── Read-only volumes: :ro
│ ├── Internal networks: no internet for DBs
│ └── Minimal port exposure
│
└── Order:
directories → networks → volumes → containers