SaltStack on Your VPS Series
    Part 4 of 6

    LAMP Stack Deployment via Salt States

    Full Nginx + PHP-FPM + MariaDB deployment with role separation and Jinja-templated virtual host management.

    40 minutes

    Architecture Overview

    This deployment separates concerns into distinct roles:

    • Web server (Nginx + PHP-FPM) — handles HTTP requests and PHP execution
    • Database server (MariaDB) — handles data storage, ideally on a separate minion
    • Shared baseline (common) — applied to all servers regardless of role

    Directory Structure

    /srv/salt/
      top.sls
      common.sls
      lamp/
        init.sls          <- applies full LAMP to one server
        webserver.sls     <- Nginx + PHP-FPM only
        database.sls      <- MariaDB only
        php.sls           <- PHP-FPM and extensions
        vhost.sls         <- Virtual host management
        files/
          nginx.conf
          php-fpm.conf
          www.conf
          site.conf.jinja

    top.sls — Role Assignment

    /srv/salt/top.sls
    base:
      '*':
        - common
    
      'lamp-single':
        - lamp
    
      'web-*':
        - lamp.webserver
        - lamp.php
        - lamp.vhost
    
      'db-*':
        - lamp.database

    common.sls — Baseline

    /srv/salt/common.sls
    essential_packages:
      pkg.installed:
        - pkgs:
          - curl
          - wget
          - vim
          - git
          - unzip
          - htop
          - ufw
          - fail2ban
          - chrony
    
    chrony_service:
      service.running:
        - name: chrony
        - enable: True
        - require:
          - pkg: essential_packages
    
    set_timezone:
      timezone.system:
        - name: UTC

    Nginx Web Server

    /srv/salt/lamp/webserver.sls
    nginx_package:
      pkg.installed:
        - name: nginx
    
    nginx_service:
      service.running:
        - name: nginx
        - enable: True
        - watch:
          - file: nginx_main_config
        - require:
          - pkg: nginx_package
    
    nginx_main_config:
      file.managed:
        - name: /etc/nginx/nginx.conf
        - source: salt://lamp/files/nginx.conf
        - user: root
        - group: root
        - mode: '0644'
        - require:
          - pkg: nginx_package
    
    nginx_default_site:
      file.absent:
        - name: /etc/nginx/sites-enabled/default
    
    allow_http:
      cmd.run:
        - name: ufw allow 'Nginx Full'
        - unless: ufw status | grep -q 'Nginx Full.*ALLOW'

    PHP-FPM

    /srv/salt/lamp/php.sls
    php_packages:
      pkg.installed:
        - pkgs:
          - php8.1-fpm
          - php8.1-cli
          - php8.1-common
          - php8.1-mysql
          - php8.1-xml
          - php8.1-curl
          - php8.1-mbstring
          - php8.1-zip
          - php8.1-bcmath
          - php8.1-intl
          - php8.1-gd
    
    php_fpm_service:
      service.running:
        - name: php8.1-fpm
        - enable: True
        - watch:
          - file: php_fpm_pool_config
        - require:
          - pkg: php_packages
    
    php_fpm_pool_config:
      file.managed:
        - name: /etc/php/8.1/fpm/pool.d/www.conf
        - source: salt://lamp/files/www.conf
        - require:
          - pkg: php_packages

    MariaDB

    /srv/salt/lamp/database.sls
    mariadb_packages:
      pkg.installed:
        - pkgs:
          - mariadb-server
          - mariadb-client
          - python3-pymysql
    
    mariadb_service:
      service.running:
        - name: mariadb
        - enable: True
        - require:
          - pkg: mariadb_packages
    
    mariadb_config:
      file.managed:
        - name: /etc/mysql/mariadb.conf.d/50-server.cnf
        - source: salt://lamp/files/50-server.cnf
        - require:
          - pkg: mariadb_packages
        - watch_in:
          - service: mariadb_service

    Virtual Host Management with Jinja

    /srv/salt/lamp/vhost.sls
    {% set sites = pillar.get('nginx_sites', {}) %}
    
    {% for site_name, site in sites.items() %}
    {{ site_name }}_webroot:
      file.directory:
        - name: {{ site.get('root', '/var/www/' + site_name) }}
        - user: www-data
        - group: www-data
        - mode: '0755'
        - makedirs: True
    
    {{ site_name }}_vhost_config:
      file.managed:
        - name: /etc/nginx/sites-available/{{ site_name }}.conf
        - source: salt://lamp/files/site.conf.jinja
        - template: jinja
        - context:
            site_name: {{ site_name }}
            server_name: {{ site.get('server_name', site_name) }}
            root: {{ site.get('root', '/var/www/' + site_name) }}
            php_socket: /run/php/php8.1-fpm.sock
    
    {{ site_name }}_vhost_enabled:
      file.symlink:
        - name: /etc/nginx/sites-enabled/{{ site_name }}.conf
        - target: /etc/nginx/sites-available/{{ site_name }}.conf
        - watch_in:
          - service: nginx_service
    {% endfor %}

    Supporting Config Files

    site.conf.jinja (Nginx vhost template)
    server {
        listen 80;
        server_name {{ server_name }} www.{{ server_name }};
        root {{ root }};
        index index.php index.html;
    
        location / {
            try_files $uri $uri/ /index.php?$query_string;
        }
    
        location ~ \.php$ {
            fastcgi_pass unix:{{ php_socket }};
            fastcgi_index index.php;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        }
    
        location ~ /\.ht {
            deny all;
        }
    }

    Pillar Data

    /srv/pillar/lamp.sls
    mariadb:
      root_password: 'your-strong-root-password'
      databases:
        - name: myapp_db
          user: myapp_user
          password: 'app-db-password'
    
    nginx_sites:
      myapp:
        server_name: example.com
        root: /var/www/myapp/public

    Deploying the Stack

    # Single server — test first
    sudo salt 'lamp-single' state.apply lamp test=True
    sudo salt 'lamp-single' state.apply lamp
    
    # Separated roles
    sudo salt 'db-01' state.apply lamp.database
    sudo salt 'web-01' state.apply lamp.webserver,lamp.php,lamp.vhost
    
    # Or let top.sls handle targeting
    sudo salt -G 'role:database' state.highstate
    sudo salt -G 'role:webserver' state.highstate

    Verifying the Deployment

    sudo salt 'web-*' service.status nginx
    sudo salt 'web-*' service.status php8.1-fpm
    sudo salt 'db-*' service.status mariadb
    sudo salt 'web-01' cmd.run 'curl -s -o /dev/null -w "%{http_code}" http://localhost'

    What's Next

    Your LAMP stack is now fully managed by Salt. In Part 5, we add Docker — letting Salt manage container workloads, deploy Compose files, and manage running containers as state resources.