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
- Ubuntu server installed (I am using Ubuntu 22.04)
- Docker installed (If you need help with installing Docker here is a good article from DigitalOcean: How To Install and Use Docker on Ubuntu 22.04)
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
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
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;
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;
This line declares the directory in which we store our website's files.
index index.html;
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;
}
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;
}
This block tells the browser requesting the file types indicated to cache them for 30 days.
location ~ /\.ht {
deny all;
}
This block denies direct access to files with a format starting with .ht
. This includes .html
and .htm
files.
error_page 404 /index.html;
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
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
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 .
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
Now you can run the command below to see the list of your images:
docker image ls
You should see your image in the list as below:
my-static-website latest f8842f2b4cbf About a minute ago 62.2MB
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
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 withHOST: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
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
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"
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
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
After the installation is complete make sure to allow Nginx in your firewall:
sudo ufw allow 'Nginx HTTP'
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
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
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
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;
}
}
Let's break this configuration up:
listen 80;
This tells Nginx to listen to port 80 for this server configuration.
server_name mydomain.com;
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
The location scope in this configuration is:
location / {
proxy_pass http://127.0.0.1:4000;
include proxy_params;
}
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
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
Now move to the enabled sites directory:
cd /etc/nginx/sites-enabled
Check if your file is linked in this directory:
ls -hla
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
Now it is time to reload your Nginx so your website comes alive:
sudo systemctl reload nginx
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)