DEV Community

Cover image for Serving a Static Website in Ubuntu [NGINX + Docker]
Adnan Babakan (he/him)
Adnan Babakan (he/him)

Posted on

Serving a Static Website in Ubuntu [NGINX + Docker]

Hi there DEV.to community.

Recently as I was working on a personal project I was looking for a simple deployment of a static website on my Ubuntu server using Docker. Although it is possible to serve a simple static website using only nginx, this article will let you know about the principles of hosting a website using Docker which you can expand as you wish.

Prerequisities

Making our website ready

Nowadays there are two main website types. First is the one rendered using a back-end service such as Laravel or Nuxt called SSR websites. And second is the websites that are pre-rendered (referred to as a generated website as well). The pre-rendered websites can be categorized into two sections, a classic website which includes a .html file for each page, and a SPA which usually directs all requests to your index.html and your JavaScript router engages to respond with the appropriate page to be shown.

Here we will discuss SPA apps only that redirect all the request to your index.html. These types of generations are usually possible using the build tools inside your framework. For instance a Nuxt project can be generated for static deployment using npm run generate and puts the output inside your .output/public directory.

Preparing the Dockerfile

We should first create a file called Dockerfile at the root of our project.

We are going to use nginx:alpine image since it is lightweight and does whatever we want:

FROM nginx:alpine
Enter fullscreen mode Exit fullscreen mode

Then we should determine the config of our nginx. To do so first of all I prefer to remove the default config file to make sure the new data is loaded correctly:

RUN rm /etc/nginx/conf.d/default.conf

RUN echo "server {\
              listen 80;\
              root /usr/share/nginx/html;\
              index index.html;\
              location / {\
                  try_files $uri $uri/ /index.html;\
              }\
              location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$ {\
                  expires 30d;\
                  access_log off;\
              }\
              location ~ /\.ht {\
                  deny  all;\
              }\
              error_page 404 /index.html;\
          }" > /etc/nginx/conf.d/default.conf
Enter fullscreen mode Exit fullscreen mode

This section deletes the /etc/nginx/conf.d/default.conf file and writes the new data in the same file (and creating it since it doesn't exist anymore). The backslashes (\) are added to the end of each line since it is interpreted as a string and we need to make sure new lines are considered inside of the string and not our dockerfile. This is called an escape character.

Let's analyze what this Nginx config does:

listen 80;
Enter fullscreen mode Exit fullscreen mode

This line tells Nginx to listen on port 80. This port is exposed inside the container when it is running and we should bind it to an external port with which we can access the web server with our server's IP. So this 80 has nothing to do with which port the application will be accessible through.

root /usr/share/nginx/html;
Enter fullscreen mode Exit fullscreen mode

This line declares the directory in which we store our website's files.

index index.html;
Enter fullscreen mode Exit fullscreen mode

This line determines the index of website. Index of a website is the file that is loaded when no address is provided with the request.

location / {
      try_files $uri $uri/ /index.html;
}
Enter fullscreen mode Exit fullscreen mode

This block redirects all the requests to the index.html. So using this config assures us that the rest of the routing is handled by our JavaScript framework's router and not by the webserver all through.

location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$ {
       expires 30d;
       access_log off;
}
Enter fullscreen mode Exit fullscreen mode

This block tells the browser requesting the file types indicated to cache them for 30 days.

location ~ /\.ht {
       deny  all;
}
Enter fullscreen mode Exit fullscreen mode

This block denies direct access to files with a format starting with .ht. This includes .html and .htm files.

error_page 404 /index.html;
Enter fullscreen mode Exit fullscreen mode

And at the end, we redirect 404 errors to our index.html as well so that your beautiful error page can be shown by your framework.

COPY .output/public /usr/share/nginx/html
Enter fullscreen mode Exit fullscreen mode

The code above will be the last line in our Dockerfile. It copies the content of your generated website inside /usr/share/ngnix/html directory of the container that will run.

So here is the full Dockefile:

FROM nginx:alpine

RUN rm /etc/nginx/conf.d/default.conf

RUN echo "server {\
              listen 80;\
              root /usr/share/nginx/html;\
              index index.html;\
              location / {\
                  try_files $uri $uri/ /index.html;\
              }\
              location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$ {\
                  expires 30d;\
                  access_log off;\
              }\
              location ~ /\.ht {\
                  deny  all;\
              }\
              error_page 404 /index.html;\
          }" > /etc/nginx/conf.d/default.conf

COPY .output/public /usr/share/nginx/html
Enter fullscreen mode Exit fullscreen mode

Building and running

There are two main ways to run this Docker project. One is manually and the other one uses a compose.yml file. Here ae both:

Manually

First we need to build the image. Run the command below in your terminal of choosing at the root of your project:

docker build -t my-static-website .
Enter fullscreen mode Exit fullscreen mode

docker build is the command that builds your image for you. The -t flag sets a name for your image so you can replace my-static-website with anything you want. And finally . indicates the folder including your Dockerfile which is the current folder. If you ever wanted to run this command from another folder you need to put the absolute address of your projects root instead of ..

It might take a few seconds (or minutes) to build you image. The output of the command above should look like this:

[+] Building 36.7s (9/9) FINISHED                                                                                                 docker:default
 => [internal] load build definition from Dockerfile                                                                                        0.0s
 => => transferring dockerfile: 686B                                                                                                        0.0s
 => [internal] load metadata for docker.io/library/nginx:alpine                                                                             0.4s
 => [internal] load .dockerignore                                                                                                           0.0s
 => => transferring context: 2B                                                                                                             0.0s
 => [1/4] FROM docker.io/library/nginx:alpine@sha256:a5127daff3d6f4606be3100a252419bfa84fd6ee5cd74d0feaca1a5068f97dcf                      15.0s
 => => resolve docker.io/library/nginx:alpine@sha256:a5127daff3d6f4606be3100a252419bfa84fd6ee5cd74d0feaca1a5068f97dcf                       0.0s
 => => sha256:b887aca7aed6134b029401507d27ac9c8fbfc5a6cf510d254bdf4ac841cf1552 11.21kB / 11.21kB                                            0.0s
 => => sha256:af676cbe1eebad012784df0b450beb143af153514b61986d262cfae62f83335d 1.79MB / 1.79MB                                              4.2s
 => => sha256:cf04c63912e16506c4413937c7f4579018e4bb25c272d989789cfba77b12f951 4.09MB / 4.09MB                                              4.4s
 => => sha256:0bd499aae1693a481ec5f38cefa5f8803e0d8a5bef0c20cb3e6989eb2975cde9 627B / 627B                                                  1.2s
 => => sha256:a5127daff3d6f4606be3100a252419bfa84fd6ee5cd74d0feaca1a5068f97dcf 9.07kB / 9.07kB                                              0.0s
 => => sha256:19db381c08a95b2040d5637a65c7a59af6c2f21444b0c8730505280a0255fb53 2.50kB / 2.50kB                                              0.0s
 => => sha256:a258ad06a688beff3d4a30eede1b350e64a9ebbd5b80b8a095eec17f3f5a9e15 956B / 956B                                                  2.0s
 => => sha256:906b4c822d155ab6595749cb264093152af7faafabdc0d5534fb161909cf71b4 394B / 394B                                                  2.9s
 => => sha256:780210e97849521bfdd49e752a71a1e150219ee398e1bd8c6399e7d8797b923f 1.21kB / 1.21kB                                              3.7s
 => => sha256:45d5bc49ce25a46e0fa4f1481ae38f289d8f5f6b207ff03d038bcb784afe821e 1.40kB / 1.40kB                                              4.5s
 => => sha256:f12230e24af8b61e0b5f5e6c066fe0cfd975bac104a495a9e8fa28dc513c271f 13.72MB / 13.72MB                                           14.6s
 => => extracting sha256:cf04c63912e16506c4413937c7f4579018e4bb25c272d989789cfba77b12f951                                                   0.1s
 => => extracting sha256:af676cbe1eebad012784df0b450beb143af153514b61986d262cfae62f83335d                                                   0.0s
 => => extracting sha256:0bd499aae1693a481ec5f38cefa5f8803e0d8a5bef0c20cb3e6989eb2975cde9                                                   0.0s
 => => extracting sha256:a258ad06a688beff3d4a30eede1b350e64a9ebbd5b80b8a095eec17f3f5a9e15                                                   0.0s
 => => extracting sha256:906b4c822d155ab6595749cb264093152af7faafabdc0d5534fb161909cf71b4                                                   0.0s
 => => extracting sha256:780210e97849521bfdd49e752a71a1e150219ee398e1bd8c6399e7d8797b923f                                                   0.0s
 => => extracting sha256:45d5bc49ce25a46e0fa4f1481ae38f289d8f5f6b207ff03d038bcb784afe821e                                                   0.0s
 => => extracting sha256:f12230e24af8b61e0b5f5e6c066fe0cfd975bac104a495a9e8fa28dc513c271f                                                   0.3s
 => [internal] load build context                                                                                                           0.0s
 => => transferring context: 3.25kB                                                                                                         0.0s
 => [2/4] RUN rm /etc/nginx/conf.d/default.conf                                                                                            20.8s
 => [3/4] RUN echo "server {              listen 80;              root /usr/share/nginx/html;              index index.html;                0.3s
 => [4/4] COPY .output/public /usr/share/nginx/html                                                                                                      0.1s
 => exporting to image                                                                                                                      0.0s
 => => exporting layers                                                                                                                     0.0s
 => => writing image sha256:f8842f2b4cbf232c6b7139efe2e348e13a12188a58e60ef1f767a635c341de26                                                0.0s
 => => naming to docker.io/library/my-static-website 
Enter fullscreen mode Exit fullscreen mode

Now you can run the command below to see the list of your images:

docker image ls
Enter fullscreen mode Exit fullscreen mode

You should see your image in the list as below:

my-static-website   latest    f8842f2b4cbf   About a minute ago   62.2MB
Enter fullscreen mode Exit fullscreen mode

Now that we have our image ready it is time to run it. To run it we can use the command below:

docker run -d -p "4000:80" my-static-website
Enter fullscreen mode Exit fullscreen mode

The docker run command tries to run the image you provided. Here we added to flags which are as below:

  • -d tells docker to run the container in detached mode. Detached mode is when the process is run in background and doesn't need your terminal session to stay open.
  • -p flag which receives an argument with HOST:CONTAINER format binds your container's innerly exposed port to a port on your actual host machine.

Now you can see the list of your running containers using the command below:

docker container ls
Enter fullscreen mode Exit fullscreen mode

You should see a record like below:

e7a66fd934c1   my-static-website   "/docker-entrypoint.…"   3 seconds ago   Up 2 seconds   0.0.0.0:4000->80/tcp, :::4000->80/tcp   quirky_keller
Enter fullscreen mode Exit fullscreen mode

Now you can access your website using IP:4000. IP is the IP of the machine docker running on. Or 127.0.0.1:4000 if you are experimenting on your own machine.

If running a container as above satisfies your needs then we are done here. But I am going to show you how to set a domain on your project so you can access it using a domain instead of an IP.

Using docker composer

Usage of a compose file is super neat and makes your life easier. You can imagine it as a set of premade commands and instructions for your docker. Create a file named compse.yml and put the content below in it:

services:
    my-static-website:
        build:
            context: .
            dockerfile: Dockerfile
        ports:
            - "4000:80"
Enter fullscreen mode Exit fullscreen mode

As you can relate probably, these settings are somehow counterparts of the commands that we've run manually.

Inside the services, we define our service called my-static-website and let docker know how to build it using our Dockerfile. And we've bound out ports as well just as we did when running the docker run command.

Now that you have this file the only command you need to run is:

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

This command builds and runs your image and container respectively just as we've done it manually. And thanks to the -d (flag of detachment) the running process moved to the background.

Setting a domain

To set a domain we need to run an Nginx instance on the machine we've deployed the container itself. This makes us able to have a reverse proxy that directs incoming HTTP requests which use port 80 to another port and sends back the response to the user.

Installing Nginx on Ubuntu is fairly simple:

sudo apt install nginx
Enter fullscreen mode Exit fullscreen mode

After the installation is complete make sure to allow Nginx in your firewall:

sudo ufw allow 'Nginx HTTP'
Enter fullscreen mode Exit fullscreen mode

This opens ports 80 and 443 for HTTP and HTTPS respectively.

Nginx uses a structure that is easy to control available sites and enabled sites. To make a site enabled it is usually recommended to create a file following the websites domain inside the /etc/nginx/sites-available directory. So first of all move inside this directory:

cd /etc/nginx/sites-available
Enter fullscreen mode Exit fullscreen mode

Then we need to make a file named after the domain. Although it is not mandated to be the domain name, following this practice makes it easier to maintain your configuration files. You may create the file using the touch command:

touch mydomain.com.conf
Enter fullscreen mode Exit fullscreen mode

Or simple using a text editor to open the file (in case the file doesn't exist it will create it upon saving). I prefer nano so here is how we do it:

nano mydomain.com.conf
Enter fullscreen mode Exit fullscreen mode

Inside the editor paste the code below:

server {
        listen 80;
        server_name mydomain.com;
        location / {
                proxy_pass http://127.0.0.1:4000;
                include proxy_params;
        }
}
Enter fullscreen mode Exit fullscreen mode

Let's break this configuration up:

listen 80;
Enter fullscreen mode Exit fullscreen mode

This tells Nginx to listen to port 80 for this server configuration.

server_name mydomain.com;
Enter fullscreen mode Exit fullscreen mode

This sets the active domains for this server configuration. You may use multiple domains here and put the www. subdomain here as well in case you want it:

server_name mydomain.com www.mydomain.com
Enter fullscreen mode Exit fullscreen mode

The location scope in this configuration is:

location / {
      proxy_pass http://127.0.0.1:4000;
      include proxy_params;
}
Enter fullscreen mode Exit fullscreen mode

This is the main part that redirects all the requests to your container. It passes every request to 127.0.0.1:4000 along with all the parameters.

Now save the file and make sure your file is saved correctly using the ls command:

ls -hla
Enter fullscreen mode Exit fullscreen mode

If you see your file here then we are done here.

Now it is time to link this file to the /etc/nginx/sites-enabled directory which Nginx reads to serve websites:

sudo ln -s /etc/nginx/sites-available/mydomain.com.conf /etc/nginx/sites-enabled
Enter fullscreen mode Exit fullscreen mode

Now move to the enabled sites directory:

cd /etc/nginx/sites-enabled
Enter fullscreen mode Exit fullscreen mode

Check if your file is linked in this directory:

ls -hla
Enter fullscreen mode Exit fullscreen mode

You should see a record as below:

lrwxrwxrwx 1 root root   44 Sep 28 17:05 mydomain.com.conf -> /etc/nginx/sites-available/mydomain.com.conf
Enter fullscreen mode Exit fullscreen mode

Now it is time to reload your Nginx so your website comes alive:

sudo systemctl reload nginx
Enter fullscreen mode Exit fullscreen mode

If there are no errors in the configuration you've just created reloading the Nginx service won't output anything. So if you see no message you are done here as well.

Now that our Nginx server waits for requests coming for mydomain.com we need to point this domain to our server.

If you are using a real domain and a real server you can use a DNS service such as Cloudflare and make an A RECORD pointing your domain to your server's IP.

In case you are just testing locally you may change your hosts file to point your domain to your servers IP.

Now accessing the mydomain.com domain should show you your static website successfully!


Hope you've found this article helpful. Feel free to let me know if there are any mistakes in the article or any points to make it better.


BTW! Check out my free Node.js Essentials E-book here:

Feel free to contact me if you have any questions or suggestions.

Top comments (0)