geek.sonde.uk updated:
Ansible Apache Docker Debian

Apache via Docker con Ansible

Imparerai a usare Ansible per deployare il container ufficiale Apache (httpd) su deb1.lan, con configurazione personalizzata montata come volume — SSL, proxy, compressione gzip e security headers inclusi. Nessuna compilazione, avvio in pochi secondi.

~10 min read Linux / Debian LAN Difficulty: Beginner
01

Prerequisiti

Prima di iniziare assicurati di avere:

  • Control node: Ansible installato sulla tua macchina locale
  • Host remoto deb1.lan raggiungibile via SSH con utente sudo
  • Docker installato su deb1.lan (il playbook lo installa automaticamente se assente)
  • Chiave SSH già copiata sull'host (ssh-copy-id [email protected])
  • Certificati SSL già presenti su deb1.lan in /etc/ssl/certs/deb1.lan-cert.pem e /etc/ssl/certs/deb1.lan-key.pem
ℹ️
Nessuna compilazione

Con Docker il container ufficiale httpd viene scaricato da Docker Hub già pronto. L'intero deploy — dal pull al container attivo — richiede meno di un minuto (escluso il download iniziale dell'immagine).

Verifica la connettività Ansible prima di procedere:

bash
ansible webservers -m ping --ask-vault-pass
02

Struttura del progetto

Creiamo la directory di lavoro sul control node:

bash
mkdir -p ansible-apache/{inventory,templates,group_vars/webservers}
cd ansible-apache
tree ansible-apache/
ansible-apache/
├── inventory/
│   └── hosts.ini
├── templates/
│   └── httpd.conf.j2          # configurazione Apache con variabili Jinja2
├── group_vars/
│   └── webservers/
│       └── vault.yml          # variabili cifrate (sudo password)
├── ansible.cfg
└── apache.yml                 # playbook principale
03

Inventario e configurazione

ini inventory/hosts.ini
[webservers]
deb1.lan

[webservers:vars]
ansible_user=ansible
ansible_become=true
ansible_become_method=sudo
ini ansible.cfg
[defaults]
inventory         = inventory/hosts.ini
remote_user       = ansible
host_key_checking = False
stdout_callback   = ansible.builtin.default
result_format     = yaml
interpreter_python = /usr/bin/python3

[privilege_escalation]
become        = True
become_method = sudo

Crea il vault con la password sudo:

bash
ansible-vault create group_vars/webservers/vault.yml

Dentro al vault scrivi:

yaml group_vars/webservers/vault.yml
ansible_become_password: "la_tua_password_sudo"
04

Template httpd.conf

Il template Jinja2 genera la configurazione Apache dall'interno del container. Le variabili tra {{ }} vengono sostituite dalle vars del playbook a deploy time. La configurazione è minimale: carica solo i moduli necessari e attiva proxy, compressione gzip, SSL e security headers.

apache templates/httpd.conf.j2
# ============================================================
#  httpd.conf — generato da Ansible, non modificare manualmente
# ============================================================

ServerRoot      "/usr/local/apache2"
ServerName      {{ apache_server_name }}
ServerAdmin     {{ apache_server_admin }}
PidFile         logs/httpd.pid

Listen {{ apache_http_port }}
Listen {{ apache_https_port }}

Timeout              60
KeepAlive            On
MaxKeepAliveRequests 100
KeepAliveTimeout     5

# ── Utente/Gruppo ────────────────────────────────────────────
User  daemon
Group daemon

# ── Moduli ───────────────────────────────────────────────────
LoadModule authz_core_module    modules/mod_authz_core.so
LoadModule authn_core_module    modules/mod_authn_core.so
LoadModule log_config_module    modules/mod_log_config.so
LoadModule mime_module          modules/mod_mime.so
LoadModule dir_module           modules/mod_dir.so
LoadModule unixd_module         modules/mod_unixd.so
LoadModule alias_module         modules/mod_alias.so

# Proxy
LoadModule proxy_module              modules/mod_proxy.so
LoadModule proxy_http_module         modules/mod_proxy_http.so
LoadModule proxy_balancer_module     modules/mod_proxy_balancer.so
LoadModule slotmem_shm_module        modules/mod_slotmem_shm.so
LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so

# Compressione
LoadModule deflate_module       modules/mod_deflate.so
LoadModule filter_module        modules/mod_filter.so
LoadModule setenvif_module      modules/mod_setenvif.so

# SSL
LoadModule ssl_module           modules/mod_ssl.so
LoadModule socache_shmcb_module modules/mod_socache_shmcb.so

# Riscrittura e headers
LoadModule rewrite_module       modules/mod_rewrite.so
LoadModule headers_module       modules/mod_headers.so

# ── MIME types ───────────────────────────────────────────────
TypesConfig conf/mime.types
AddType application/x-compress  .Z
AddType application/x-gzip      .gz .tgz

# ── Document root ────────────────────────────────────────────
DocumentRoot "/usr/local/apache2/htdocs"

<Directory "/usr/local/apache2/htdocs">
    Options       FollowSymLinks
    AllowOverride All
    Require       all granted
    DirectoryIndex index.html
</Directory>

<Directory />
    Require all denied
</Directory>

# ── Compressione deflate ─────────────────────────────────────
<IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/html text/plain text/xml \
                                   text/css text/javascript \
                                   application/javascript application/json \
                                   application/xml application/xhtml+xml
    DeflateCompressionLevel 6
    SetEnvIfNoCase Request_URI \.(?:gif|jpg|jpeg|png|zip|gz|bz2|rar)$ no-gzip
</IfModule>

# ── Proxy ────────────────────────────────────────────────────
<IfModule mod_proxy.c>
    ProxyRequests     Off
    ProxyPreserveHost On
    ProxyErrorOverride Off
    ProxyTimeout      60
{% if apache_proxy_pass is defined %}
    ProxyPass        {{ apache_proxy_path }}  {{ apache_proxy_pass }}
    ProxyPassReverse {{ apache_proxy_path }}  {{ apache_proxy_pass }}
{% endif %}
</IfModule>

# ── Security headers ─────────────────────────────────────────
<IfModule mod_headers.c>
    Header always set X-Content-Type-Options  "nosniff"
    Header always set X-Frame-Options         "SAMEORIGIN"
    Header always set X-XSS-Protection        "1; mode=block"
    Header always set Referrer-Policy         "strict-origin-when-cross-origin"
    Header always unset Server
</IfModule>

ServerTokens   Prod
ServerSignature Off

# ── Log ──────────────────────────────────────────────────────
LogLevel  warn
ErrorLog  "logs/error.log"
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
CustomLog "logs/access.log" combined

# ── SSL globale ───────────────────────────────────────────────
<IfModule mod_ssl.c>
    SSLSessionCache        shmcb:logs/ssl_scache(512000)
    SSLSessionCacheTimeout 300
    SSLProtocol            all -SSLv3 -TLSv1 -TLSv1.1
    SSLCipherSuite         HIGH:!aNULL:!MD5:!3DES
    SSLHonorCipherOrder    On
</IfModule>

# ── VirtualHost HTTP — redirect a HTTPS ──────────────────────
<VirtualHost *:{{ apache_http_port }}>
    ServerName    {{ apache_server_name }}
    DocumentRoot  /usr/local/apache2/htdocs

    ErrorLog  logs/{{ apache_server_name }}-error.log
    CustomLog logs/{{ apache_server_name }}-access.log combined

    RewriteEngine On
    RewriteRule ^ https://%{HTTP_HOST}:{{ apache_https_port }}%{REQUEST_URI} [R=301,L]
</VirtualHost>

# ── VirtualHost HTTPS ─────────────────────────────────────────
<VirtualHost *:{{ apache_https_port }}>
    ServerName    {{ apache_server_name }}
    DocumentRoot  /usr/local/apache2/htdocs

    ErrorLog  logs/{{ apache_server_name }}-ssl-error.log
    CustomLog logs/{{ apache_server_name }}-ssl-access.log combined

    SSLEngine             On
    SSLCertificateFile    /etc/ssl/certs/{{ apache_server_name }}-cert.pem
    SSLCertificateKeyFile /etc/ssl/certs/{{ apache_server_name }}-key.pem

    <Directory "/usr/local/apache2/htdocs">
        Options       FollowSymLinks
        AllowOverride All
        Require       all granted
    </Directory>

{% if apache_proxy_pass is defined %}
    ProxyPass        {{ apache_proxy_path }}  {{ apache_proxy_pass }}
    ProxyPassReverse {{ apache_proxy_path }}  {{ apache_proxy_pass }}
{% endif %}
</VirtualHost>
ℹ️
User/Group nel container

L'immagine ufficiale httpd usa daemon/daemon come utente interno, non www-data. Il ServerRoot è /usr/local/apache2 — path fisso dell'immagine Docker.

05

Playbook apache.yml

Il playbook si occupa di tre cose: installare Docker sull'host se assente, distribuire la configurazione generata dal template, e avviare il container con i volumi e le porte corrette.

yaml apache.yml
---
- name: Deploy Apache via Docker su deb1.lan
  hosts: webservers
  become: true

  # ── Variabili ─────────────────────────────────────────────────
  vars:
    apache_server_name:  deb1.lan
    apache_server_admin: [email protected]
    apache_http_port:    8071
    apache_https_port:   7443
    apache_conf_dir:     /sw/apache/conf
    apache_htdocs_dir:   /sw/apache/htdocs
    apache_logs_dir:     /sw/apache/logs
    apache_ssl_dir:      /etc/ssl/certs
    apache_image:        httpd:latest
    apache_container_name: apache-sw
    # Proxy opzionale — decommenta per attivare:
    # apache_proxy_path: /api/
    # apache_proxy_pass: http://127.0.0.1:8000/

  # ── Tasks ─────────────────────────────────────────────────────
  tasks:

    # ── BLOCCO 1: Docker ──────────────────────────────────────────

    - name: Installa dipendenze Docker
      ansible.builtin.apt:
        name:
          - ca-certificates
          - curl
          - gnupg
        state: present
        update_cache: true

    - name: Aggiungi la GPG key di Docker
      ansible.builtin.apt_key:
        url:   https://download.docker.com/linux/debian/gpg
        state: present

    - name: Aggiungi il repository Docker
      ansible.builtin.apt_repository:
        repo:  "deb [arch=amd64] https://download.docker.com/linux/debian {{ ansible_distribution_release }} stable"
        state: present

    - name: Installa Docker Engine
      ansible.builtin.apt:
        name:
          - docker-ce
          - docker-ce-cli
          - containerd.io
        state: present

    - name: Assicura che Docker sia avviato e abilitato
      ansible.builtin.systemd:
        name:    docker
        state:   started
        enabled: true

    # ── BLOCCO 2: directory e configurazione ──────────────────────

    - name: Crea le directory su host
      ansible.builtin.file:
        path:  "{{ item }}"
        state: directory
        mode:  "0755"
      loop:
        - "{{ apache_conf_dir }}"
        - "{{ apache_htdocs_dir }}"
        - "{{ apache_logs_dir }}"

    - name: Distribuisce httpd.conf dal template Jinja2
      ansible.builtin.template:
        src:  templates/httpd.conf.j2
        dest: "{{ apache_conf_dir }}/httpd.conf"
        mode: "0644"
      notify: Riavvia container Apache

    - name: Crea la pagina index di benvenuto
      ansible.builtin.copy:
        content: |
          <!DOCTYPE html>
          <html><head><meta charset="utf-8">
          <title>{{ apache_server_name }}</title></head>
          <body><h1>Apache su {{ apache_server_name }}</h1>
          <p>Container: {{ apache_container_name }}</p></body></html>
        dest: "{{ apache_htdocs_dir }}/index.html"
        mode: "0644"

    # ── BLOCCO 3: container ───────────────────────────────────────

    - name: Pull dell'immagine httpd ufficiale
      community.docker.docker_image:
        name:   "{{ apache_image }}"
        source: pull

    - name: Avvia il container Apache
      community.docker.docker_container:
        name:          "{{ apache_container_name }}"
        image:         "{{ apache_image }}"
        state:         started
        restart_policy: unless-stopped
        ports:
          - "{{ apache_http_port }}:{{ apache_http_port }}"
          - "{{ apache_https_port }}:{{ apache_https_port }}"
        volumes:
          - "{{ apache_conf_dir }}/httpd.conf:/usr/local/apache2/conf/httpd.conf:ro"
          - "{{ apache_htdocs_dir }}:/usr/local/apache2/htdocs:ro"
          - "{{ apache_logs_dir }}:/usr/local/apache2/logs"
          - "{{ apache_ssl_dir }}:/etc/ssl/certs:ro"

  # ── Handlers ──────────────────────────────────────────────────
  handlers:
    - name: Riavvia container Apache
      community.docker.docker_container:
        name:  "{{ apache_container_name }}"
        state: started
        restart: true
⚠️
Collezione community.docker

Il playbook usa i moduli community.docker. Se non è installata, esegui sul control node: ansible-galaxy collection install community.docker

06

Esecuzione del playbook

Installa la collection Docker se non ancora presente, poi esegui il playbook:

bash
# Una tantum: installa la collection Docker
ansible-galaxy collection install community.docker

# Esegui il playbook
ansible-playbook apache.yml --ask-vault-pass

Output atteso:

output
PLAY RECAP *****************************************************
deb1.lan : ok=11  changed=4  unreachable=0  failed=0
ℹ️
Idempotenza

Rieseguire il playbook è sicuro: i task già applicati risultano ok. Se httpd.conf è cambiato, l'handler riavvia automaticamente il container per applicare la nuova configurazione.

07

Verifica

Controlla che il container sia in esecuzione:

bash
ansible deb1.lan --ask-vault-pass -m command -a "docker ps --filter name=apache-sw"

Verifica la configurazione Apache dall'interno del container:

bash
ansible deb1.lan --ask-vault-pass -m command \
  -a "docker exec apache-sw httpd -t"

Test HTTP e HTTPS dalla tua macchina locale:

bash
curl -I http://deb1.lan:8071/
curl -Ik https://deb1.lan:7443/

Segui i log in tempo reale:

bash
ansible deb1.lan --ask-vault-pass -m command \
  -a "docker logs -f apache-sw"
Deploy completato

Se curl -Ik https://deb1.lan:7443/ restituisce HTTP/1.1 200 OK e docker ps mostra il container Up, Apache è operativo. La configurazione è in /sw/apache/conf/, i log in /sw/apache/logs/ — entrambi sull'host, fuori dal container.