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 ansibleCreate a project structure:
my-deployment/
├── ansible/
│ ├── inventory.ini
│ ├── playbook.yml
│ └── group_vars/
│ └── all.yml
└── .github/
└── workflows/
└── deploy.ymlStep 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/python3Replace 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: reloadedStep 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 pingRun the playbook:
ansible-playbook -i ansible/inventory.ini ansible/playbook.ymlAnsible 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: FalseAdd these secrets to your GitHub repository (Settings → Secrets and variables → Actions):
SSH_PRIVATE_KEY: Your server's SSH private keySERVER_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.