Skip to content

LAMP Stack with Ansible & Docker

This project modernizes the deployment of Legacy PHP applications, evolving from manual configuration to Immutable Infrastructure. Unlike basic container deployments, this solution addresses real-world challenges: compiling custom PHP extensions (mysqli), managing credentials securely outside the codebase, and standardizing the development environment.

  1. Immutability: On-demand custom Docker image builds to inject system dependencies during deployment.
  2. Security (12-Factor App): Full decoupling of credentials. Source code contains no secrets; they are injected as environment variables.
  3. Standardization: Implementation of a Makefile wrapper to abstract Python/Ansible complexity and ensure zero-friction onboarding.

The workflow implements a remote Build & Deploy pattern orchestrated by Ansible. The control node handles configuration and secrets, while the remote host builds and runs the containers.

graph LR
    %% --- Style Definitions ---
    classDef ansibleNode fill:#fff9c4,stroke:#fbc02d,stroke-width:2px,color:#000000;
    classDef dockerNode fill:#e0f7fa,stroke:#0288d1,stroke-width:2px,color:#000000;
    classDef secretNode fill:#ffcdd2,stroke:#c62828,stroke-width:2px,color:#000000;

    subgraph Control_Node ["Control Node (Dev)"]
        style Control_Node fill:#eceff1,stroke:#cfd8dc,color:#000000
        Make[("Makefile Wrapper")]:::ansibleNode
        Ansible[("Ansible Playbook")]:::ansibleNode
        Secrets[("secrets.yml<br/>(GitIgnored)")]:::secretNode
        
        Make --> Ansible
        Secrets -.-> Ansible
    end

    subgraph Remote_Host ["App Server (Ubuntu 24.04)"]
        style Remote_Host fill:#0288d1,stroke:#cfd8dc,stroke-width:2px,color:#000000
        
        subgraph Docker_Runtime ["Docker Engine"]
            style Docker_Runtime fill:#eceff1,stroke:#455a64,color:#000000
            
            Build[("Docker Build<br/>(PHP + Mysqli)")]:::dockerNode
            App[("Container Web<br/>Apache")]:::dockerNode
            DB[("Container DB<br/>MariaDB")]:::dockerNode
            
            Ansible -- "1. Upload Dockerfile" --> Build
            Build -- "2. Image Created" --> App
            Ansible -- "3. Inject ENV Vars" --> App
            App -- "Internal DNS" --> DB
        end
    end
    
    linkStyle default stroke:#333,stroke-width:2px,fill:none;

The solution uses a professional structure separating configuration, secrets, and deployment logic.

  1. Execution Wrapper (Makefile)

    To avoid versioning and virtual environment issues, we encapsulate commands in a Makefile. This ensures every developer uses the exact same tooling.

    Makefile
    setup:
    python3 -m venv .venv
    .venv/bin/pip install ansible-core docker
    .venv/bin/ansible-galaxy install -r requirements.yml
    deploy:
    .venv/bin/ansible-playbook playbook/deploy-lamp.yml
  2. Secret Management (Override Pattern)

    Ansible loads public variables (all.yml) first, then overwrites them with a local secret file that is excluded from Git (.gitignore).

    playbook/deploy-lamp.yml
    - hosts: app_servers
    vars_files:
    - "../group_vars/all.yml" # Public Structure (Placeholders)
    - "../group_vars/secrets.yml" # Local Secrets (Real Passwords)
  3. Playbook Logic (Build & Run)

    The playbook not only pulls images but uploads a Dockerfile and compiles a custom image to support mysqli.

    playbook/deploy-lamp.yml
    tasks:
    - name: Build Custom PHP Image
    community.docker.docker_image:
    name: "{{ custom_image_name }}"
    tag: latest
    build:
    path: "{{ remote_project_path }}" # Remote path to Dockerfile
    source: build
    force_source: true
    - name: Deploy Web Container
    community.docker.docker_container:
    name: "{{ project_name }}-web"
    image: "{{ custom_image_name }}:latest" # Use the freshly built image
    env:
    # Secure injection of environment variables
    DB_HOST: "{{ project_name }}-db"
    DB_USER: "{{ mysql_user }}"
    DB_PASSWORD: "{{ mysql_pass }}"
  4. Agnostic Code (PHP)

    The source code (src/index.php) contains no credentials. It reads the environment configuration, complying with The Twelve-Factor App principles.

    src/index.php
    <?php
    // Secure reading from Environment Variables
    $servername = getenv('DB_HOST');
    $username = getenv('DB_USER');
    $password = getenv('DB_PASSWORD');
    $conn = new mysqli($servername, $username, $password, $dbname);
    if ($conn->connect_error) {
    die("❌ Connection failed: " . $conn->connect_error);
    }
    echo "✅ Successfully connected to MariaDB.";
    ?>
  5. Standardized Execution

    Thanks to the Makefile, deployment is reduced to two commands, regardless of the underlying complexity.

    Ventana de terminal
    # Initialization (First time only)
    make setup
    # Idempotent Deploy
    make deploy
FeatureTraditional Bash ScriptAnsible + Docker Pro
CredentialsHardcoded in files (.php)Injected in Memory (ENV)
DependenciesManual installation (apt-get)Automatic Docker Build
Portability”Works on my machine”Standardized via Makefile

This project serves as the baseline for cloud migration. We have documented this process in the Cloud Operations section:

  1. IaaS Migration: Replicate this architecture using Azure Virtual Machines.

  2. Azure Container Registry (ACR): Move the build process to the cloud.

  3. Azure Key Vault: Centralized secret management.