Publicando uma aplicação NodeJs na Digital Ocean

Publicando uma aplicação NodeJs na Digital Ocean

Enfim, depois de todo esforço para desenvolver aquela aplicação, chegou a hora de subir ela em produção. Escolher onde e como disponibilizar essa solução para seus clientes em potencial pode ser determinante para o sucesso do projeto. Com isso em mente, temos ótimas soluções no mercado, entre as principais estão: Amazon AWS, Google Cloud, Microsoft Azure, e a que será foco desse artigo, Digital Ocean. Cada um destes provedores de serviço tem características bem peculiares que podem ou não aderir ao objetivo do projeto, não estando no escopo do artigo discorrer sobre as características de cada uma, porém é importante destacar que a Digital Ocean atende bem a muitos cenários, em especial aqueles que não possuem um grande fluxo de caixa inicial e precisam ter mais controle sobre o custo total de infraestrutura com um valor mais previsível, isso acontece pois lá é possível iniciar pequenas máquinas virtuais, denominadas “droplets” com um custo mensal a partir de US $ 5.00 por mês, na data que escrevo o artigo, com 1GB de memória, 1vCPU, 25 GB de SSD e 1TB de transferência, um valor bem razoável para o que entrega.

Como exemplo, a aplicação que iremos utilizar no artigo é uma api muito simplificada disponível em https://github.com/meneguite/node-simple-api, que nos possibilita descrever com mais facilidade uma forma de levar uma aplicação a produção, usando para isso além do node e npm, o nginx como proxy reverso e o pm2 como supervisor de instancia da aplicação.

Configuração do Droplet

Para iniciarmos é necessário uma conta na Digital Ocean, caso ainda não tenha uma, crie a por esse link https://m.do.co/c/2e1c3d77e32b, o cadastro é muito simplificado, e seguindo o link irá receber US $100 de bônus, o que possibilitara testar completamente a solução proposta por este artigo.

Com a conta criada vamos criar nosso primeiro droplet:

Selecionaremos o plano que melhor se enquadra na nossa necessidade:

É possível incluir novos blocos de storage ao droplet, extendendo a capacidade de armazenamento base do plano, porém para esse exemplo não será necessário.

Selecionar a região do datacenter:

Selecionar o método de autenticação:

Para a autenticação recomendo fortemente que crie uma chave ssh, se já não possuir uma, isso será um bom incremento de segurança em sua instancia.

É possível também, quando necessário, anexar ao droplet um agendamento automático de backup.

Tudo correndo bem, visualizará uma tela semelhante a demonstrada abaixo, com nome da instancia e o ip externo atribuído a mesma:

Para acessar nossa instancia podemos utilizar o comando:

1
2
3
ssh [email protected] 

# Onde 161.35.10.18 é o ip externo disponibilizado pelo Digital Ocean

Instalando e Configurando as dependências para o projeto

Configurando um novo usuário comum

Como pode-se perceber ao acessar nosso novo droplet, o usuário base criado é o root, que possui privilégios muito elevados, sendo fortemente desencorajado a rodar uma aplicação com esse usuário, assim nossa primeira configuração será criar um novo usuário com privilégios menores para rodar nossa aplicação.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
adduser web

# Output
Adding user `web' ...
Adding new group `web' (1000) ...
Adding new user `web' (1000) with group `web' ...
Creating home directory `/home/web' ...
Copying files from `/etc/skel' ...
New password:
Retype new password:
passwd: password updated successfully
Changing the user information for web
Enter the new value, or press ENTER for the default
Full Name []: Web
Room Number []:
Work Phone []:
Home Phone []:
Other []:
Is the information correct? [Y/n] y

A nova conta criada agora é um usuário com privilégios básicos, porém em alguns momentos precisaremos fazer algumas tarefas administrativas, caso não queira fazer a troca de usuário sempre que necessário executar essas tarefas, pode-se incluir esse novo usuário no grupo “sudo”, permitindo que o usuário comum tenha acesso a comandos administrativos mediante ao uso do prefixo “sudo”, para isso execute o seguinte comando:

1
usermod -aG sudo web

Liberando acesso via ssh para o novo usuário

Caso esteja usando uma chave ssh, como sugerido acima, para acesso ao servidor, será necessário adicionar uma chave autorizada para o usuário também, permitindo que o mesmo consiga logar no sistema. Em sistemas críticos é extremamente importante não usar a mesma chave para ambos os acessos, porém para este exemplo iremos copiar a mesma autorização do usuário root para simplificar o processo.

1
rsync --archive --chown=web:web /root/.ssh /home/web

Com esse comando será copiado os arquivos necessários para acesso, mantendo as permissões adequadas. Agora podemos nos desconectar via root acessando novamente o servidor usando nosso novo usuário com o comando:

1
ssh [email protected]

Ativando e configurando algumas regras básicas de acesso no Firewall

O linux possui muitas ferramentas interessantes e poderosas para manter a integridade do servidor restringindo o acesso ao mínimo necessário, para esse exemplo iremos utilizar um firewall que já vem incluso no ubuntu e que a principio atende bem nossa demanda, visto que simplifica muito a criação e administração das regras de acesso ao servidor usando perfis de aplicação registradas na instalação de alguns softwares. Para visualizar os perfis disponíveis no sistema:

1
2
3
4
5
sudo ufw app list

# Output
Available applications:
OpenSSH

Antes de ativar o firewall, muita atenção para garantir que as conexões via SSH estão habilitadas, para isso execute o seguinte comando:

1
2
3
4
5
sudo ufw allow OpenSSH

# Output
Rules updated
Rules updated (v6)

Depois disso podemos ativar nosso firewall usando seguinte comando:

1
2
3
4
5
sudo ufw enable

# Output
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup

Para visualizar as regras ativas no sistema execute o comando:

1
2
3
4
5
6
7
8
9
sudo ufw status

# Output
Status: active

To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
OpenSSH (v6) ALLOW Anywhere (v6)

Instalando o NodeJs

Para instalarmos o node iremos usar os repositórios disponibilizados via PPA com os seguintes comandos:

1
2
cd ~
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -

Após a conclusão dos comandos acima, estamos aptos agora a proceder com a instalação do node. Repare porém, que no segundo comando eu defino a versão 14, a LTS no momento que escrevo este artigo, porém provavelmente só ajustar a versão mais atual será o suficiente para fazer a instalação da versão correta que precisa para rodar sua aplicação. Para instalar agora o nodejs basta executar o seguinte comando:

1
sudo apt install nodejs build-essential

A instalação do pacote “build-essential” é opcional neste exemplo, porém muito provavelmente vai precisar dele no dia a dia no servidor com aplicações node.

Para verificar a instalação podemos executar o seguinte comando:

1
2
3
4
node -v

# Output
v14.17.2
1
2
3
4
npm -v

# Output
6.14.13

Instalando e configurando o PM2

O PM2 é um gerenciador de processos para aplicativos node.js que nos possibilitará gerenciar, escalonar e manter o aplicativos sempre disponível. A instalação do PM2 é muito simples e feita diretamente usando o NPM com o comando abaixo:

1
sudo npm install [email protected] -g

Para verificar se a instalação foi feita com sucesso use o comando:

1
2
3
4
pm2 --version

# Output
5.1.0

Agora chegou o momento de obtermos nossa aplicação para seguirmos com a configuração, para simplificar esse artigo removendo complexidades de integrar esse fluxo em um servidor de CI/CD por exemplo, vou apenas simular que já tenho os arquivos no servidor, baixando os mesmos do repositório via GIT com a seguinte sequência de comandos:

1
2
3
cd ~
git clone -b master https://github.com/meneguite/node-simple-api.git node-simple-api
cd node-simple-api

Para visualizar os arquivos disponibilizados:

1
2
3
4
5
6
7
8
9
10
ls -la

# Output
total 24
drwxrwxr-x 3 web web 4096 Jul 12 22:01 .
drwxr-xr-x 8 web web 4096 Jul 12 22:01 ..
-rw-rw-r-- 1 web web 291 Jul 12 22:01 ecosystem.config.js
drwxrwxr-x 8 web web 4096 Jul 12 22:01 .git
-rw-rw-r-- 1 web web 1627 Jul 12 22:01 index.js
-rw-rw-r-- 1 web web 594 Jul 12 22:01 README.md

Ps: No droplet criado com ubuntu 20 já temos o git instalado por padrão, porém caso ainda não o tenha, basta executar o seguinte comando “sudo apt install -y git” para instalar.

Com os arquivos em mãos podemos dar inicio a configuração da nossa aplicação no PM2, essa configuração pode ser feita parametrizando o pm2 via linha de comando, porém para simplificar disponibilizei no repositório um arquivo “ecosystem.config.js” que já possui todos os parâmetros necessários para configurar a aplicação. O formato do arquivo ecosystem é o seguinte:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
apps: [
{
name: 'node-simple-api',
script: './index.js',
instances: 2,
exec_mode: 'cluster',
merge_logs: true,
env_production: {
DEBUG: false,
NODE_ENV: 'production',
APP_PORT: 3000,
},
},
],
};

Para iniciarmos nossa aplicação usando esse nosso arquivo e garantir que o mesmo seja iniciado automaticamente sempre que o PM2 iniciar devemos executar o seguinte comando:

1
2
3
4
5
6
7
8
9
10
11
pm2 start ecosystem.config.js --env production && pm2 save

# Output
[PM2] Applying action restartProcessId on app [node-simple-api](ids: [ 0, 1 ])
[PM2] [node-simple-api](0) ✓
[PM2] [node-simple-api](1) ✓
┌─────┬────────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├─────┼────────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ node-simple-api │ default │ N/A │ cluster │ 14177 │ 0s │ 1 │ online │ 0% │ 37.8mb │ web │ disabled │
│ 1 │ node-simple-api │ default │ N/A │ cluster │ 14189 │ 0s │ 1 │ online │ 0% │ 30.8mb │ web │ disabled │

Assim já temos nossa aplicação rodando na porta 3000 localmente, como podemos verificar abaixo:

1
2
3
4
curl http://127.0.0.1:3000/users

# Output
[{"id":1,"name":"Nanna Pedersen","email":"[email protected]"},{"id":2,"name":"Sarah Oliver","email":"[email protected]"},{"id":3,"name":"Hector Guerrero","email":"[email protected]"},{"id":4,"name":"Noah Poulsen","email":"[email protected]"}]

O PM2 é muito flexível e poderoso, nesse momento já temos disponível nossa aplicação e podemos escalar ela sem nenhuma dificuldade, por exemplo, para subir para 4 o número de instancias disponíveis, podemos executar o comando:

1
2
3
4
5
6
7
8
9
10
11
12
13
pm2 scale node-simple-api 4

# Output
[PM2] Scaling up application
[PM2] Scaling up application
┌─────┬────────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├─────┼────────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ node-simple-api │ default │ N/A │ cluster │ 14177 │ 4m │ 1 │ online │ 0% │ 39.8mb │ web │ disabled │
│ 1 │ node-simple-api │ default │ N/A │ cluster │ 14189 │ 4m │ 1 │ online │ 0% │ 38.6mb │ web │ disabled │
│ 2 │ node-simple-api │ default │ N/A │ cluster │ 14238 │ 0s │ 0 │ online │ 0% │ 35.4mb │ web │ disabled │
│ 3 │ node-simple-api │ default │ N/A │ cluster │ 14245 │ 0s │ 0 │ online │ 0% │ 30.3mb │ web │ disabled │
└─────┴────────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

Mais informações e exemplos de uso do PM2 podem ser visualizados na documentação oficial disponível em https://pm2.keymetrics.io/docs/usage/quick-start/

Para configurarmos o PM2 para iniciar junto com o sistema operacional é bem simples, e o próprio PM2 já nos facilita com o comando “pm2 startup”

1
2
3
4
5
6
pm2 startup

# Output
[PM2] Init System found: systemd
[PM2] To setup the Startup Script, copy/paste the following command:
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u web --hp /home/web
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u web --hp /home/web

# Output
-------------

__/\\\\\\\\\\\\\____/\\\\____________/\\\\____/\\\\\\\\\_____
_\/\\\/////////\\\_\/\\\\\\________/\\\\\\__/\\\///////\\\___
_\/\\\_______\/\\\_\/\\\//\\\____/\\\//\\\_\///______\//\\\__
_\/\\\\\\\\\\\\\/__\/\\\\///\\\/\\\/_\/\\\___________/\\\/___
_\/\\\/////////____\/\\\__\///\\\/___\/\\\________/\\\//_____
_\/\\\_____________\/\\\____\///_____\/\\\_____/\\\//________
_\/\\\_____________\/\\\_____________\/\\\___/\\\/___________
_\/\\\_____________\/\\\_____________\/\\\__/\\\\\\\\\\\\\\\_
_\///______________\///______________\///__\///////////////__


Runtime Edition

PM2 is a Production Process Manager for Node.js applications
with a built-in Load Balancer.

Start and Daemonize any application:
$ pm2 start app.js

Load Balance 4 instances of api.js:
$ pm2 start api.js -i 4

Monitor in production:
$ pm2 monitor

Make pm2 auto-boot at server restart:
$ pm2 startup

To go further checkout:
http://pm2.io/


-------------

[PM2] Init System found: systemd
Platform systemd
Template
[Unit]
Description=PM2 process manager
Documentation=https://pm2.keymetrics.io/
After=network.target

[Service]
Type=forking
User=web
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/usr/bin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
Environment=PM2_HOME=/home/web/.pm2
PIDFile=/home/web/.pm2/pm2.pid
Restart=on-failure

ExecStart=/usr/lib/node_modules/pm2/bin/pm2 resurrect
ExecReload=/usr/lib/node_modules/pm2/bin/pm2 reload all
ExecStop=/usr/lib/node_modules/pm2/bin/pm2 kill

[Install]
WantedBy=multi-user.target

Target path
/etc/systemd/system/pm2-web.service
Command list
[ 'systemctl enable pm2-web' ]
[PM2] Writing init configuration in /etc/systemd/system/pm2-web.service
[PM2] Making script booting at startup...
[PM2] [-] Executing: systemctl enable pm2-web...
Created symlink /etc/systemd/system/multi-user.target.wants/pm2-web.service → /etc/systemd/system/pm2-web.service.
[PM2] [v] Command successfully executed.
+---------------------------------------+
[PM2] Freeze a process list on reboot via:
$ pm2 save

[PM2] Remove init script via:
$ pm2 unstartup systemd

1
2
3
4
5
pm2 save

# Output
[PM2] Saving current process list...
[PM2] Successfully saved in /home/web/.pm2/dump.pm2

Instalando e configurando o nginx como proxy reverso

Para instalarmos o nginx via PPA devemos executar os seguintes comandos:

1
2
sudo add-apt-repository ppa:nginx/stable -y
sudo apt install -y nginx

Podemos verificar se a instalação foi finalizada com sucesso com o seguinte comando:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sudo service nginx status

# Output
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
Active: active (running) since Tue 2021-07-13 13:56:54 UTC; 18min ago
Docs: man:nginx(8)
Main PID: 749 (nginx)
Tasks: 2 (limit: 1136)
Memory: 11.0M
CGroup: /system.slice/nginx.service
├─749 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
└─752 nginx: worker process

Jul 13 13:56:53 node-simple-api systemd[1]: Starting A high performance web server and a reverse proxy server...
Jul 13 13:56:54 node-simple-api systemd[1]: Started A high performance web server and a reverse proxy server.

Tendo isso em mãos vamos iniciar a configuração de um endereço DNS de entrada, para o teste irei usar o endereço “simple-api.meneguite.com”, para isso iremos criar o arquivo “/etc/nginx/sites-available/simple-api.meneguite.com.conf” com o seguinte conteúdo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
server {
listen 80 default_server;
listen [::]:80 default_server;

server_tokens off;

server_name simple-api.meneguite.com;

# Aditional Security Headers
# ref: https://developer.mozilla.org/en-US/docs/Security/HTTP_Strict_Transport_Security
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";

# ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
add_header X-Frame-Options DENY always;

# ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
add_header X-Content-Type-Options nosniff always;

# ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection
add_header X-Xss-Protection "1; mode=block" always;

location / {
proxy_http_version 1.1;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarde $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_cache_bypass $http_upgrade;
proxy_buffering off;
proxy_pass "http://127.0.0.1:3000";
}
}

Remover o arquivo default de configuração do nginx:

1
sudo rm /etc/nginx/sites-enabled/default

Criar um link simbólico para habilitar o novo arquivo de configuração:

1
sudo ln -s /etc/nginx/sites-available/simple-api.meneguite.com.conf /etc/nginx/sites-enabled/simple-api.meneguite.com.conf

Para testarmos e verificarmos se os arquivos e configurações estão ok antes de reiniciar os serviços execute o seguinte comando:

1
2
3
4
sudo service nginx configtest

# Output
* Testing nginx configuration [ OK ]

Tudo certo e configurado podemos fazer o reload do nginx para que o mesmo leia as configurações novamente e ative corretamente os serviços:

1
sudo service nginx reload

Se tudo estiver corrido como esperado até aqui, ao executar o comando abaixo já poderemos ver nossa api funcionando já por trás do proxy reverso do nginx.

1
2
3
4
curl http://127.0.0.1/users

# Output
[{"id":1,"name":"Nanna Pedersen","email":"[email protected]"},{"id":2,"name":"Sarah Oliver","email":"[email protected]"},{"id":3,"name":"Hector Guerrero","email":"[email protected]"},{"id":4,"name":"Noah Poulsen","email":"[email protected]"}]

Tudo configurado e funcionando, porém se tentar acesso pelo browser receberá a seguinte mensagem:

Isso acontece devido a ainda não termos feito a liberação da porta 80 de entrada necessária para acesso ao nginx, para fazer essa liberação devemos proceder com os seguintes comandos:

Verificar os apps disponíveis:

1
2
3
4
5
6
7
8
sudo ufw app list

# Output
Available applications:
Nginx Full
Nginx HTTP
Nginx HTTPS
OpenSSH

No nosso exemplo não configuramos os certificados para usarmos o https, assim vamos por hora liberar acesso apenas a porta http do ngix com o seguinte comando:

1
2
3
4
5
sudo ufw allow 'Nginx HTTP'

# Output
Rule added
Rule added (v6)

E enfim temos nosso serviço instalado e configurado:

Nosso servidor está acessível externamente e com a aplicação clousterizada usando o PM2, porém não é tão legal acessar nosso serviço sem um https, muito menos pelo ip, para resolver isso podemos usar o serviço da cloudflare para nos prover um certificado https válido, este processo é bem simples e descrevo como fazer no artigo “Publicar um site com Github Pages e CloudFlare” acessível no endereço: https://meneguite.com/2018/11/10/publicar-um-site-com-github-pages-e-cloudflare

Tudo pronto agora! Para o escopo deste artigo chegamos ao final, mas claro, sabemos que muitas outras necessidades vem ao criar uma aplicação, com essa configuração básica já consegue iniciar e validar pontos iniciais com um custo muito baixo, porém durante a trajetória provavelmente vai sentir a necessidade de um banco de dados, relacional ou não, e pode lançar outros droplets para resolver esta demanda, ou pode optar por usar um serviço disponibilizado pela própria Digital Ocean o “Managed Databases”, com esse serviço terceiriza toda a complexidade inicial de configurar e manter um servidor de banco de dados, para mais informações acesse “https://www.digitalocean.com/products/managed-databases/“. Outra demanda que muito provavelmente precisará em breve é trabalhar na resiliência da aplicação, ainda que muito estável, os servidores ou o próprio droplet pode parar de funcionar em algum momento, ou mesmo sua aplicação pode crescer e somente um único servidor deixar de ser suficiente, assim uma boa estratégia será incluir no meio do caminho um servidor de load balancer, serviço provido pela própria digital ocean, e lançar duas ou mais instancias do droplet que configuramos neste artigo, assim mesmo que tenha uma demanda maior ou um dos droplets tenha algum problema, nosso serviço continua a funcionar normalmente.

Os códigos usados neste artigo estão disponíveis no repositório https://github.com/meneguite/node-simple-api.git, qualquer dúvida ou sugestão de melhora fique a vontade para deixar aqui abaixo nos comentários.

Comentários

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×