I chose to use a VPS mainly because of the cost benefits it offers. With this option, I had the opportunity to learn and test in a controlled environment and avoid financial surprises at the end of the month. The flexibility to set up and test different scenarios allows me to replicate real-life situations, promoting learning and skill development in a practical and effective way, all within a predictable budget. This option is perfect for those who want to explore new opportunities without spending too much money.
1. Rent a VPS service to obtain dedicated resources.
There are several options, but in my case, I went with the Contabo service, which for just $5 per month, offers 4 CPU cores, 6 GB RAM, 400 GB SSD storage, 200 Mbit/s network speed, and 32 TB per Monthly traffic (unlimited access)
Using the IP of the contracted machine, use the following command on the terminal to access the server for the first time through the SSH protocol:
ssh root@
Use the command to find your machine’s IP to set DNS
ip addr
Set up DNS on the service where you purchased the domain. For detailed guidance on how to perform this configuration, see the provider’s official documentation.
2.Update package
sudo apt update && sudo apt upgrade -y
3. Create new user
Working directly with the root user may expose your server to unnecessary risks, best practice is to create a new user with limited permissions.
adduser newuser
4. Grant sudo permissions
After creating the user, we have to add it to the sudo group so that it can execute commands that require advanced privileges without using the root user directly
usermod -aG sudo newuser
Now we can switch to the new user to start setting up the system
su - newuser
5. Remove SSH password authentication
To improve the security of SSH connections, it is recommended to disable password authentication, which eliminates the risk of password-based brute force attacks.
Before disabling password authentication for SSH, we must create an SSH key pair to ensure that we can still access the server
ssh-keygen -t ed25519 -C "useremail@email.com"
## -t ed25519: Especifica o tipo da chave a ser gerada.
## -C "seuemail@exemplo.com: Adiciona um comentário à chave Durante a execução do comando, você deverá escolher o local onde deseja salvar a chave ou optar pelo local padrão: `~/.ssh/chave`
After creating the key, we must add the SSH key to the server
ssh-copy-id -i ~/.ssh/sshkey.pub newuser@serverip
- Indicates the path where your key is located locally -i ~/.ssh/sshkey.pub
- This command copies the SSH public key into the archive
~/.ssh/authorized_keys
On the remote server, allow access to the user using the created key
Now try to access the server using your SSH private key located in the same directory as the public key.
ssh -p 22 -i ~/.ssh/sshkey newuser@serverip
- -p specifies the SSH port defined on the server
- -i ~/.ssh/sshkey specifies the private key path
If access using SSH keys works, we can remove SSH password authentication.
First open the SSH configuration file
sudo nano /etc/ssh/sshd_config
Find and change the following lines in the file
## HARDENING SSH CONECTION
PermitRootLogin no ## BLOQUEIA O LOGIN DIRETO COM ROOT
PasswordAuthentication no ## DESATIVA CONEXÃO POR SENHA
UsePAM no ## DESATIVA O USO DE PAM NO SSH
After changing these lines, save and close the profile and restart the SSH service:
sudo systemctl reload ssh
To verify that the changes are valid, try connecting
root@serverip # a resposta deve ser essa root@serverip Permission denied (publickey).
Use SSH to connect to the new user.
6. Install docker and docker-compose
To configure the environment, follow the official documentation to install Docker and Docker Compose:
https://docs.docker.com/engine/install/ubuntu/
https://docs.docker.com/compose/install/linux/
After installing Docker, execute the image to verify that it is functioning properly.
docker run -p 80:80 -d nginxdemos/hello
sudo systemctl enable docker
sudo usermod -aG docker newuser
7. Configure firewall
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
To make Docker comply with UFW rules, edit the Docker configuration file
sudo nano /etc/docker/daemon.json
{
"iptables": false
}
Restart docker
sudo systemctl restart docker
8. Docker file configuration of the project
Create file Dockerfile Create a docker image in the root directory of the project
# Stage 1: Construção da aplicação usando Node.js
FROM node:22 AS build
# Define o diretório de trabalho dentro do container para organizar os arquivos
WORKDIR /app
# Copia os arquivos de configuração do Node.js (package.json e package-lock.json) para o container
COPY package*.json ./
# Instala as dependências do projeto
RUN npm install
# Copia o restante dos arquivos do projeto para o container
COPY . .
# Executa o comando para construir a aplicação (adapte este comando conforme o projeto)
RUN npm run build
# Stage 2: Configuração do servidor Nginx para servir a aplicação
FROM nginx:alpine
# Define o diretório de trabalho do Nginx onde os arquivos estáticos serão armazenados
WORKDIR /usr/share/nginx/html
# Copia os arquivos da build gerada no estágio anterior para o diretório do Nginx
COPY --from=build /app/dist/codebyfernandes/browser .
# Copia o arquivo de configuração personalizado do Nginx (opcional, se necessário)
COPY default.conf /etc/nginx/conf.d/default.conf
# Expõe a porta 80 para o servidor
EXPOSE 80
# Define o comando padrão para iniciar o Nginx
CMD ["nginx", "-g", "daemon off;"]
Create a default.conf file in the project root directory:
server {
listen 80; # Porta onde o servidor vai escutar
server_name localhost; # Nome do servidor (pode ser substituído pelo domínio)
root /usr/share/nginx/html; # Diretório onde estão os arquivos da aplicação
index index.html; # Arquivo principal da aplicação
# Rota principal para servir o Angular
location / {
try_files $uri $uri/ /index.html; # Redireciona todas as rotas para o index.html
}
# Configuração de cache para arquivos estáticos
location ~* \.(?:ico|css|js|gif|jpe?g|png|woff2?|eot|ttf|svg|webmanifest)$ {
expires 6M; # Define o tempo de expiração para 6 meses
access_log off; # Desativa logs para esses arquivos
add_header Cache-Control "public"; # Permite cache público
}
error_page 404 /index.html; # Redireciona erros 404 para o index.html
# Logs personalizados
error_log /var/log/nginx/angular-error.log; # Registro de erros
access_log /var/log/nginx/angular-access.log; # Registro de acessos
}
Now create the image locally and run it afterwards
docker build -t meu-app:1.0 .
docker run -p 80:80 -d meu-app:1.0
-
d
:Run the container in the background. -
p 80:80
: Map port 80 of the container to port 80 of the host.
If everything is configured correctly, you will be able to access your project from: http://localhost
Now push your image to Docker Hub using the following file:
Docker Hub: Build and push the first image
9. Docker compose configuration
Create the compose.yaml file in the root directory of the project. First, let us add the image of the web application
services:
# Define os serviços do projeto
webapp:
# Nome do serviço
image: fernandeeess/portfolio-app:prod
# Imagem do contêiner usada para o serviço (versão de produção)
Now let us configure the reverse proxy for your project using the following command transportationsimplifying management by automatically setting up SSL certificates and adjusting load balancers based on the services created.
services:
reverse-proxy:
# Define o serviço do reverse proxy
image: traefik:v3.1
# Especifica a imagem do Traefik que será usada
command:
# Ativa o provedor Docker
- "--providers.docker"
# Desativa a exposição automática de serviços no Traefik
- "--providers.docker.exposedbydefault=false"
# Configura a porta para HTTPS (websecure)
- "--entryPoints.websecure.address=:443"
# Configura o desafio TLS para emissão do certificado SSL
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
# Define o e-mail para registrar os certificados SSL
- "--certificatesresolvers.myresolver.acme.email=youremail"
# Define o local para armazenar os certificados SSL
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
# Configura a porta para HTTP (web)
- "--entrypoints.web.address=:80"
# Redireciona HTTP para HTTPS
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
ports:
# Mapeia as portas do host para o contêiner (HTTP e HTTPS)
- "80:80"
- "443:443"
volumes:
# Volume para armazenar certificados SSL
- letsencrypt:/letsencrypt
# Permite que o Traefik se comunique com o Docker para detectar serviços
- /var/run/docker.sock:/var/run/docker.sock
webapp:
# Define o serviço da aplicação web
image: fernandeeess/portfolio-app:prod
# Especifica a imagem do contêiner da aplicação
volumes:
letsencrypt:
# Volume para armazenar os dados do Let's Encrypt
Add as label to Web application service
webapp:
# Define o serviço da aplicação web
image: fernandeeess/portfolio-app:prod
# Especifica a imagem do contêiner que será usada para o serviço
labels:
# Habilita o Traefik para este serviço
- "traefik.enable=true"
# Define a regra de roteamento baseada no domínio
- "traefik.http.routers.webapp.rule=Host(`yourdomain.com`)"
# Define o entrypoint como HTTPS (websecure)
- "traefik.http.routers.webapp.entrypoints=websecure"
# Configura o resolver de certificado SSL a ser usado
- "traefik.http.routers.webapp.tls.certresolver=myresolver"
# Define a porta interna do contêiner que será usada pelo load balancer
- "traefik.http.services.webapp.loadbalancer.server.port=80"
deploy:
# Configuração de implantação do serviço
mode: replicated
# Define o número de réplicas (instâncias) do serviço
replicas: 3
Configure the Watchtower service to monitor tagged Docker images prod
. Whenever there is an update, Watchtower automatically pulls the changes and restarts the service.
# Continuous Delivery/Deployment
watchtower:
image: containrrr/watchtower # Imagem do Watchtower.
command:
- "--label-enable" # Monitora apenas serviços com labels específicas.
- "--interval" # Define o intervalo de verificação.
- "30" # Verifica atualizações a cada 30 segundos.
- "--rolling-restart" # Reinicia os serviços de forma gradual.
volumes:
- /var/run/docker.sock:/var/run/docker.sock # Permite ao Watchtower gerenciar containers.
# Adicione esta label ao serviço "webapp" para que o Watchtower o monitore:
# - "com.centurylinklabs.watchtower.enable=true"
The final file should look like this:
services:
watchtower:
image: containrrr/watchtower
command:
- "--label-enable"
- "--interval"
- "30"
- "--rolling-restart"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
reverse-proxy:
image: traefik:v3.1
command:
- "--providers.docker"
- "--providers.docker.exposedbydefault=false"
- "--entryPoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
- "--certificatesresolvers.myresolver.acme.email=email@email.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
ports:
- "80:80"
- "443:443"
volumes:
- letsencrypt:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock
webapp:
image: fernandeeess/portfolio-app:prod
labels:
- "traefik.enable=true"
- "traefik.http.routers.webapp.rule=Host(`yourdomain.com`)"
- "traefik.http.routers.webapp.entrypoints=websecure"
- "traefik.http.routers.webapp.tls.certresolver=myresolver"
- "traefik.http.services.webapp.loadbalancer.server.port=80"
- "com.centurylinklabs.watchtower.enable=true"
deploy:
mode: replicated
replicas: 3
volumes:
letsencrypt:
If everything is configured correctly, you will be able to access your project on the configured domain
10. Set up CI using GitHub Actions
Automate the process of building and pushing Docker images to Docker Hub. Please follow the instructions provided at the link below:
Use GitHub Actions to automatically build Docker images and push them to Docker Hub
Remember, the image must be tagged “prod” so that Watchtower can pull the changes and restart the service.
11. Links
- folder
-
Docker installation and configuration:
-
Docker files and concepts:
-
Additional resources and related tools:
-
Firewalls and Security Practices: