Skip to content

Instantly share code, notes, and snippets.

@rigwild
Last active December 3, 2023 01:18
Show Gist options
  • Save rigwild/a9f6b9d64e41448fea919277b8fa53f3 to your computer and use it in GitHub Desktop.
Save rigwild/a9f6b9d64e41448fea919277b8fa53f3 to your computer and use it in GitHub Desktop.
VM Setup for dockerized apps and Portainer, including exposed NGINX with TLS configuration

Introduction

The goal of this this tutorial is to fully deploy a VM with multiple apps.

All the apps data will be stored in Docker volumes mounted to the host at /var/www/my-deploys for easy edit and backup. The benefit of this is you can create a GitHub repository and store all your configuration files in one place.

To deploy the docker-compose.yml of the apps below into Portainer, go to Stacks > Add stack.

Pre-requisites

NGINX and Certbot

Inspired from: https://mindsers.blog/en/post/https-using-nginx-certbot-docker/

We will run NGINX inside a Docker container, and configure our hosts on the VM at /var/www/my-deploys/nginx/.

Installation

mkdir -p /var/www/my-deploys/

Stack Name: nginx

version: '3'

services:
  webserver:
    image: nginx:latest
    user: '$UID:$GID'
    ports:
      - 80:80
      - 443:443
    network_mode: host
    restart: always
    volumes:
      - /var/www/my-deploys/nginx/nginx.conf:/etc/nginx/nginx.conf
      - /var/www/my-deploys/nginx/conf.d/:/etc/nginx/conf.d/
      - /var/www/my-deploys/nginx/ssl/:/etc/nginx/ssl/
      - /var/www/my-deploys/nginx/.well-known/:/usr/share/nginx/html/.well-known/

Update the NGINX configuration to include our mounted directory configurations.

sudo nano /var/www/my-deploys/nginx/nginx.conf
user nginx;
worker_processes auto;

include /etc/nginx/modules-enabled/*.conf;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;

events {
        worker_connections 1024;
}

http {
        ##
        # Basic Settings
        ##

        sendfile on;
        # tcp_nopush on;
        types_hash_max_size 2048;
        # server_tokens off;

        server_names_hash_bucket_size 128;
        # server_name_in_redirect off;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

        ##
        # SSL Settings
        ##

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
        ssl_prefer_server_ciphers on;

        ##
        # Logging Settings
        ##

        access_log /var/log/nginx/access.log main;
        error_log /var/log/nginx/error.log;

        ##
        # Gzip Settings
        ##

        gzip on;

        # gzip_vary on;
        # gzip_proxied any;
        # gzip_comp_level 6;
        # gzip_buffers 16 8k;
        # gzip_http_version 1.1;
        # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

        ##
        # Virtual Host Configs
        ##

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
}
sudo nano /var/www/my-deploys/nginx/conf.d/default.conf
server {
    listen       80;
    listen  [::]:80;
    server_name  _;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
}
docker exec nginx-webserver-1 nginx -s reload

When deployed, you should be able to reach you NGINX web server on port 80 and see the NGINX hello page.

For all NGINX configurations, you can use 127.0.0.1 because we specified network_mode: host which make the network of the container the same as the host. We are trying to achieve portability, not full isolation.

Add a service

We will configure a service running locally on the VM on port 49500 to be accessible externally at url diff.rigwild.dev.

Simply replace all diff.rigwild.dev with your own domain.

You can either follow the tutorial below or run source 2_add_nginx_service.sh script.

sudo nano /var/www/my-deploys/nginx/conf.d/diff.rigwild.dev.conf
upstream diff.rigwild.dev {
    server 127.0.0.1:49500;
    keepalive 64;
}

server {
## tls off ##    if ($host = diff.rigwild.dev) {
## tls off ##        return 301 https://$host$request_uri;
## tls off ##    }

    listen 80;
    listen [::]:80;
    server_name diff.rigwild.dev;

    location ^~ /.well-known/ {
        alias /usr/share/nginx/html/.well-known/;
    }

## tls off ##     return 404;
}

## tls off ## server {
## tls off ##     listen 443 ssl;
## tls off ##     listen [::]:443 ssl;
## tls off ##     server_name diff.rigwild.dev;
## tls off ##
## tls off ##     location / {
## tls off ##         proxy_set_header X-Forwarded-Host $host;
## tls off ##         proxy_set_header X-Forwarded-Server $host;
## tls off ##         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
## tls off ##         proxy_pass http://diff.rigwild.dev;
## tls off ##         proxy_http_version 1.1;
## tls off ##         proxy_pass_request_headers on;
## tls off ##         proxy_set_header Connection "keep-alive";
## tls off ##         proxy_store off;
## tls off ##     }
## tls off ##
## tls off ##     ssl_certificate     /etc/nginx/ssl/live/diff.rigwild.dev/fullchain.pem;
## tls off ##     ssl_certificate_key /etc/nginx/ssl/live/diff.rigwild.dev/privkey.pem;
## tls off ## }

To test if it works, you need to reload the NGINX configuration, you can do either:

  • In Portainer, go to Stacks > nginx > nginx-webserver-1 > Restart
  • Run (benefit: zero downtime!)
docker exec nginx-webserver-1 nginx -s reload

You should be able to access your service.

curl diff.rigwild.dev
<!DOCTYPE html>
<html lang="en">
  <title>Redirecting...</title>
  <h1>Redirecting...</h1>
  <p>You should be redirected automatically to the target URL: <a href="/login?next=/">/login?next=/</a>. If not, click the link.</p>
</html>

Now let's configure HTTPS/TLS for our domain using the Let's Encrypt ACME. Let's first do a dry run to make sure our setup is working as expected.

sudo certbot certonly --webroot --webroot-path /var/www/my-deploys/nginx/ -d diff.rigwild.dev --dry-run

If it was successfuly, you can now request your certificate!

sudo certbot certonly --webroot --webroot-path /var/www/my-deploys/nginx/ -d diff.rigwild.dev
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Account registered.
Requesting a certificate for diff.rigwild.dev

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/diff.rigwild.dev/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/diff.rigwild.dev/privkey.pem
This certificate expires on 2024-02-24.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
 * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
 * Donating to EFF:                    https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Now that we got the certificates, we can move them to the Docker volume shared with the NGINX container, then enable the TLS configuration.

sudo mkdir -p /var/www/my-deploys/nginx/ssl/live/
sudo mkdir -p /var/www/my-deploys/nginx/ssl/archive/

sudo cp -R /etc/letsencrypt/archive/diff.rigwild.dev /var/www/my-deploys/nginx/ssl/archive/
sudo cp -R /etc/letsencrypt/live/diff.rigwild.dev /var/www/my-deploys/nginx/ssl/live/

sudo cp /var/www/my-deploys/nginx/conf.d/diff.rigwild.dev.conf /var/www/my-deploys/nginx/conf.d/diff.rigwild.dev.conf.old
sudo sed -i -e 's/## tls off ## //g' /var/www/my-deploys/nginx/conf.d/diff.rigwild.dev.conf

docker exec nginx-webserver-1 nginx -s reload

Done! The service is not accessible at https://diff.rigwild.dev/, and insecure requests http will be redirected to secure https.

curl http://diff.rigwild.dev/
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx/1.25.3</center>
</body>
</html>

Prometheus, Prometheus Pushgateway and Grafana

Stack name: monitoring

version: '3.2'

services:
  prometheus:
    image: prom/prometheus
    user: '$UID:$GID'
    command:
      - '--config.file=/opt/prometheus/prometheus.yml'
      - '--web.listen-address=127.0.0.1:9090'
      - '--storage.tsdb.retention.time=1y'
      - '--storage.tsdb.path=/opt/prometheus/data'
    ports:
      - 9090:9090
    network_mode: host
    volumes:
      - /var/www/my-deploys/prometheus/prometheus.yml:/opt/prometheus/prometheus.yml
      - /var/www/my-deploys/prometheus/data/:/opt/prometheus/data/

  prometheus-pushgateway:
    image: prom/pushgateway
    command:
      - '--web.listen-address=127.0.0.1:9091'
      - '--web.external-url=https://prometheus-pushgateway.rigwild.dev'
    network_mode: host
    ports:
      - 9091:9091

  grafana:
    image: grafana/grafana-oss
    user: '$UID:$GID'
    container_name: grafana
    restart: unless-stopped
    network_mode: host
    ports:
      - '9099:9099'
    volumes:
      - /var/www/my-deploys/grafana/data/:/var/lib/grafana/
      - /var/www/my-deploys/grafana/defaults.ini:/usr/share/grafana/conf/defaults.ini

Prometheus configuration:

sudo mkdir /var/www/my-deploys/prometheus/
sudo chown -R 65534:65534 /var/www/my-deploys/prometheus/data

sudo nano /var/www/my-deploys/prometheus/prometheus.yml
# my global config
global:
  scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
  alertmanagers:
    - static_configs:
        - targets:
          # - alertmanager:9093

rule_files:
  # - "first_rules.yml"
  # - "second_rules.yml"

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: 'prometheus'

    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.

    static_configs:
      - targets: ['localhost:9091', 'localhost:9090']

Now simply add your pushgateway URL to your NGINX configuration, see Add a service to NGINX.

Grafana configuration:

sudo mkdir /var/www/my-deploys/grafana/
sudo chown -R 65534:65534 /var/www/my-deploys/grafana/

sudo wget https://raw.githubusercontent.com/grafana/grafana/main/conf/defaults.ini -O /var/www/my-deploys/grafana/defaults.ini
sudo sed -i 's/^http_addr =.*$/http_addr = 127.0.0.1/g' /var/www/my-deploys/grafana/defaults.ini
sudo sed -i 's/^http_port =.*$/http_port = 9099/g' /var/www/my-deploys/grafana/defaults.ini
sudo sed -i 's/^enforce_domain =.*$/enforce_domain = true/g' /var/www/my-deploys/grafana/defaults.ini
sudo sed -i 's/^domain =.*$/domain = grafana.rigwild.dev/g' /var/www/my-deploys/grafana/defaults.ini

Now simply add your grafana URL to your NGINX configuration, see Add a service to NGINX.

#!/bin/bash
# Check if script is ran by root user -> exit
if [[ $EUID -eq 0 ]]; then echo "This script should not be ran by root!"; exit 1; fi
# Stop script on error
set -e
set -o pipefail
sudo apt update
sudo apt upgrade -y
# Install common packages
sudo apt install -y \
linux-generic \
build-essential \
apt-transport-https \
ca-certificates \
curl \
gnupg-agent \
jq \
bat \
software-properties-common \
fail2ban \
nginx \
snapd
# Install Snap and certbot
sudo snap install core; sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
# Install Node.js
sudo apt-get install -y ca-certificates curl gnupg
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
NODE_MAJOR=20
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
sudo apt install -y nodejs
node -v
# Install pnpm
curl -fsSL https://get.pnpm.io/install.sh | sh -
# Install PM2 and zx
pnpm i -g pm2 zx
source ~/.bashrc
# Install Redis
sudo apt install -y redis-server
sudo sed -i -e 's/supervised no/supervised systemd/g' /etc/redis/redis.conf
sudo systemctl restart redis.service
# Configure fail2ban
awk '{ printf "# "; print; }' /etc/fail2ban/jail.conf | sudo tee /etc/fail2ban/jail.local
sudo sed -i -e 's/maxretry = 5/maxretry = 3/g' /etc/fail2ban/jail.conf
sudo sed -i -e 's/# \[sshd\]/# \[sshd-example-jail\]/g' /etc/fail2ban/jail.conf
sudo sed -i -e 's/\[sshd\]/\[sshd\]\nenabled = true/g' /etc/fail2ban/jail.conf
sudo sed -i -e 's/findtime = 10m/findtime = 15m/g' /etc/fail2ban/jail.conf
sudo service fail2ban restart
# Configure SSH
# Disable SSH password login
# sudo sed -i -e 's/#PasswordAuthentication yes/PasswordAuthentication no\n#PasswordAuthentication yes/g' /etc/ssh/sshd_config
# Change SSH port from 22 to 2222
sudo sed -i -e 's/#Port 22/Port 2222\n#Port 22/g' /etc/ssh/sshd_config
sudo mkdir -p /etc/systemd/system/ssh.socket.d
sudo bash -c 'cat << EOF > /etc/systemd/system/ssh.socket.d/listen.conf
[Socket]
ListenStream=
ListenStream=2222
EOF'
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
# Add Swap
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
sudo cp /etc/fstab /etc/fstab.bak
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
# Finalize setup
echo "alias grep='grep --color=auto'" >> ~/.bashrc
echo "alias fgrep='fgrep --color=auto'" >> ~/.bashrc
echo "alias egrep='egrep --color=auto'" >> ~/.bashrc
echo "alias l='LANG=C ls -ahl --color=auto $*'" >> ~/.bashrc
echo "alias ll='LANG=C ls -ahl --color=auto $*'" >> ~/.bashrc
echo "alias gs='git status'" >> ~/.bashrc
echo "alias gl='git log'" >> ~/.bashrc
echo "alias gb='git branch'" >> ~/.bashrc
echo "alias gc='git checkout'" >> ~/.bashrc
echo "alias bat='batcat'" >> ~/.bashrc
sudo mkdir -p /var/www
sudo chown -R $UID:$GID /var/www
source ~/.bashrc
my_deploy_domain=diff.rigwild.dev
my_service_port=49500
set -e
set -x
sudo bash -c "cat >> /var/www/my-deploys/nginx/conf.d/$my_deploy_domain.conf" << EOL
upstream $my_deploy_domain {
server 127.0.0.1:$my_service_port;
keepalive 64;
}
server {
## tls off ## if (\$host = $my_deploy_domain) {
## tls off ## return 301 https://\$host\$request_uri;
## tls off ## }
listen 80;
listen [::]:80;
server_name $my_deploy_domain;
location ^~ /.well-known/ {
alias /usr/share/nginx/html/.well-known/;
}
## tls off ## return 404;
}
## tls off ## server {
## tls off ## listen 443 ssl;
## tls off ## listen [::]:443 ssl;
## tls off ## server_name $my_deploy_domain;
## tls off ##
## tls off ## location / {
## tls off ## proxy_set_header X-Forwarded-Host \$host;
## tls off ## proxy_set_header X-Forwarded-Server \$host;
## tls off ## proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
## tls off ## proxy_pass http://$my_deploy_domain;
## tls off ## proxy_http_version 1.1;
## tls off ## proxy_pass_request_headers on;
## tls off ## proxy_set_header Connection "keep-alive";
## tls off ## proxy_store off;
## tls off ## }
## tls off ##
## tls off ## ssl_certificate /etc/nginx/ssl/live/$my_deploy_domain/fullchain.pem;
## tls off ## ssl_certificate_key /etc/nginx/ssl/live/$my_deploy_domain/privkey.pem;
## tls off ## }
EOL
docker exec nginx-webserver-1 nginx -s reload
sudo certbot certonly --register-unsafely-without-email --agree-tos --webroot --webroot-path /var/www/my-deploys/nginx/ -d $my_deploy_domain
sudo mkdir -p /var/www/my-deploys/nginx/ssl/live/
sudo mkdir -p /var/www/my-deploys/nginx/ssl/archive/
sudo cp -R /etc/letsencrypt/archive/$my_deploy_domain /var/www/my-deploys/nginx/ssl/archive/
sudo cp -R /etc/letsencrypt/live/$my_deploy_domain /var/www/my-deploys/nginx/ssl/live/
sudo cp /var/www/my-deploys/nginx/conf.d/$my_deploy_domain.conf /var/www/my-deploys/nginx/conf.d/$my_deploy_domain.conf.old
sudo sed -i -e 's/## tls off ## //g' /var/www/my-deploys/nginx/conf.d/$my_deploy_domain.conf
docker exec nginx-webserver-1 nginx -s reload
echo "DONE!! Trying to curl the domain"
echo "DONE!! Trying to curl the domain"
echo "DONE!! Trying to curl the domain"
curl http://$my_deploy_domain/
curl https://$my_deploy_domain/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment