Ansible is an agentless configuration management tool that runs over SSH. Installing the same packages on 100 servers, distributing config files, starting services — all with a single command. It needs Python, but that's already installed on target servers.

Installation

Related guides: What is DNS, settings · Domain names & WHOIS lookup · Hosting types guide · Nginx configuration · Plesk panel guide

bash
# Ubuntu/Debian
sudo apt install -y ansible

# Or with pip
pipx install ansible
ansible --version

# Having an SSH key on target servers is all you need
ssh-copy-id root@web-1.example.com

Inventory

ini
# inventory.ini
[web]
web-1.example.com
web-2.example.com
web-3.example.com ansible_host=1.2.3.4

[db]
db-1.example.com
db-2.example.com

[production:children]
web
db

[web:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/deploy_key
yaml
# inventory.yml (alternative)
all:
  children:
    web:
      hosts:
        web-1.example.com:
        web-2.example.com:
      vars:
        ansible_user: deploy
    db:
      hosts:
        db-1.example.com:

Ad-Hoc Commands

bash
# Ping all servers
ansible all -i inventory.ini -m ping

# Run a command
ansible web -i inventory.ini -m shell -a 'uptime'

# Install a package
ansible web -i inventory.ini -m apt -a 'name=htop state=present' --become

# Copy a file
ansible web -i inventory.ini -m copy -a 'src=./nginx.conf dest=/etc/nginx/nginx.conf'

Playbook — Basic Example

yaml
# site.yml
- name: Harden web servers
  hosts: web
  become: true

  vars:
    ssh_port: 22022

  tasks:
    - name: Apt update
      apt:
        update_cache: yes
        upgrade: safe
        cache_valid_time: 3600

    - name: Required packages
      apt:
        name:
          - fail2ban
          - ufw
          - nginx
          - htop
        state: present

    - name: Change SSH port
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^#?Port '
        line: 'Port {{ ssh_port }}'
      notify: restart sshd

    - name: UFW rules
      ufw:
        rule: allow
        port: '{{ item }}'
      loop: ['{{ ssh_port }}', '80', '443']

    - name: UFW enable
      ufw:
        state: enabled
        policy: deny

  handlers:
    - name: restart sshd
      service: { name: ssh, state: restarted }
bash
# Run it
ansible-playbook -i inventory.ini site.yml

# Dry run
ansible-playbook -i inventory.ini site.yml --check --diff

# Single host only
ansible-playbook -i inventory.ini site.yml --limit web-1.example.com

# Resume from a specific task
ansible-playbook -i inventory.ini site.yml --start-at-task='SSH port'

Roles — Modular Structure

bash
# Scaffold a role
ansible-galaxy init roles/nginx

# Structure
roles/nginx/
├── tasks/main.yml        # main tasks
├── handlers/main.yml     # service restarts etc.
├── templates/            # jinja2 templates
├── files/                # static files
├── vars/main.yml         # role variables
└── defaults/main.yml     # overridable defaults
yaml
# roles/nginx/tasks/main.yml
- name: Install Nginx
  apt: { name: nginx, state: present }

- name: Config template
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    validate: 'nginx -t -c %s'
  notify: reload nginx

- name: Service enabled
  service:
    name: nginx
    state: started
    enabled: yes

# roles/nginx/handlers/main.yml
- name: reload nginx
  service: { name: nginx, state: reloaded }

# Main playbook
- hosts: web
  become: true
  roles:
    - nginx

Jinja2 Templates

nginx
# roles/nginx/templates/nginx.conf.j2
worker_processes {{ ansible_processor_vcpus }};
worker_connections {{ worker_connections | default(1024) }};

{% for server in groups['backend'] %}
upstream backend {
    server {{ hostvars[server].ansible_host }}:3000;
}
{% endfor %}

server {
    listen 443 ssl;
    server_name {{ server_name }};
    ssl_certificate /etc/letsencrypt/live/{{ server_name }}/fullchain.pem;
}

Ansible Vault

bash
# Encrypted secrets file
ansible-vault create secrets.yml
# editor opens, with content like:
#   db_password: supersecret
#   api_key: xyz

ansible-vault edit secrets.yml
ansible-vault encrypt existing.yml
ansible-vault decrypt encrypted.yml

# Use in a playbook
ansible-playbook site.yml --ask-vault-pass
ansible-playbook site.yml --vault-password-file=.vault_pass

Real World: Full Deploy Playbook

yaml
- hosts: web
  become: true
  vars_files:
    - secrets.yml

  tasks:
    - name: Create app user
      user:
        name: app
        home: /var/www/app
        shell: /bin/bash

    - name: Clone/update repo
      git:
        repo: 'git@github.com:user/app.git'
        dest: /var/www/app
        version: main
        accept_hostkey: yes
      become_user: app
      notify: restart app

    - name: NPM install
      npm:
        path: /var/www/app
        production: yes
        state: present
      become_user: app

    - name: Env file
      template:
        src: env.j2
        dest: /var/www/app/.env
        owner: app
        mode: '0600'

    - name: Run with PM2
      command: pm2 startOrReload ecosystem.config.js
      args: { chdir: /var/www/app }
      become_user: app

  handlers:
    - name: restart app
      command: pm2 restart ecosystem.config.js
      become_user: app

Modern Web Hosting and Server Infrastructure

A performant web hosting service rests on three infrastructure decisions: NVMe SSD disks (4-6× IOPS over SATA SSD), LiteSpeed Web Server or Nginx + LSCache (9× request capacity over Apache) and CloudLinux + Imunify360 isolation. The hosting provider's control panel (cPanel, Plesk, DirectAdmin), daily backup policy, data center location and support response time make a big difference too. Turkish locations give low latency to local visitors, while Hetzner Frankfurt or OVH Roubaix suit global traffic. As your site grows, transitioning from shared hosting to VPS to dedicated server scales CPU/RAM/disk to your needs.