8.2.4 Automated Website Deployment on VPS with Ansible
Automating the deployment process allows you to accelerate and simplify application updates, especially when it comes to a production server. In this guide, we will look in detail at how to deploy a Next.js application on an Ubuntu server using Ansible with roles, templates, and variables.
Ansible is a configuration management and automation tool that doesn’t require installing agents on target servers. It is ideal for deploying Node.js applications, such as Next.js, in a Linux environment.
Requirements
- Virtual Server or Dedicated Server service;
- Ubuntu server with SSH access;
- Ansible installed on your local machine;
- SSH key for connecting to the server.
Generating an SSH Key
Before connecting Ansible to the server via SSH, you need to create a key pair if it doesn’t already exist. This will ensure secure and automated connection to the remote server.
Execute the command, replacing your_email@example.com with your email:
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
Then add the public key to the ~/.ssh/authorized_keys file on your server:
# replace user with your server username and IP with your server's IP
ssh-copy-id -i ~/.ssh/id_rsa.pub user@IP
After this, Ansible will be able to connect to the server without entering a password.
Local Ansible Installation
Ansible is installed on the developer’s or administrator’s local machine, not on the target server. It manages remote servers via SSH, so there’s no need to install Ansible on every machine you want to deploy an application to.
sudo apt update
sudo apt install software-properties-common
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install ansible
These commands need to be executed on your local machine, from where Ansible will be run.
Ansible Project Structure
nextjs-deploy/
├── ansible.cfg # Main Ansible configuration file
├── inventory.ini # Inventory with IP addresses and server connection data
├── playbook.yml # Main script describing the order of role execution
└── roles/ # Directory with roles, each performing a separate part of automation
├── common/ # Role performing basic server preparation
│ ├── handlers/ # Event handlers (e.g., service restarts)
│ └── tasks/ # Main tasks: package installation, firewall configuration, etc.
├── nodejs/ # Role for installing Node.js and PM2
│ └── tasks/ # Tasks for installing Node.js and supporting tools
└── nextjs/ # Role managing the deployment of the Next.js application itself
├── handlers/ # Handlers (e.g., PM2 restart)
└── tasks/ # Tasks: repository cloning, dependency installation, building, and configuration
Setting Up the Inventory
The inventory in Ansible is a file that describes server addresses and connection parameters. It’s necessary for specifying target machines where automated actions will be performed. Here we’ll describe the server’s IP address, user, and path to the private SSH key.
# You need to replace IP, user, id_rsa_path with your server's IP address, username, and path to the SSH key file.
[web_servers]
web1 ansible_host=SERVER_IP ansible_user=user ansible_ssh_private_key_file=id_rsa_path
[all:vars]
ansible_python_interpreter=/usr/bin/python3
Main Playbook
The Playbook is the main script in which Ansible describes what actions to perform on target servers. In this case, it manages the sequence of roles needed for full deployment of a Next.js application.
- name: Deploy NextJS application to Ubuntu server
hosts: web_servers
become: yes
roles:
- common
- nodejs
- nextjs
Ansible Roles
In Ansible, roles are used to structure automation code. Each role contains a specific task, such as installing Node.js, configuring nginx, or cloning an application. This helps make the playbook modular, easily readable, and reusable.
Common Role
This role is responsible for basic server setup: updating packages, installing necessary utilities, and configuring network security. It creates a reliable foundation for subsequent automation steps.
System updates, dependency installation, and firewall configuration:
# - update apt package cache
- name: Update apt cache
apt: update_cache: yes
# - install useful utilities such as git, curl, nginx, and other system dependencies
- name: Install packages
apt:
name:
- git
- curl
- build-essential
- nginx
- ufw
state: present
# - configure the UFW firewall, allowing access only for SSH and HTTP/HTTPS
- name: Enable UFW and allow Nginx/SSH
ufw:
rule: allow
name: "{{ item }}"
loop:
- OpenSSH
- "Nginx Full"
# - enable the firewall with a policy of denying all unauthorized connections by default
- name: Enable firewall
ufw:
state: enabled
policy: deny
NodeJS Role
Installing Node.js and PM2:
# - add the external NodeSource repository to install the current version of Node.js;
- name: Add NodeSource repository
shell: curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
# - install Node.js itself from the repository;
- name: Install Node.js
apt:
name: nodejs
state: present
# - install PM2 — a process manager needed to manage and restart the Next.js application in production.
- name: Install PM2
npm:
name: pm2
global: yes
NextJS Role
Download, install, build, launch:
# - clone the Next.js application repository from Git;
- name: Clone repo
git:
repo: "{{ nextjs_repo_url }}"
dest: "{{ nextjs_app_path }}"
version: "{{ nextjs_repo_branch }}"
notify: restart nextjs application
# - install all dependencies using npm;
- name: Install dependencies
npm:
path: "{{ nextjs_app_path }}"
state: present
# - build the production version of the application;
- name: Build NextJS
shell: cd {{ nextjs_app_path }} && npm run build
# - create a PM2 configuration file describing how to run the application;
- name: Create PM2 config
template:
src: pm2-config.json.j2
dest: "{{ nextjs_app_path }}/pm2-config.json"
# - configure nginx as a reverse proxy for accessing the application via domain;
- name: Configure Nginx
template:
src: nginx-site.conf.j2
dest: /etc/nginx/sites-available/{{ nextjs_app_name }}
# - activate the nginx configuration by adding a symbolic link to the `sites-enabled` directory.
- name: Enable Nginx site
file:
src: /etc/nginx/sites-available/{{ nextjs_app_name }}
dest: /etc/nginx/sites-enabled/{{ nextjs_app_name }}
state: link
Application Variables
nextjs_app_name: my-nextjs-app # Name of the Next.js application to be used in PM2 and nginx
nextjs_app_path: ~/www/my-nextjs-app # Path on the server where the application will be located
nextjs_app_port: 3000 # Port on which the application will run inside the container/on the server
nextjs_repo_url: https://github.com/youruser/repo.git # Link to the repository with the application code
nextjs_repo_branch: main # Git branch from which the application will be cloned
nextjs_domain_name: example.com # Domain through which the application will be accessible via Nginx
Templates
Templates in Ansible are used to generate configuration files with variables that are substituted during execution. This allows flexible adaptation of configurations for different environments and simplifies deployment. In this project, we use templates to configure PM2 and Nginx.
This template creates a configuration file for PM2 — a process manager that will manage the running of the Next.js application in production.
{
"apps": [{
"name": "{{ nextjs_app_name }}",
"script": "npm",
"args": "start",
"cwd": "{{ nextjs_app_path }}",
"instances": "max",
"exec_mode": "cluster",
"env": {
"NODE_ENV": "production",
"PORT": "{{ nextjs_app_port }}"
}
}]
}
The template describes the configuration of an Nginx virtual host that will act as a reverse proxy for the Next.js application.
server {
listen 80;
server_name {{ nextjs_domain_name }};
location / {
proxy_pass http://localhost:{{ nextjs_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;
}
}
Execute the following command to check the playbook and deploy the application.
ansible-playbook -i inventory.ini playbook.yml --check
Tip: Always check your playbooks with the –check flag before running them on production servers to avoid unexpected changes:
For additional information, refer to official sources: