This repository was created for the talk at the PyStok #55 event. This guide will demonstrate how to use Docker, Docker-Compose, and Gitlab CI to do your small dockerized project and deploy to mikr.us VPS.
If you have any problems feel free to create an issue on GitHub
You can also review the app at GitLab.
If you have any questions, you can also join my Discord.
Before starting, make sure that you have installed the following:
- Docker (Linux, Mac, Windows)
- Docker Compose v2 - You need this only if you have installed Docker Engine or Docker CLI. Docker Desktop includes Docker Compose.
-
Create an empty project directory.
mkdir django-poll
You can name the directory on something easy for you to remember. This directory is the context for your application image. The directory should only contain resources to build that image.
-
Go to the root of your project directory.
cd django-poll
-
Create a new file called
Dockerfile
in your project directory.The Dockerfile defines an application's image content via one or more build commands that configure that image. Once built, you can run the image in a container. For more information on
Dockerfile
, see the Docker user guide and the Dockerfile reference. -
Add the following content to the
Dockerfile
.# syntax=docker/dockerfile:1 FROM python:3 ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 WORKDIR /code COPY requirements.txt /code/ RUN pip install -r requirements.txt COPY . /code/
This
Dockerfile
starts with a Python parent image. The parent image is modified by:- settings environment variables:
ENV PYTHONDONTWRITEBYTECODE=1
- This prevents Python from writing out pyc filesENV PYTHONUNBUFFERED=1
- This keeps Python from buffering stdin/stdoutWORKDIR /code
- adding a newcode
directoryCOPY requirements.txt /code/
- copingrequirements.txt
file from context to thecode
folder inside the docker imageRUN pip install -r requirements.txt
- installing the Python requirements defined in therequirements.txt
file
- settings environment variables:
-
Create a
requirements.txt
in your project directory.This file is used by the
RUN pip install -r requirements.txt
command in yourDockerfile
. -
Add the required software to the file.
# Install the last Django LTS Django>=3.2,<3.3 # Install PostgreSQL database adapter psycopg2>=2.9
-
Create a file called
docker-compose.yml
in your project directory.The
docker-compose.yml
file describes the services that make your app. In this example, those services are a web server and a database. The compose file also describes which Docker images these services use, how they link together, and any volumes they might need to be mounted inside the containers. Finally, thedocker-compose.yml
file describes which ports these services expose. See thedocker-compose.yml
reference for more information on how this file works. -
Add the following configuration to the file.
services: db: image: postgres restart: unless-stopped volumes: - postgresql_data:/var/lib/postgresql/data environment: - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres} web: build: . restart: unless-stopped command: python manage.py runserver 0.0.0.0:8000 volumes: - .:/code ports: - "8000:8000" environment: - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres} depends_on: - db volumes: postgresql_data:
This file defines two services: The
db
service and theweb
service.db
service will mountpostgresql_data
volume to/var/lib/postgresql/data
inside Docker. Thanks to this, data from the container won't be lost. We also set the new environment variable:POSTGRES_PASSWORD
with a password to the Postgres database.${POSTGRES_PASSWORD:-postgres}
means take environment variablePOSTGRES_PASSWORD
or if it doesn't exist, set the default value topostgres
.web
service defines a few more things.- We set the current directory as context (
build: .
). It will be used when we build a container usingdocker compose build
restart: unless-stopped
restart the container unless stopped e.g. usingdocker compose stop
- Entrypoint command that will be used to run the container is
python manage.py runserver 0.0.0.0:8000
. - We mount the current directory (the directory where is
docker-compose.yml
file) inside the/code
folder in the container.
volumes: - .:/code
- Next, we publish
8000
port outside the container. Below code map TCP port 8000 in the container to port 8000 on the Docker host (<host>:<container>
).
ports: - "8000:8000"
- We add a new environment variable,
POSTGRES_PASSWORD
, with the default value "postgres". - Our container depends on
db
, so the container with the database should start first, and thenweb
container will start.
depends_on: - db
- We set the current directory as context (
In this step, you create a Django starter project by building the image from the build context defined in the previous procedure.
-
Create in your project directory the Django project by running the docker compose run command as follows. The project name should follow PEP-8 naming convention.
docker compose run web django-admin startproject django_poll .
This instructs Compose to run
django-admin startproject <project_name>
in a container, using theweb
service's image and configuration. Because theweb
image doesn't exist yet, Compose builds it from the current directory, as specified by thebuild: .
line indocker-compose.yml
.Once the
web
service image is built, Compose runs it and executes thedjango-admin startproject
command in the container. This command instructs Django to create a set of files and directories representing a Django project. -
After the
docker compose
command completes, list the contents of your project.$ ls -l drwxr-xr-x 2 root root django_poll -rw-rw-r-- 1 user user docker-compose.yml -rw-rw-r-- 1 user user Dockerfile -rwxr-xr-x 1 root root manage.py -rw-rw-r-- 1 user user requirements.txt
If you are running Docker on Linux, the files
django-admin
created are owned by root. This happens because the container runs as the root user. Change the ownership of the new files.sudo chown -R $USER:$USER django_poll manage.py
If you are running Docker on Mac or Windows, you should already have ownership of all files, including those generated by
django-admin
. List the files to verify this.
In this section, you set up the database connection for Django and start docker compose
with a project.
-
In your project directory, edit the
django_poll/settings.py
file. -
Add to the beginning of the file
import os
-
Replace the
DATABASES = ...
with the following:DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'postgres', 'USER': 'postgres', 'PASSWORD': os.getenv('POSTGRES_PASSWORD'), 'HOST': 'db', 'PORT': 5432, } }
These settings are required because we decided to use postgres as database. We specified it in
docker-compose.yml
. -
Run the docker compose up command from the top level directory for your project.
$ docker compose up [+] Running 2/0 ⠿ Container django-poll-db-1 Running 0.0s ⠿ Container django-poll-web-1 Created 0.1s Attaching to django-poll-db-1, django-poll-web-1 ⠿ Container django-poll-db-1 Created 0.1s ⠿ Container django-poll-web-1 Created 0.1s Attaching to django-poll-db-1, django-poll-web-1 django-poll-db-1 | The files belonging to this database system will be owned by user "postgres". django-poll-db-1 | This user must also own the server process. < ... > django-poll-web-1 | Watching for file changes with StatReloader django-poll-web-1 | Performing system checks... django-poll-web-1 | django-poll-web-1 | System check identified no issues (0 silenced). django-poll-web-1 | December 11, 2022 - 17:15:11 django-poll-web-1 | Django version 3.2.16, using settings 'django_poll.settings' django-poll-web-1 | Starting development server at http://0.0.0.0:8000/ django-poll-web-1 | Quit the server with CONTROL-C.
At this point, your Django app should be running at port
8000
on your Docker host. On Docker Desktop for Mac and Docker Desktop for Windows, go tohttp://localhost:8000
on a web browser to see the Django welcome page.Note:
On specific platforms (Windows 10), you might need to edit
ALLOWED_HOSTS
insidesettings.py
and add your Docker hostname or IP address to the list. For demo purposes, you can set the value to:ALLOWED_HOSTS = ['*']
This value is not safe for production usage. Refer to the Django documentation for more information.
-
List running containers.
In another terminal window, list the running Docker processes with the
docker ps
ordocker container ls
command.$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 42a04442ac98 django-poll-web "python manage.py ru…" 3 minutes ago Up 3 minutes 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp django-poll-web-1 2068de7d75d8 postgres "docker-entrypoint.s…" 3 minutes ago Up 3 minutes 5432/tcp django-poll-db-1
-
Shut down services and clean up by using either of these methods:
-
Stop the application by typing
Ctrl-C
in the same shell in where you started it:Gracefully stopping... (press Ctrl+C again to force) Killing django-poll-web-1 ... done Killing django-poll-db-1 ... done
-
Or, for a more elegant shutdown, switch to a different shell, and run docker compose down from the top level of your Django sample project directory.
$ docker compose down Stopping django-poll-web-1 ... done Stopping django-poll-db-1 ... done Removing django-poll-web-1 ... done Removing django-poll-web_run_1 ... done Removing django-poll-db-1 ... done Removing network django-poll_default
-
In this section, you prepare a project for easy local development.
-
Create a file called
docker-compose.override.yml
in your project directory and add the following configuration. More about multiple Compose files can be read here -
Add the following configuration to the file.
services: web: command: python manage.py runserver 0.0.0.0:8000 volumes: - .:/code ports: - "8000:8000"
This file overrides the
web
service by adding the custom commandrunserver
and mounting the current directory to/code
location inside a container. Thanks to this operation, you will be able to work with the code inside the container that will be reloaded automatically. We also map8000
port from container to host. -
Edit a
requirements.txt
in your project directory and add the following packages.# Install Python WSGI HTTP Server for UNIX gunicorn>=20.1 # Install simplified static file serving for Python web apps whitenoise>=6.2
-
Edit a
web
section indocker-compose.yml
. It should look like below:web: image: ${CI_REGISTRY_IMAGE:-local/django_poll}:${CI_COMMIT_REF_SLUG:-local} build: . environment: - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres} depends_on: - db
We have added an image name with default values which helps us when we call it in CI/CD. We use there Gitlab Ci Variables which are available in pipelines. Sections that were transferred to the
docker-compose.override.yml
file have been deleted. -
Edit the
settings.py
file in your application:- Add to
MIDDLEWARE
list'whitenoise.middleware.WhiteNoiseMiddleware',
at the end. - Add
STATIC_ROOT = '/static/'
as config of root folder contains static files.
- Add to
-
Edit
urls.py
file in your application by adding- at the beginning:
from django.conf import settings from django.conf.urls.static import static
- at the bottom:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
-
Now, you are ready to rebuild the project. This is needed for new dependencies declared in the
requirements.txt
file to be installed. Just run$ docker compose build [+] Building 1.6s (14/14) FINISHED => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 32B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => resolve image config for docker.io/docker/dockerfile:1 1.0s => CACHED docker-image://docker.io/docker/dockerfile:1@sha256:9ba7531bd80fb0a858632727cf7a112fbfd19b17e94c4e84ced81e24ef1a0dbc 0.0s => [internal] load build definition from Dockerfile 0.0s => [internal] load .dockerignore 0.0s => [internal] load metadata for docker.io/library/python:3 0.0s => [1/5] FROM docker.io/library/python:3 0.0s => [internal] load build context 0.0s => => transferring context: 7.11kB 0.0s => CACHED [2/5] WORKDIR /code 0.0s => CACHED [3/5] COPY requirements.txt /code/ 0.0s => CACHED [4/5] RUN pip install -r requirements.txt 0.0s => [5/5] COPY . /code/ 0.0s => exporting to image 0.1s => => exporting layers 0.0s => => writing image sha256:770f3446a8da8cd89b454e5f6ea66368ec6fbcb16ab8b2b916f10b96389122e3 0.0s => => naming to docker.io/local/django_poll:local
-
Now, you can run the project once more. Just run the Docker compose up command from the top-level directory for your project.
$ docker compose up [+] Running 2/2 ⠿ Container django-poll-db-1 Created 0.0s ⠿ Container django-poll-web-1 Recreated 0.1s Attaching to django-poll-db-1, django-poll-web-1 django-poll-db-1 | django-poll-db-1 | PostgreSQL Database directory appears to contain a database; Skipping initialization django-poll-db-1 | django-poll-db-1 | 2022-12-18 23:14:28.416 UTC [1] LOG: starting PostgreSQL 15.1 (Debian 15.1-1.pgdg110+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit django-poll-db-1 | 2022-12-18 23:14:28.416 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432 django-poll-db-1 | 2022-12-18 23:14:28.416 UTC [1] LOG: listening on IPv6 address "::", port 5432 django-poll-db-1 | 2022-12-18 23:14:28.421 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" django-poll-db-1 | 2022-12-18 23:14:28.428 UTC [29] LOG: database system was shut down at 2022-12-18 23:10:04 UTC django-poll-db-1 | 2022-12-18 23:14:28.435 UTC [1] LOG: database system is ready to accept connections django-poll-web-1 | Watching for file changes with StatReloader django-poll-web-1 | Performing system checks... django-poll-web-1 | django-poll-web-1 | System check identified no issues (0 silenced). django-poll-web-1 | December 18, 2022 - 23:14:30 django-poll-web-1 | Django version 3.2.16, using settings 'django_poll.settings' django-poll-web-1 | Starting development server at http://0.0.0.0:8000/ django-poll-web-1 | Quit the server with CONTROL-C.
Now you should develop your project. For example, you can write your first Django app
with an official Django tutorial.
Remember that all commands are given inside the tutorial you should run inside your container.
If the tutorial says that you should run the command:
$ python manage.py startapp polls
You need to go first inside your container and then run this command.
$ cd django-poll/
user@local-pc:~/Workspace/django-cicd/django-poll$ docker compose exec web bash
root@ae372cbade9b:/code# python manage.py startapp polls
root@ae372cbade9b:/code#
We run this command from the place where we create docker-compose.yml
Remember that all files created inside a container on your local file system will have root
permissions. This happens because the container runs as the root user. You can change
the ownership of the new files using the command below. It will change ownership of all files
inside the current directory.
$ sudo chown -R $USER:$USER .
Our app should have a simple home page to see if everything works fine.
- Go to
settings.py
, search forTEMPLATES = [...
and add to['DIRS']
folder with templates:['templates'],
so it should look like this:TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': ['templates'], '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', ], }, }, ]
- At the root of your project create folder
templates
- Inside the folder
templates
create ahome.html
file and paste it into your simple HTML site. In my case content of this file looks as follows:<!doctype html> <html class="no-js" lang="en"> <head> <meta charset="utf-8"> <title>PyStok 55 - Alfabet dewelopera - (A)utomatyczne (B)udowanie (C)iągłe (D)ostarczanie</title> <meta name="description" content=""> <meta name="viewport" content="width=device-width, initial-scale=1"> </head> <body> <h1>Alfabet dewelopera - (A)utomatyczne (B)udowanie (C)iągłe (D)ostarczanie</h1> <p>Hello world!</p> <p>Krzysztof Owsieniuk</p> </body> </html>
- Open
urls.py
and add before urlpatterns import:And next, add tofrom django.views.generic import TemplateView
urlpatterns
path to your homepage so it should look like this:urlpatterns = [ path('', TemplateView.as_view(template_name='home.html')), path('admin/', admin.site.urls), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
If your project is ready to deploy, you must prepare your VPS (Virtual Private Server). In our case, we will use MIKR.US. We want to run Docker inside, so we must choose the Mikrus 2.1 server or above. After purchasing the service, you will receive auth data to the server on your email address.
-
Login into your VPS through ssh.
$ ssh root@<your_server>.mikr.us -p 10000
-
We need to install Docker inside. For this purpose, we will use NOOBS scripts preinstalled on the machine. In our case, we will use chce_dockera.sh script, which will install
Docker
$ . /opt/noobs/scripts/chce_dockera.sh [...] Unable to find image 'hello-world:latest' locally latest: Pulling from library/hello-world 2db29710123e: Pull complete Digest: sha256:c77be1d3a47d0caf71a82dd893ee61ce01f32fc758031a6ec4cf1389248bb833 Status: Downloaded newer image for hello-world:latest Hello from Docker! This message shows that your installation appears to be working correctly. To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon. 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. (amd64) 3. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading. 4. The Docker daemon streamed that output to the Docker client, which sent it to your terminal. To try something more ambitious, you can run an Ubuntu container with: $ docker run -it ubuntu bash Share images, automate workflows, and more with a free Docker ID: https://hub.docker.com/ For more examples and ideas, visit: https://docs.docker.com/get-started/
-
Of course, this is a minimal setup, and it is not recommended to use it in the production server.
After our server has a minimal setup, we need to create our GitLab project and create some more local setups. If this is your first time with Gitlab CI, please familiarize yourself with Gitlab CI Quick Start. This section will use GitLab CI/CD Examples and SSH keys with GitLab CI/CD adapted to our needs.
-
Create a new SSH key pair locally with ssh-keygen. Run
ssh-keygen -t
followed by the key type and an optional comment. This comment is included in the .pub file that’s created. You can use an email address for the comment. For example, for ED25519:$ ssh-keygen -t ed25519 -C "<comment>"
For 2048-bit RSA:
$ ssh-keygen -t rsa -b 2048 -C "<comment>"
Remember that your generated private key should be stored by default in
~/.ssh/
folder. -
Create a new Gitlab project
-
Add the private key as a GitLab CI variable to your project. You can do it from
Settings->CI/CD->Variables
. A variable should have the nameSSH_PRIVATE_KEY
and store your private key generated in 1 step. -
You need to add two more variables as above.
SSH_SERVER
should store your server addressSSH_PORT
should store your server port
-
Minimal Gitlab setup is complete. Now we need to prepare our project. First, we should set up our repository with a local project. Because we have an existing project, if we try to clone git inside a folder with files, it could throw an error:
fatal: destination path '.' already exists and is not an empty directory.
. So we need to do it differently way e.g.git clone [email protected]:<you gitlab repo>.git temp && mv temp/.git . && rm -rf temp
or use one of other solutions like init repo and add origin. -
Create a
.gitlab-ci.yml
file at the repository's root. This file is where you define the CI/CD jobs. Add the following content to the file.# Use the official docker image in all stages image: docker:latest # We define two stages stages: - build - deploy # The first stage will be responsible for building our project using `docker compose` command # and push built images to the GitLab repository docker-build: stage: build services: - docker:dind before_script: - echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin script: - docker compose -f docker-compose.yml build - docker compose push # Second stage will deploy our app to the server using a remote docker host connection. docker-deploy: stage: deploy variables: # Setup temporary stage variable DOCKER_HOST: ssh://root@$SSH_SERVER:$SSH_PORT before_script: # Install ssh-agent if not already installed. Docker requires it. - 'command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )' # Run ssh-agent (inside the built environment) - eval $(ssh-agent -s) # Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store # We're using tr to fix line endings which makes ed25519 keys work # without extra base64 encoding. # https://gitlab.com/gitlab-examples/ssh-private-key/issues/1#note_48526556 - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - # Create a `.ssh` folder if it does not exist - mkdir -p ~/.ssh # Use ssh-keyscan to scan the keys of your private server. - ssh-keyscan -Hp $SSH_PORT $SSH_SERVER >> ~/.ssh/known_hosts # Login on a remote server to the GitLab repository - echo "$CI_REGISTRY_PASSWORD" | docker -H $DOCKER_HOST login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY script: # We pull our image built-in `docker-build` stage. The image is downloaded one at a time due to the: # https://github.com/docker/compose/issues/9448 - docker compose -f docker-compose.yml pull web - docker compose -f docker-compose.yml up -d --remove-orphans
-
Add
wait-for-it.sh
file to the project root. You can download the content of this file from here. This file is a bash script that will wait on the availability of a host and TCP port. You can also use the command above to download the file.$ wget https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh
Remember to add execution right for this script using
$ chmod +x wait-for-it.sh
-
Edit your
docker-compose.yml
file, and afterbuild: .
section add the following content:command: bash -c " ./wait-for-it.sh db:5432 && python manage.py migrate --noinput && python manage.py collectstatic --noinput && gunicorn django_poll.wsgi --workers 2 --bind 0.0.0.0:8000 " ports: - "80:8000"
This will add a custom entrypoint command running at your server's container startup. The first thing this script does is wait for the database until she is ready to accept connections. Then we will migrate our migrations, collect static files, which will be served by
whitenoise
, and run the gunicorn WSGI server with our app at the8000
port inside the container. We map this port to the80
port at our server because we will accept connection at this port from the internet. This is the default port for the HTTP protocol. -
Edit
settings.py
, turn off debug mode settingDEBUG = False
and add your domain name toALLOWED_HOSTS
list, or you can set it to ALLOWED_HOSTS = ['*'] for development purposes. -
Last but not least is to create
.dockerignore
at the root of your project. This file tells Docker when he is building a container which files should ignore and don't add inside the image. We should add to this file all files which we won't be able at our server and could potentially be dangerous. In my case, this file contains the following content.*.py[cod] *.mo *.db *.egg-info *.sql* .cache .project .idea .pydevproject .idea/ .DS_Store .git/ .sass-cache __pycache__ dist docs env logs Dockerfile docker-compose*
-
Commit all your changes and push them to a remote server.
$ git add . && git commit -m "Init django poll app" && git push
After you push your changes at the Gitlab inside the
CI/CD->Pipelines
section, you should see you running jobs.
You can track changes that were done at the pipeline by clicking a single job name and going inside.
You should see a screen like the one below. It describes all things which the GitLab runner server has done.
-
If everything works fine, after login into your server through the ssh you and using
curl
onlocalhost
you should see the same content as your created.root@i359:~# curl localhost <!doctype html> <html class="no-js" lang="en"> <head> <meta charset="utf-8"> <title>PyStok 55 - Alfabet dewelopera - (A)utomatyczne (B)udowanie (C)iągłe (D)ostarczanie</title> <meta name="description" content=""> <meta name="viewport" content="width=device-width, initial-scale=1"> </head> <body> <h1>Alfabet dewelopera - (A)utomatyczne (B)udowanie (C)iągłe (D)ostarczanie</h1> <p>Hello world!</p> <p>Krzysztof Owsieniuk</p> </body>
If everything went okay in the previous steps, the last stage is to set up your domain. There are two most common cases we may encounter. The first is when you don't have your domain, and the second is when you have one.
If you don't have your own domain, you can configure free subdomain provided by Mikr.us. For this, you
need to go to the management panel and go to the tab Subdomeny
. Next,
name your subdomain and save the changes.
Setup can take about 5 minutes. After this time, go to your subdomain and check if everything works fine!
There are a few different ways to connect your domain with Mikr.us. We will use the most recommended i.e.
we will use free Cloudflare for this purpose. So if you don't have
a Cloudflare account, create it. Next, login, go to Websites
, and click + Add site
.
Next, enter your site at the input and click Add Site
. The next step is to choose a subscription plan. We will
use the free plan, so select the free option at the bottom and continue. Now you have to change your domain DNS to given by
Cloudflare. Pointing to Cloudflare’s nameservers is a critical step in activation and must be completed
for Cloudflare to optimize and protect your site. Log in to your domain provider, go to DNS settings, and set the server
to provided by Cloudflare. Save your changes. Registrars can take 24 hours to process nameserver updates.
You will receive an email when your site is active on Cloudflare. After your domain setup is complete, go to Cloudflare to
your domain, next DNS->Records
and add a new record with the AAAA
type name you choose and your Mikr.us IPv6 address, Proxy status
must be checked.
You can check your IPv6 Mikrus address by login through ssh and using the command:
ip -6 addr
or if you don't want to search, you can use this:
/sbin/ip -6 addr | grep inet6 | awk -F '[ \t]+|/' '{print $3}' | grep -v ^::1 | grep -v ^fe80
After saving if everything was fine, go to your subdomain and enjoy the completed process!