DEV Community

Cover image for Install a Second Instance of Nginx via Docker.
darwish
darwish

Posted on

Install a Second Instance of Nginx via Docker.

Recently, I faced a use case where I needed to install a Django app on a server that already had a deployed instance of Frappe ERPNext. If you don’t know, Frappe has a CLI called Bench, and it handles the generation of config files automatically. So, I found it a bad idea to inject other files to deploy my new app. Instead, I decided to install another instance of Nginx via Docker and connect it to another port on my server.

Assumptions

To follow this tutorial, I assume that you already have a cloud server running anywhere like AWS, GCP, Digital Ocean, etc., and you can SSH to your server.

Setting Up a New User and ssh

please note that i use UBUNTU instance so you can follow the steps if your package manager is apt

First, let’s create a new user to separate everything. I will SSH as root to the server and run this command

adduser dev
Enter fullscreen mode Exit fullscreen mode

This should create a new username “dev” on my cloud instance. After this, I will go to my normal terminal and generate a new SSH key for the new user I’ve created with this command.

ssh-keygen -t rsa -b 4096 -f ~/.ssh/digital
Enter fullscreen mode Exit fullscreen mode

this should generate two files under ~/.ssh/digital and ~/.ssh/digital.pub

I will copy the contents of the public file by running this.

xclip -sel clip < ~/.ssh/digital.pub
Enter fullscreen mode Exit fullscreen mode

And after that, I will go to my server and open the authorized_keys file.

vim /home/dev/.ssh/authorized_keys
Enter fullscreen mode Exit fullscreen mode

and add the contents of my public key .

you can install vim if it’s not installed by running
apt install vim
with that i can run this command on my terminal to directly connect to the new user i’ve created

ssh dev@ipaddress -i ~/.ssh/digital
Enter fullscreen mode Exit fullscreen mode

install docker and give permissions to the new user

Now we need to install Docker and add the new user to the Docker group to be able to run all Docker commands without sudo. We can do this by following the documentation page: https://docs.docker.com/engine/install/ubuntu/

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
Enter fullscreen mode Exit fullscreen mode

And after adding the repository source, now you can run.

apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Enter fullscreen mode Exit fullscreen mode

Remember that I am still logged in as root, so I don’t need sudo here. However, of course, you will need to run sudo if you are not logged in as root. The reason I do this from the root account is that I don’t want to give the dev account sudo access to limit their permissions and avoid any conflicts with the running frapppe instance.

Now we can test our Docker installation by running this.

docker run hello-world
Enter fullscreen mode Exit fullscreen mode

if this is result then the installation is successfull

Image description

But still, if you tried to log in with the "dev" user and run this command, you will get a permission error. To solve this, we need to run this command from our root account.

usermod -aG docker dev
Enter fullscreen mode Exit fullscreen mode

This should add our user “dev” to the Docker group, which enables this user to use Docker.

login with the new user and create the folders

After all this, I will SSH to the new user and try to run “docker run hello-world” to validate that Docker is working. Also, I can open my ~/.ssh/config file on my local computer and add this block.

Host digital
    HostName 68.183.214.170
    User dev
    IdentityFile ~/.ssh/digital
Enter fullscreen mode Exit fullscreen mode

To be able to easily SSH to my server, via this command.

ssh digital
Enter fullscreen mode Exit fullscreen mode

I prefer to create this structure

src
    ├── apps
    └── common
Enter fullscreen mode Exit fullscreen mode

I put everything in the “src” directory, and the second level includes “src/apps” and “src/common”. In the “common” directory, I install every container that will be used across different apps, such as nginx, postgres, or redis. In the “apps” directory, I install my apps to avoid installing multiple instances of the common containers.

so let’s create our folders by runing these commands

mkdir -p src/common/nginx
mkdir -p src/apps/omdabus.esolvelabs.com
Enter fullscreen mode Exit fullscreen mode

Notice here, I used “omdabus.esolvelabs.com” as the folder name to easily memorize my folders by the domain they are deployed at later on.

And then we need to go to the nginx folder and create the docker-compose.yml file.

cd src/common/nginx `
vim docker-compose.yaml
Enter fullscreen mode Exit fullscreen mode

nginx/docker-compose.yml

services:
  nginx:
    image: nginx
    container_name: nginx
    restart: always
    volumes:
      - ./conf.d:/etc/nginx/conf.d/:ro
    ports:
      - "81:80"
      - "4433:443"

networks:
  default:
    external: true
    name: nginx
Enter fullscreen mode Exit fullscreen mode

Notice here we used 81 & 4433 for our ports, as well as adding volumes to bind our config folder to our container. We also added a network with the name Nginx to be able to connect our Django app later on.

but we should create this network first by running this.

docker network create nginx
Enter fullscreen mode Exit fullscreen mode

Now let’s create our Django container.

For the sake of this tutorial, I won’t delve deeper into how I dockerized the Python app. Instead, I will focus on Nginx and how to configure it.

So here is an example of the docker-compose.yml file inside src/apps/omdabus.esovlelabs.com.

services:
   elomda_bus:
    image: exploremelon/elomda_bus:0.0.11
    container_name: elomda_bus
    restart: always
    networks:
      - nginx
networks:
   nginx:
      external: true
Enter fullscreen mode Exit fullscreen mode

After this, I will run my container and make sure that it connects to the same network “nginx”.

you can run this to validate your network

docker network inspect nginx
Enter fullscreen mode Exit fullscreen mode

Now, we can go back to our Nginx folder and add the config file. First, we need to add a DNS record to connect the domain to our app. I have a domain managed by Cloudflare, so I just need to open my Cloudflare account and add a new A record with the name of the subdomain I need to use, along with the IP address of your server.

Image description

You can get your IP by running this command:

curl ifconfig.me
Enter fullscreen mode Exit fullscreen mode

Now we can add a new file called “omdabus.esolvelabs.com.conf” under nginx/conf.d and add this content.

upstream bus {
    server elomda_bus:8000;
}

server {
    listen 80;
    server_name omdabus.esolvelabs.com;

    location / {
        proxy_pass http://bus;
    }
}
Enter fullscreen mode Exit fullscreen mode

If you try to visit the URL: http://omdabus.esolvelabs.com:81 now, you will see an error,

Image description

So, we need to edit settings.py file in Django to update the allowed hosts. To do this, we need to add a new volume on our app docker-compose file to avoid re-uploading each time we change things on the configuration. We can do this by going to the app folder.

cd src/apps/omdabus.esolvelabs.com

and then add the “settings.py” file

vim settings.py

I will put the code inside my Python repo settings.py.

"""
Generated by 'django-admin startproject' using Django 2.0.

For more information on this file, see
https://docs.djangoproject.com/en/2.0/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.0/ref/settings/
"""

import os

STRIPE_SECRET_KEY = os.getenv('STRIPE_SECRET_KEY')
STRIPE_PUBLISHABLE_KEY = os.getenv('STRIPE_PUBLISHABLE_KEY')

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'i0&iq&e9u9h6(4_7%pt2s9)f=c$kso=k$c$w@fi9215s=1q0^d')

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False  

ALLOWED_HOSTS = ['0.0.0.0' , '192.168.1.6', 'localhost', '127.0.0.1']

CSRF_TRUSTED_ORIGINS = []   
CSRF_COOKIE_SECURE = True   
CSRF_COOKIE_HTTPONLY = True  
SESSION_COOKIE_SECURE = True  

SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_SSL_REDIRECT = False
SECURE_HSTS_SECONDS = 31536000   

# Application definition

INSTALLED_APPS = [
    'orders.apps.OrdersConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.humanize',
    # 'import_export',
]

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware', 
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'pizza.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'pizza.wsgi.application'


# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# Password validation
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/

STATIC_URL = '/static/'
Enter fullscreen mode Exit fullscreen mode

but i will change the allowed hosts line to bus to it

ALLOWED_HOSTS = ['0.0.0.0' , '192.168.1.6', 'bus', 'localhost', '127.0.0.1']
Enter fullscreen mode Exit fullscreen mode

notice that bus here is the name of the upstream we used on nginx config file

after this i need to edit the docker-compose file of my app to add the new volume

services:
   elomda_bus:
    image: exploremelon/elomda_bus:0.0.11
    container_name: elomda_bus
    volumes:
      - ./settings.py:/app/pizza/settings.py
    restart: always
    networks:
      - nginx
networks:
   nginx:
      external: true
Enter fullscreen mode Exit fullscreen mode

Then, try to reload,

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

and now it should be working.

But if you try to submit any form again, you will see an error.

Image description

To solve this, we need to add our URL to the CSRF hosts in settings.py.

CSRF_TRUSTED_ORIGINS = ["http://omdabus.esolvelabs.com:81" , "https://omdabus.esolvelabs.com:4433"]  
Enter fullscreen mode Exit fullscreen mode

At this point, everything should work if we visit http://omdabus.esolvelabs.com:81

setup tls via certbot and dns-cloudflare-credentials

Let’s set up TLS with Certbot. I prefer to use dns-cloudflare-credentials for its simplicity, so what you need is to get the API token from your Cloudflare account that is connected to your domain. You can do this by going to https://dash.cloudflare.com/profile/api-tokens

make sure you are logged in first. Then, you can choose one of the provided templates.

Image description

For the sake of this tutorial, I will use the Edit Zone DNS. After this, you need to install Certbot if it’s not installed and also install the Cloudflare plugin by running this as the root user.
Enter fullscreen mode Exit fullscreen mode
apt install certbot python3-certbot-dns-cloudflare
Enter fullscreen mode Exit fullscreen mode

Finally, to generate our certificates, we need to obtain the generated API token and save it in a file inside our nginx folder. Personally, I use a file named dns-credentials/cloudflare.ini.

dns_cloudflare_api_token=yourtoken

now we can run this command to generate our certificates

certbot certonly --dns-cloudflare --dns-cloudflare-credentials dns-credentials/cloudflare.ini -d omdabus.esolvelabs.com --email info@esolvelabs.com --agree-tos --non-interactive --force-renewal --cert-name omdabus.esolvelabs.com --keep-until-expiring --rsa-key-size 4096

notice here you need to replace the domain name with your actual domain
Enter fullscreen mode Exit fullscreen mode

This should create folder under /etc/letsenctrypt/live.

Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/omdabus.esolvelabs.com/fullchain.pem

So let’s add a volume to our container, and let’s edit the config file for the last time.

nginx/docker-compose.yaml

services:
  nginx:
    image: nginx
    container_name: nginx
    restart: always
    volumes:
      - ./conf.d:/etc/nginx/conf.d/:ro
      - /etc/letsencrypt/:/etc/letsencrypt
    ports:
      - "81:80"
      - "4433:443"

networks:
  default:
    external: true
    name: nginx
Enter fullscreen mode Exit fullscreen mode

The new volume should connect our letsencrypt folder on the host to the letsencrypt folder on the container, and we should edit the conf.d/omdabus.esolvelabs.com.conf.

upstream bus {
    server elomda_bus:8000;
}

server {
    listen 80;
    server_name omdabus.esolvelabs.com;

    return 301 https://$server_name:4433$request_uri;
}

server {
    listen 443 ssl http2;
    server_name omdabus.esolvelabs.com;

    ssl_certificate /etc/letsencrypt/live/omdabus.esolvelabs.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/omdabus.esolvelabs.com/privkey.pem;

    location / {
        proxy_pass http://bus;
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice here, we redirected the traffic on port 80 to the HTTPS server. However, we also used port 4433 for the redirection because we mapped the ports on our host differently. Additionally, we added the SSL block and attached the generated certificates to it. Now, if we visit https://omdabus.esolvelabs.com:4433, we should see that everything works properly.

Conclusion

This guide has shown you how to set up a second Nginx server using Docker. This is useful when you want to run multiple web applications on the same server without them interfering with each other.
Here are the main things we did:

1 . Created a new user: This helps keep your different applications separate.

  1. Installed Docker: Docker is a tool that makes it easy to run applications in containers.

  2. Set up Nginx: Nginx is a web server that helps people access your website.

  3. Created a network: This allows your Django app and Nginx to communicate.

5.Configured Nginx: We told Nginx to serve your Django app on a specific port.

  1. Secured the website: We added HTTPS to your website using Certbot.

Here’s a GitHub repository that demonstrates the steps outlined in the previous response:

https://github.com/darwishdev/dockerized-nginx-server

Top comments (0)