Tips on reading the systemd journal logs

When looking at logs, you should almost always add the --utc flag or check server's local
time (timedatectl status). It should always be UTC (timedatectl set-timezone UTC).
Why? Because when you look at logs, you often compare them with other logs/events. E.g.
Sentry issues, papertrail logs, other systems, other servers, ...

Let's talk about the most frequent journalctl commands.

Usually, you don't want to see all the logs. You want to see logs from a specific unit
(e.g., HAProxy, MySQL...). You also want to specify the time range. For this, we need to
add 2 flags. --unit (-u for short) and
--since (-S for short).

Generally, I like to use full flag names because IMO it's a good practice to do so. Why?
You learn them faster, and you will remember them longer because they are words. This is
the reason why we invented domains.
You also often copy-paste those commands to your co-workers, to your documentation, etc.
And if a developer isn't familiar with those flags, they will need to check the docs.
If we use full flag names, this is often unnecessary because full flag names are more

$ journalctl --unit haproxy --since "2 days ago"
$ journalctl --unit haproxy --since "2 hours ago"
$ journalctl --unit haproxy --since "2018-06-26"
$ journalctl --unit haproxy --since "2018-06-26 23:00"
$ journalctl --unit haproxy --since "2018-06-26" --until "2018-06-27"

You can also select multiple units.

$ journalctl --unit haproxy --unit mysql

Sometimes you want to search through logs. You have 2 options. You can use the --grep
flag or you can pipe the journalctl output to grep.

I usually prefer to pipe the output because of 2 things. 1. I can do things like count
the results (grep --count ...). Also, results are printed to your terminal, and you
can copy-paste them.
The bad thing with piping the results is that you don't see some information like
e.g., system reboot.

journalctl --unit haproxy | grep  "Segmentation fault"
journalctl --unit haproxy | grep --count "Segmentation fault"
journalctl --unit haproxy --grep "Segmentation fault"

If you want to immediately see the last, e.g., 1000 lines of the journal log, you can use
--pager-end (-e for short) to jump to the end and --lines 1000 (-n) to show you
the last 1000 lines.

$ journalctl --pager-end --lines 1000

Show kernel messages from the last boot:

$ journalctl --boot

To see system log messages, we need to filter by identifier. You can also use --identifier
for other services (e.g., haproxy).

$ systemctl --identifier kernel
$ systemctl --identifier haproxy

When you are doing live monitoring, you will want to use the --follow (-f) flag:

$ systemctl --unit haproxy --follow


Trojan horse

A Trojan horse is a computer program that is downloaded into a victim's computer with the intention of disguising itself and collecting information, corrupting the computer, installing other malicious programs and stealing passwords. These Trojans have many different forms and different threats, which are detailed below:

  • Monitoring tools: This detects activities on the computer and sends information to the hacker.
  • Replacement: delegitimates user control of the computer to another person.
  • Denial of service: position that prevents other software from functioning.

Trojan Threats

A Trojan can be used to execute malicious code. This code is hidden inside the Trojan and when activated it will download the malicious program and install it. Sometimes it can also delete files on the computer. Besides, it can also damage other computer applications and causes hard drives to become heads of viruses. The following are some of the Trojan attacks methods:

  • Virus downloads: when the user visits a website, the Trojan downloads the virus and runs it.
  • Monitor downloads: when a user visits a download site, the Trojan downloads the program and runs it.
  • Other viruses: These viruses include spyware and worms. Spyware and worms will run in the background of the computer and track activities being done on the computer. They can copy themselves to other computer systems as well as to the ones outside the computer. Once it copies itself to another computer, it can be activated, and this can be done by an Internet user who downloads it. Once activated, it can perform tasks that the creator of the Trojan wants done.

Worms are more malicious than viruses. It is called a worm because it replicates itself, by sending itself to other computers through network drives or through emails.

Spyware: This is any software that collects information about the user through the Internet or through the machine. This information is then transferred to the third party. Sometimes, the information transferred is encrypted so that the third party cannot decrypt it. It can also perform tasks that impact a user's privacy, including recording keystrokes and sending cookies with attachments to the user.

Deceptive adware: This is a misleadingly advertised software program that promises a service that it does not provide. It can harm a user's computer and/or steal private information.

What steps can you take to protect your computer?

  1. Make sure that you have a comprehensive software protection suite installed and up to date on your computer.
  2. Make sure that you have anti-virus software installed and up to date. If your anti-virus is out of date, update it.
  3. Make sure that you have a spyware and adware removal tool installed and up to date.
  4. Use adware and spyware removal programs weekly or even daily.
  5. Programs that provide cloud protection should have anti-virus and spyware protection.
  6. Invest in Identity Theft Protection. It offers real-time protection from spam, online phishing, malicious websites and free website blocking.

Business Operations simulations – Tackling the Gantt Chart

Many companies have a project management tool that allows them to create and maintain the Gantt Chart and other chart-based project management tools. But for those who have never used one, or have not experienced the Gantt Chart approach, the process is very straightforward.

The components of the Gantt Chart comprise the bars, for each task or activity, along with the start and finish time. Doing things like deciding when to add new tasks, combine activities or complete other activities depends on what the outset and finish times are. When bad meetings with no resolution occur, the Gantt simply lists all these activities with a start and finish time, they are put into the appropriate " quadrant " of the Gantt.

There is a second way of looking at the Gantt. It is called a colored trigger. This type of Gantt is created around a box. It is filled with red and represents activities for which the project team has a clear time line that must be completed. Gray areas in the Gantt let the managers know that the activities are occurring but no resolution has been completed.

F Anchorage Glossary defines attribute as follows:

boys: drugs are very dangerous

water: water should be checked every other day

hazards: hazards, elements or processes that could affect the quality of work to be performed or generate health problems for employees

prices: prices to be discounted, rescheduled or set aside until a further date

exploring the Gantt Chart has some other advantages over more traditional project management tools. The Gantt, for example, is a great way of expressing a common problem and discovering the most efficient way of dealing with that problem. It requires not the same amount of work to create and maintain as simple project management tools. Almost any Gantt chart is up to date as additional resources are added on a subject or simply things change on the projects. Thus all other project managers and project teams have a common tool.

The Gantt chart means that time is spent on all activities in the project already implemented. If the activities are still running when the project ends, then it will not be over till after the end of the project.

NixOS Configuration

This is my compact version of NixOS Configuration chapter that can be found on the NixOS Manual. I've added and modified a few things.

NixOS Configuration File

The NixOS configuration file generally looks like this:

{ config, pkgs, ... }:

{ option definitions

The first line ({ config, pkgs, ... }:) denotes that this is actually a function that takes at least the two arguments config and pkgs. The function returns a set of option definitions ({ ... }). These definitions have the form name = value, where name is the name of an option and value is its value. For example,

{ config, pkgs, ... }:

{ services.httpd.enable = true;
  services.httpd.adminAddr = "";
  services.httpd.documentRoot = "/webroot";

Options have various types of values. The most important are:

  1. Strings

    networking.hostName = "dexter";
  2. Booleans

    networking.firewall.enable = true;
  3. Integers

    boot.kernel.sysctl."net.ipv4.tcp_keepalive_time" = 60;
  4. Sets (name/value pairs enclosed in braces)

    services.httpd = {
      enable = true;
      adminAddr = "";
      documentRoot = "/webroot";

    You can use // operator if you need to merge two attribute sets. For example:

      exampleOrgCommon = {
        hostName = "";
        documentRoot = "/webroot";
        adminAddr = "";
        enableUserDir = true;
    in {
      services.httpd.virtualHosts = [
        (exampleOrgCommon // {
          enableSSL = true;
          sslServerCert = "/root/ssl-example-org.crt";
          sslServerKey = "/root/ssl-example-org.key";
  5. Lists

    boot.kernelModules = [ "fuse" "kvm-intel" "coretemp" ];
  6. Packages (Usually, the packages you need are already part of the Nix Packages collection, which is a set that can be accessed through the function argument pkgs.)

    environment.systemPackages =
    [ pkgs.thunderbird
    services.postgresql.package = pkgs.postgresql_10;

    If you need to add a custom package, take a look at Adding Custom Packages chapter.

Ref: NixOS Manual


Functions provide another method of abstraction. For instance, suppose that we want to generate lots of different virtual hosts, all with identical configuration except for the hostname. This can be done as follows:

  services.httpd.virtualHosts =
      makeVirtualHost = name:
        { hostName = name;
          documentRoot = "/webroot";
          adminAddr = "";
      [ (makeVirtualHost "")
        (makeVirtualHost "")
        (makeVirtualHost "")
        (makeVirtualHost "")

Here, makeVirtualHost is a function that takes a single argument name and returns the configuration for a virtual host. That function is then called for several names to produce the list of virtual host configurations.

This works because attribute values are expressions (as opposed to attribute keys).

See Abstractions chapter
how we can improve this even further (hint: by using map function).

WordPress on NixOS

Let's take a look at a real world example of a configuration.nix that deploys 2 WP blogs.
Keep in mind that when we install NixOS the /etc/nixos/configuration.nix file will already
contain some configuration. It might look something like this:

{ pkgs, ... }: {
  imports = [

  boot.loader.timeout = 60;
  boot.loader.grub.device = "/dev/vda";

  i18n.consoleUseXkbConfig = true;
  services.xserver.xkbVariant = "dvp";

  networking = {
    useDHCP = false;
    nameservers = [ "" "" ];
    defaultGateway = "185.186.999.1";
    interfaces.ens3 = {
      ipv4.addresses = [{
        address = "185.186.999.996";
        prefixLength = 24;

  services.openssh = {
    enable = true;
    permitRootLogin = "yes";

  users.users.root.initialPassword = "secret";
  system.stateVersion = "19.09";

We want to keep this configuration and just add ours on top of this.

{ pkgs, ... }:

  # For shits and giggles, let's package the responsive theme
  responsiveTheme = pkgs.stdenv.mkDerivation {
    name = "responsive-theme";
    # Download the theme from the wordpress site
    src = pkgs.fetchurl {
      url = "";
      sha256 = "1g1mjvjbx7a0w8g69xbahi09y2z8wfk1pzy1wrdrdnjlynyfgzq8";
    # We need unzip to build this package
    buildInputs = [ pkgs.unzip ];
    # Installing simply means copying all files to the output directory
    installPhase = "mkdir -p $out; cp -R * $out/";

  # WordPress plugin 'akismet' installation example
  akismetPlugin = pkgs.stdenv.mkDerivation {
    name = "akismet-plugin";
    # Download the theme from the wordpress site
    src = pkgs.fetchurl {
      url = "";
      sha256 = "sha256:1wjq2125syrhxhb0zbak8rv7sy7l8m60c13rfjyjbyjwiasalgzf";
    # We need unzip to build this package
    buildInputs = [ pkgs.unzip ];
    # Installing simply means copying all files to the output directory
    installPhase = "mkdir -p $out; cp -R * $out/";

  makeWPConfig = { name, root, aliases }: {
    "${name}" = {
      database = {
        host = "localhost";
        name = name;
        passwordFile = pkgs.writeText "wordpress-dbpass" "secret";
        createLocally = true;
      themes = [ responsiveTheme ];
      plugins = [ akismetPlugin ];
      virtualHost = {
        adminAddr = "admin@localhost";
        serverAliases = aliases;
        documentRoot = root;
in {

  networking.firewall.enable = true;
  networking.firewall.allowedTCPPorts = [ 80 443 ];
  services.httpd.adminAddr = "";
  services.wordpress = map makeWPConfig [
      name = "foo";
      root = "/var/www/foo_com";
      aliases = [ "" "" ];
      name = "bar";
      root = "/var/www/bar_com";
      aliases = [ "" "" ];

services.wordpress doesn't allow us to have different database users with createLocally
option set to true. If we want to use different database users per WP instance we need
to configure them ourselves and set the createLocally to false.

Here are all the services.wordpress options.



The NixOS configuration mechanism is modular. If your configuration.nix becomes too big, you can split it into multiple files.
Modules have exactly the same syntax as configuration.nix. In fact, configuration.nix is itself a module. You can use other modules by including them from configuration.nix, e.g.:

{ config, pkgs, ... }:

{ imports = [ ./vpn.nix ./kde.nix ];
  services.httpd.enable = true;
  environment.systemPackages = [ pkgs.emacs ];

Here, we include two modules from the same directory, vpn.nix and kde.nix. The latter might look like this:

{ config, pkgs, ... }:

{ services.xserver.enable = true;
  services.xserver.displayManager.sddm.enable = true;
  services.xserver.desktopManager.plasma5.enable = true;

When multiple modules define an option, NixOS will try to merge the definitions.

With multiple modules, it may not be obvious what the final value of a configuration option is. The command nixos-option allows you to find out:

$ nixos-option services.xserver.enable

$ nixos-option boot.kernelModules
[ "tun" "ipv6" "loop" ... ]

Interactive exploration of the configuration is possible using nix repl, a read-eval-print loop for Nix expressions. A typical use:

$ nix repl '<nixpkgs/nixos>'

nix-repl> config.networking.hostName

So if we take another look at our configuration.nix where we defined WP services we can
apply some modularity to it. Let's take out the WP configuration and put it in a new file
named wp.nix. Our configuration.nix will now look like this:

{ pkgs, ... }: {
  imports = [

  boot.loader.timeout = 60;

And our wp.nix config file can stay exactly the same (as defined above). We don't need
to manually merge thous two configuration files and the structure will be cleaner and
easier to maintain.

If you need to take a quick look at nix syntax you can find it here: Syntax Summary.

Short introduction to Nix, NixOS and NixOps

NixOps: Declarative Provisioning and Deployment

  • It is declarative. There is no difference between doing a new deployment or doing an
    upgrade of an existing deployment. The resulting machine configurations will be the
    same, allowing deployments to be upgraded or reproduced reliably.

  • There are several prominent configuration management systems with declarative models,
    such as Cfengine, Puppet and Chef. However, the systems they manage still have
    underlying imperative configuration models, such as configuration files in /etc
    that are updated in place by deployment actions. Thus the result of a deployment may
    still depend on the previous configuration of the system.

  • It performs provisioning. For instance, if we instantiate an Amazon EC2 machine
    as part of a larger deployment, it may be necessary to put the IP address or host
    name of that machine in a configuration file on another machine, and to ensure that
    any changes are propagated properly.

  • It allows abstracting over the target environment. The same specification can be
    deployed to different cloud backends.

  • NixOps ensures that machines can talk to each other, e.g. by creating tunnels
    between machines in different EC2 regions.

  • With NixOps, the same toolchain supports both development and production use.
    By contrast, Vagrant provisions VirtualBox virtual machines to set up test
    environments which can then be configured by tools such as Chef. To deploy to e.g.
    EC2, other tools are required.

  • It uses a single configuration formalism (Nix’s purely functional language) for
    package management and system configuration management. This makes it very easy to add
    ad hoc packages to a deployment.

  • The functional approach is less suited to automatically finding optimal solutions
    to sets of constraints (e.g. to find a deployment that satisfies a feature model).

  • NixOps tracks the state of deployments in a SQLite database.

Ref: Charon: Declarative Provisioning and Deployment


NixOps deploys NixOS machines, so we start with a brief overview of NixOS' configuration
model. In NixOS, machines are configured by providing a file (typically
/etc/nixos/configuration.nix) that specifies the desired configuration of the system.
For instance, the following file specifies that we want a machine that runs the
Apache web server:

    { services.httpd.enable = true;
      services.httpd.documentRoot = "/data";

Configuration changes are realised by running the command nixos-rebuild, which
evaluates the system configuration, builds all dependencies, and finally starts,
restarts or stops any new, changed or removed system services in the new configuration.

For instance, if the previous configuration had services.httpd.enable = false, then
running nixos-rebuild will cause Apache httpd to be built or downloaded (if it wasn’t
already present in the system), an httpd.conf configuration file to be generated, and
finally httpd to be started.

In NixOS, all system services are started and monitored using the systemd program
(ref: You can ask for detailed
status information about a unit, for instance, the PostgreSQL database service:

$ systemctl status postgresql.service

More resources:


NixOS builds on Nix, a purely functional package manager. NixOS uses Nix to build
packages and other static system configuration artifacts such as configuration files in
a reproducible way. The file configuration.nix is essentially a parameter to a Nix
function that evaluates to a large dependency graph of packages, configuration files and
boot scripts in the Nix store, together constituting the system.

Nix stores these artifacts in the filesystem in locations such as
/nix/store/wjbcr40b...-apache-httpd-2.2.23/, where wjbcr40b... is a cryptographic
hash of the dependencies of the artifact.
As a result when upgrading the old system configuration is never overwritten, allowing
efficient rollbacks and nearly atomic upgrades. This is super helpful when we
accidentally crash our system.

Here is a nice tour of Nix where you can get more
familiar with Nix.

More resources:

[NixCon 2019] Reading Nix expressions (notes)

This are my notes from the Reading Nix expressions talk in NixCon 2019.

Nix package language has a similar syntax to the "JSON meets functional language concepts". E.g. you have Let-expressions which I would say is a functional concept (ref: - I first saw it in Elm and Haskell.

An important thing to keep in mind is that Nix is a lazy language so some code might not get evaluated if it is not called. This means you can actually read the code from the "middle". For example, you can start reading this config in line 12 and then continue to the next one. Once you see a variable like e.g. ${version} you can "evaluate" it in your head (in this case version is defined in line 1).

The point is that some variables/functions can be defined but might not get called so Nix won't evaluate them. This is completely different from how e.g. Python works.


Nix functions are just lambda expressions. Example:

a: b: a + b

Another representation is keyword arguments or a set pattern:

{ a, b }: a + b

This one is the most popular. You can call this function with a and b attributes. If you need additional arguments you can use an ellipsis (...):

{ a, b, c, ... }: a + b + c

This will work on any set that contains at least the three named attributes.



Nix expression evaluator has a bunch of functions and constants built in. For example:

  • toString e (Convert the expression e to a string)
  • import path (Load, parse and return the Nix expression in the file path)
  • throw s (Throw an error message s. This usually aborts Nix expression evaluation)
  • map f list (Apply the function f to each element in the list list)

There are tons of other [buildins available][].


We have several reserved keywords in Nix:


Nix also provides a bunch of operators like Arithmetic addition, subtraction, division, etc.

See this link for the full list of operators.

This is the very basic of the Nix language. You will need the above knowledge if you will want to start hacking nix config files. Or should I say if you want to know what you are actually doing while hacking nix config files 🙂

Create a new package in Go using Go Modules

Go Modules

The purpose of this post is to give a very simple example and instructions on how to
create a Go package with the Go Modules.

Create a new package

Let's start by creating a new repository (e.g. go-modules) and apply the following
folder/file structure to it:

|-- bar
|   `-- bar.go
|-- foo
|   `-- foo.go
`-- main.go

Now run

$ go mod init gomodules

This will create go.mod file with the following structure:

module gomodules

go 1.13

Now let's add an external dependency to our project:

$ go get

This will add require v1.5.2 // indirect to go.mod and it will also
create go.sum file with pinned packages.

Now let's add some code.

Add this to main.go:

package main

import (

func main() {

Add this to foo/foo.go:

package foo

import (

func main() {

Add this to bar/bar.go:

package bar

import (

func Hello() {
    fmt.Println("Hello from Bar.")

Now run go build .. This will produce a file executable file named gomodules. If we run
it this is what the output should be:

$ ./gomodules
Hello, world.
Hello from Bar.
Hello, world.

The first Hello, world. is from calling foo.Hello() in main.go then we call bar.Hello()
which produces Hello from Bar. and Hello, world. because bar.Hello also calls

NOTE: Do not forget to name the public functions with a
capital letter
otherwise they won't be available in main.go (because they won't be exported).

Pin a package

Now let's add another package but this time let's pin it to a specific version:

$ go get

If we would already have added text-generator we would need to use the -u (upgrade)
flag to apply the changes.

$ go get -u

This will add v0.1.0 to go.mod file. The v0.1.0
refers to the github tag. You can see all tags/releases here:
(for liderman/text-generator package).

Now let's modify main.go file a bit to use our new package:

package main

import (

func main() {

    tg := text_generator.New()
    template := "Good {morning|day}!"


Now we need to build the package again and run it:

$ go build
$ ./gomodules
Hello, world.
Hello from Bar.
Hello, world.
Good day!

Tidy your go.mod

To make sure that go.mod matches the source code in the module run go tidy:

$ go mod tidy

It adds any missing modules necessary to build the current module's
packages and dependencies, and it removes unused modules that
don't provide any relevant packages. It also adds any missing entries
to go.sum and removes any unnecessary ones.

See this article
using go list, go mod why and go mod graph.

What is the difference between package versions?

Using apidiff to determine API compatibility.


Allow root access without password

First add a user:

$ sudo adduser ubuntu
$ sudo usermod -aG sudo ubuntu

Edit sudoser file with visudo (edit the sudoers file in a safe fashion).

$ sudo visudo

Add this line in the last line and save it.


Now sudo -s or sudo su - will login to root without asking password. This is also
required if you want to run ansible scripts with become: yes
(see: ansible docs).

Next thing we need to add our ssh public key to .ssh/authorized_keys so that we can
ssh without password.

$ cd  # go to home directory
$ mkdir .ssh
$ chmod 755 ~/.ssh  # .ssh directory should have 755 permissions and be owned by the user
$ touch authorized_keys
$ chmod 644 ~/.ssh/authorized_keys  # authorized_keys file should have 644 permissions and be owned by the user
$ nano .ssh/authorized_keys

Copy and paste your public ssh key into .ssh/authorized_keys and save it. That is it.

You can also run this ansible playbook to do basically the same thing:

- hosts: all
    - name: Add the user 'ubuntu' with a primary group of 'admin'
        name: ubuntu
        groups: admin
        append: yes
    - name: PKI | get pubkey from Github and placed as authorized_keys
       url:{{ item }}.keys
       dest: /tmp/{{ item }}.keys
        - karantan
    - name: PKI | Ensure .ssh/ folder exists
        owner: ubuntu
        group: ubuntu
        path: /home/ubuntu/.ssh/
        state: directory

    - name: PKI | Add downloaded keys to authorized_keys
        dest: /home/ubuntu/.ssh/authorized_keys
        src: /tmp
        regexp: \.keys$
        owner: ubuntu
        group: ubuntu

    - name: PKI | Disallow password authentication
        dest: /etc/ssh/sshd_config
        regexp: "^(#)?(\\s)?PasswordAuthentication (yes|no)$"
        line: PasswordAuthentication no

    - name: Restart sshd
      action: service name=ssh state=restarted

And then you can run playbooks as ubuntu like this:

- hosts: <hosts>
    - name: Create test directory as ubuntu user
      become: yes
      become_user: ubuntu
        path: /home/ubuntu/test
        state: directory

    - name: Create test2 directory as root
        path: /home/ubuntu/test2
        state: directory

Install Apache & Set Up Virtual Hosts

This is just a quick step by step guide taken from DigitalOcean's tutorial (see the references at the bottom). The main difference is that it has less explanation - so if you know what you are doing you can just copy & paste the commands ...

$ sudo apt-get update
$ sudo apt-get install apache2

Set Global ServerName to Suppress Syntax Warnings

Next, we will add a single line to the /etc/apache2/apache2.conf file to suppress a warning message. While harmless, if you do not set ServerName globally, you will receive the following warning when checking your Apache configuration for syntax errors:

$ sudo apache2ctl configtes
$ sudo nano /etc/apache2/apache2.conf

Add `ServerName server_domain_or_IP` save and close the file.

Adjusting the Firewall

$ sudo ufw app list
$ sudo ufw allow 'Apache'
$ sudo ufw status

If `ufw` is not enabled do it by running `sudo ufw enable`. Make sure that OpenSSH is on the allowed list.

Checking your Web Server

$ sudo systemctl status apache2

Setting Up Virtual Hosts

Create the directory for your_domain as follows:

$ sudo mkdir /var/www/your_domain
$ nano /var/www/your_domain/index.html

Add to index.html:

<title>Welcome to Your_domain!</title>
<h1>Success! The your_domain virtual host is working!</h1>

In order for Apache to serve this content, it's necessary to create a virtual host file with the correct directives. Instead of modifying the default configuration file located at /etc/apache2/sites-available/000-default.conf directly, let's make a new one at /etc/apache2/sites-available/your_domain.conf:

$ sudo nano /etc/apache2/sites-available/your_domain.conf

Paste the following to apache config:

<VirtualHost *:80>
ServerAdmin webmaster@localhost
ServerName your_domain
ServerAlias www.your_domain
DocumentRoot /var/www/your_domain
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined

Let's enable the file with the a2ensite tool and disable the default one:

$ sudo a2ensite your_domain.conf
$ sudo a2dissite 000-default.conf

Check the apache config and reload the apache:

$ sudo apache2ctl configtest
$ sudo systemctl reload apache2

On your local computer edit hosts file:

$ sudo nano /etc/hosts

Add e.g `<server_IP>` entry, save and quit. Open your web browser and go to ``. You should see `Success! The virtual host is working!` (i.e. Virtual Hosts is correctly configured).