Install a Second Instance of Nginx via Docker.

Darwishdev
9 min read1 day ago

--

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

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

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

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

vim /home/dev/.ssh/authorized_keys

and add the contents of my public key .

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

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

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

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

if this is result then the installation is successfull

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

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

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

ssh digital

I prefer to create this structure

src
├── apps
└── common

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 this commands

mkdir -p src/common/nginx
mkdir -p src/apps/omdabus.esolvelabs.com

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

you can install vim if it’s not installed by running

apt install vim

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

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

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

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

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.

You can get your IP by running this command:

curl ifconfig.me

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;
}
}

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

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/'

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']

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

Then, try to reload, and now it should be working.

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

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"]  

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.

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.

apt install certbot python3-certbot-dns-cloudflare

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

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

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;
}
}

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.

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

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

4. 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.

6. 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

--

--