Ansible: Automating Server Setup Made Simple

Ansible: Automating Server Setup Made Simple

Kite Eugine

Kite Eugine • Nov 29, 2025

Imagine you've just signed up for a server hosting service, and now you need to install Node.js, set up Nginx, configure SSL certificates, and deploy your Next.js application. You SSH into your server and start typing commands one by one. Everything works perfectly.

But then your server crashes, or you need to set up a second server for staging, or your team grows and someone else needs to replicate your setup. Suddenly, you're either spending hours redoing everything manually or desperately trying to remember all the commands you ran three months ago.

This is where Ansible comes in—a tool that lets you write down all those setup steps once and run them automatically, as many times as you need, on as many servers as you want.

The Pain Point: Manual Server Configuration is a Nightmare

Let's be honest about what managing servers manually looks like:

The Time Sink: Setting up a single server with all the necessary software, configurations, and security measures can take hours. Need to do it for five servers? There goes your entire week.

The Memory Game: You set up a server perfectly six months ago. Now you need to do it again, but you can't quite remember if you installed that one package before or after updating the system, or what exact configuration you used for Nginx.

The Consistency Problem: When you manually configure multiple servers, tiny differences creep in. One server has a slightly different Nginx configuration, another is missing a security update, and suddenly you're troubleshooting issues that only happen in production.

The Documentation Nightmare: You try to document everything in a text file, but it's hard to keep updated, and reading through pages of commands is tedious and error-prone.

The Onboarding Challenge: A new team member joins, and you need to explain the entire server setup process. You either spend hours walking them through it or hand them a document and hope they don't miss any steps.

What Ansible Solves

Ansible is an automation tool that turns your server configuration into code. Instead of manually typing commands, you write "playbooks" (simple YAML files) that describe what you want your servers to look like, and Ansible makes it happen.

Infrastructure as Code: Your entire server setup becomes a file you can read, edit, version control with Git, and share with your team. It's living documentation that actually works.

Idempotency: Ansible is smart about what it does. If you run the same playbook twice, it won't duplicate work. It checks the current state and only makes changes if needed. This means you can safely run your playbooks over and over without breaking anything.

No Agent Required: Unlike some tools, Ansible doesn't require you to install special software on your servers. It connects via SSH, which you're already using, making it simple and secure.

Human-Readable: Ansible playbooks are written in YAML, which looks almost like English. Even non-technical team members can understand what your automation does.

Reusability: Write once, use everywhere. The same playbook can configure one server or a hundred servers identically.

Real-World Example: Deploying a Next.js App with Complete DevOps Setup

Let's walk through a real scenario: You've built a Next.js application, bought a domain from Namecheap, and have a Hostinger VPS. You want to deploy your app with Nginx as a reverse proxy, PM2 to keep it running, SSL certificates for security, and GitHub Actions for continuous deployment.

Step 1: Setting Up Your Ansible Environment

First, install Ansible on your local machine (not the server):

# On Mac
brew install ansible

# On Ubuntu/Debian
sudo apt update
sudo apt install ansible

# On Windows (use WSL)
sudo apt update && sudo apt install ansible

Create a project structure:

my-deployment/
├── ansible/
│   ├── inventory.ini
│   ├── playbook.yml
│   └── group_vars/
│       └── all.yml
└── .github/
    └── workflows/
        └── deploy.yml

Step 2: Create Your Inventory File

The inventory tells Ansible which servers to manage. Create ansible/inventory.ini:

[webservers]
production ansible_host=your-server-ip ansible_user=root

[webservers:vars]
ansible_python_interpreter=/usr/bin/python3

Replace your-server-ip with your actual Hostinger server IP address.

Step 3: Store Your Variables

Create ansible/group_vars/all.yml to store configuration:

# Application settings
app_name: my-nextjs-app
app_domain: yourdomain.com
app_port: 3000
deploy_user: deployer
app_directory: /var/www/{{ app_name }}

# GitHub repository
github_repo: https://github.com/yourusername/your-repo.git
github_branch: main

# Node.js version
nodejs_version: "20"

Step 4: Create Your Ansible Playbook

Now for the main playbook at ansible/playbook.yml:

---
- name: Deploy Next.js Application with Full DevOps Setup
  hosts: webservers
  become: yes
  
  tasks:
    # System Updates
    - name: Update apt cache
      apt:
        update_cache: yes
        cache_valid_time: 3600

    - name: Upgrade all packages
      apt:
        upgrade: dist

    # Install Basic Dependencies
    - name: Install required packages
      apt:
        name:
          - curl
          - git
          - nginx
          - certbot
          - python3-certbot-nginx
          - ufw
        state: present

    # Setup Node.js
    - name: Download Node.js setup script
      get_url:
        url: "https://deb.nodesource.com/setup_{{ nodejs_version }}.x"
        dest: /tmp/nodejs_setup.sh
        mode: '0755'

    - name: Run Node.js setup script
      shell: bash /tmp/nodejs_setup.sh
      args:
        creates: /etc/apt/sources.list.d/nodesource.list

    - name: Install Node.js
      apt:
        name: nodejs
        state: present
        update_cache: yes

    # Install PM2 globally
    - name: Install PM2
      npm:
        name: pm2
        global: yes
        state: present

    - name: Setup PM2 startup script
      shell: pm2 startup systemd -u {{ deploy_user }} --hp /home/{{ deploy_user }}
      changed_when: false

    # Create deployment user
    - name: Create deployment user
      user:
        name: "{{ deploy_user }}"
        shell: /bin/bash
        createhome: yes

    - name: Add deployment user to sudo group
      user:
        name: "{{ deploy_user }}"
        groups: sudo
        append: yes

    # Setup application directory
    - name: Create application directory
      file:
        path: "{{ app_directory }}"
        state: directory
        owner: "{{ deploy_user }}"
        group: "{{ deploy_user }}"
        mode: '0755'

    # Clone or update repository
    - name: Clone or update application repository
      git:
        repo: "{{ github_repo }}"
        dest: "{{ app_directory }}"
        version: "{{ github_branch }}"
        force: yes
      become_user: "{{ deploy_user }}"

    # Install dependencies and build
    - name: Install npm dependencies
      npm:
        path: "{{ app_directory }}"
        state: present
      become_user: "{{ deploy_user }}"

    - name: Build Next.js application
      shell: npm run build
      args:
        chdir: "{{ app_directory }}"
      become_user: "{{ deploy_user }}"

    # Configure PM2
    - name: Create PM2 ecosystem file
      copy:
        content: |
          module.exports = {
            apps: [{
              name: '{{ app_name }}',
              script: 'npm',
              args: 'start',
              cwd: '{{ app_directory }}',
              instances: 2,
              exec_mode: 'cluster',
              env: {
                NODE_ENV: 'production',
                PORT: {{ app_port }}
              }
            }]
          }
        dest: "{{ app_directory }}/ecosystem.config.js"
        owner: "{{ deploy_user }}"
        group: "{{ deploy_user }}"

    - name: Start application with PM2
      shell: pm2 start ecosystem.config.js
      args:
        chdir: "{{ app_directory }}"
      become_user: "{{ deploy_user }}"
      ignore_errors: yes

    - name: Restart application with PM2
      shell: pm2 restart {{ app_name }}
      become_user: "{{ deploy_user }}"

    - name: Save PM2 configuration
      shell: pm2 save
      become_user: "{{ deploy_user }}"

    # Configure Nginx
    - name: Remove default Nginx site
      file:
        path: /etc/nginx/sites-enabled/default
        state: absent

    - name: Create Nginx configuration
      copy:
        content: |
          server {
              listen 80;
              server_name {{ app_domain }} www.{{ app_domain }};

              location / {
                  proxy_pass http://localhost:{{ app_port }};
                  proxy_http_version 1.1;
                  proxy_set_header Upgrade $http_upgrade;
                  proxy_set_header Connection 'upgrade';
                  proxy_set_header Host $host;
                  proxy_cache_bypass $http_upgrade;
                  proxy_set_header X-Real-IP $remote_addr;
                  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                  proxy_set_header X-Forwarded-Proto $scheme;
              }
          }
        dest: /etc/nginx/sites-available/{{ app_name }}

    - name: Enable Nginx site
      file:
        src: /etc/nginx/sites-available/{{ app_name }}
        dest: /etc/nginx/sites-enabled/{{ app_name }}
        state: link

    - name: Test Nginx configuration
      command: nginx -t
      register: nginx_test
      changed_when: false

    - name: Reload Nginx
      service:
        name: nginx
        state: reloaded

    # Configure Firewall
    - name: Configure UFW defaults
      ufw:
        direction: incoming
        policy: deny

    - name: Allow SSH
      ufw:
        rule: allow
        port: '22'

    - name: Allow HTTP
      ufw:
        rule: allow
        port: '80'

    - name: Allow HTTPS
      ufw:
        rule: allow
        port: '443'

    - name: Enable UFW
      ufw:
        state: enabled

    # Setup SSL with Let's Encrypt
    - name: Obtain SSL certificate
      command: >
        certbot --nginx -d {{ app_domain }} -d www.{{ app_domain }}
        --non-interactive --agree-tos -m your-email@example.com --redirect
      args:
        creates: /etc/letsencrypt/live/{{ app_domain }}/fullchain.pem

    - name: Setup SSL renewal cron job
      cron:
        name: "Renew SSL certificates"
        minute: "0"
        hour: "0,12"
        job: "certbot renew --quiet"

  handlers:
    - name: Reload Nginx
      service:
        name: nginx
        state: reloaded

Step 5: Running Your Playbook

Before running, make sure your domain's DNS A record points to your server's IP address (configure this in your Namecheap dashboard).

Test the connection:

ansible -i ansible/inventory.ini webservers -m ping

Run the playbook:

ansible-playbook -i ansible/inventory.ini ansible/playbook.yml

Ansible will now automatically:

  • Update your server
  • Install Node.js, Nginx, and all dependencies
  • Set up PM2 to manage your application
  • Clone your repository
  • Build your Next.js app
  • Configure Nginx as a reverse proxy
  • Set up SSL certificates
  • Configure the firewall
  • Start your application

The entire process takes about 5-10 minutes, and you can rerun it anytime to ensure your server matches the desired configuration.

Step 6: GitHub Actions for Continuous Deployment

Create .github/workflows/deploy.yml:

name: Deploy to Production

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Setup SSH
      uses: webfactory/ssh-agent@v0.8.0
      with:
        ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

    - name: Add server to known hosts
      run: |
        mkdir -p ~/.ssh
        ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts

    - name: Install Ansible
      run: |
        sudo apt update
        sudo apt install -y ansible

    - name: Run Ansible playbook
      run: |
        ansible-playbook -i ansible/inventory.ini ansible/playbook.yml
      env:
        ANSIBLE_HOST_KEY_CHECKING: False

Add these secrets to your GitHub repository (Settings → Secrets and variables → Actions):

  • SSH_PRIVATE_KEY: Your server's SSH private key
  • SERVER_IP: Your server's IP address

Now, every time you push to the main branch, GitHub Actions will automatically run your Ansible playbook and deploy the latest version of your application.

Why This Matters

With this setup:

Consistency: Every deployment is identical. No more "it works on my machine" problems.

Speed: Deploying to a new server takes minutes instead of hours. Need to create a staging environment? Just add another entry to your inventory file.

Documentation: Your playbook is self-documenting. New team members can read it to understand exactly how your infrastructure is configured.

Version Control: Your entire infrastructure setup is in Git. You can see who changed what and when, and roll back if something breaks.

Disaster Recovery: If your server dies, you can spin up a new one and have it configured identically in minutes.

Collaboration: Your entire team can contribute to infrastructure improvements through pull requests, just like application code.

Conclusion

Ansible transforms server management from a tedious, error-prone manual process into a simple, repeatable, and reliable automated workflow. You write your desired server state once in a playbook, and Ansible ensures your servers match that state, every time.

The initial setup requires some learning, but the time saved and errors prevented make it invaluable for any project that's more than a hobby. Whether you're managing one server or a hundred, Ansible gives you confidence that your infrastructure is exactly how you want it, documented, and reproducible.

Start small—maybe just automating your basic server setup—and gradually add more tasks as you get comfortable. Your future self (and your team) will thank you.

Comments (0)

No comments yet. Be the first to comment!

Related Posts