🐳 How to dockerize your static website with Nginx, automatic renew SSL for domain by Certbot and deploy it to DigitalOcean?
Tags: tutorial, nginx, docker, beginners
Published: 2020-01-26 22:13:31

## Introduction

Welcome back, friends! 👋 There's a very big topic on our agenda... but **don't worry**, it'll be interesting and very informative!

📌 **Objectives of article:**

1. Quick meet with Docker Compose (not "_deep_", but close);
2. Configure Docker containers for Nginx, Certbot and frontend;
3. Write simple static website (using `Parcel.js` as bundler);
4. Push finished project to git repository;
5. Deploy project to [DigitalOcean](https://m.do.co/c/b41859fa9b6e) droplet;

> _Without too much modesty_, I advise you to add this article to your bookmarks, because nowhere will you find such a **detailed** description of the deploying process.

...and we begin! 🔥 

## What's Docker Compose?

Follow official Docker [docs](https://docs.docker.com/compose/):

> Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services. Then, with a single command, you create and start all the services from your configuration.
>
> ### Features:
>
> - Multiple isolated environments on a single host;
> - Preserve volume data when containers are created;
> - Only recreate containers that have changed;
> - Variables and moving a composition between environments;

### How it works? 🙄

1. Define your app’s environment with a `Dockerfile`;
2. Define the services that make up your app in `docker-compose.yml` so they can be run together in an isolated environment;
3. Run `docker-compose up` and Compose starts and runs your entire app;

## Project structure

---

```console
$ tree .
.
├── .editorconfig
├── .gitignore
├── .prettierignore
├── Makefile
├── docker-compose.prod.yml
├── docker-compose.yml
├── frontend
│   ├── .dockerignore
│   ├── Dockerfile
│   ├── package.json
│   └── src
│       ├── common
│       │   ├── robots.txt
│       │   └── sitemap.xml
│       ├── css
│       │   ├── reset.css
│       │   └── style.css
│       ├── html
│       │   └── index.html
│       ├── images
│       │   └── logo.png
│       └── js
│           └── index.js
└── webserver
    ├── nginx
    │   ├── default.conf
    │   ├── nginx.conf
    │   └── site.com.conf
    └── register_ssl.sh
```

---

### No time to read article, but want answers here and now? 🤔

No problem! I created repository with the project structure to be discussed in this article on my GitHub **especially** for you:

{% github koddr/example-static-website-docker-nginx-certbot no-readme %}

Just `git clone` and read instructions from `README`.

### Docker Compose configuration

Let's look to `docker-compose.yml` file. This is main file, which contain basic configuration for the containers:

```yaml
# ./docker-compose.yml

version: "3.7"

services:
  nginx:
    container_name: nginx
    image: nginx:alpine
    networks:
      - nginx_net
    volumes: # 💡
      - ./webserver/nginx/default.conf:/etc/nginx/conf.d/default.conf
      - ./webserver/nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./webserver/certbot/conf:/etc/letsencrypt
      - ./webserver/certbot/www:/var/www/certbot
    ports:
      - 80:80
    restart: unless-stopped
    command: /bin/sh -c "while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g 'daemon off;'" # 💡

  certbot:
    container_name: certbot
    image: certbot/certbot
    networks:
      - nginx_net
    volumes:
      - ./webserver/certbot/conf:/etc/letsencrypt
      - ./webserver/certbot/www:/var/www/certbot
    restart: unless-stopped
    entrypoint: /bin/sh -c "trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;" # 💡
    depends_on:
      - nginx

networks:
  nginx_net:
    name: nginx_net
```

💡 **It's useful to know:**

- Items in `volumes` directive should reads as follows:

`<local dir|file>`:`<container dir|file>`

For example, `./webserver/nginx/nginx.conf:/etc/nginx/nginx.conf` mean: copy `nginx.conf` file from local folder `./webserver/nginx` to container folder `/etc/nginx`.

> If you want to copy folder with all files, just specify volume like this: 
> `./my-project/folder:/var/www/folder`

- `command` directive for `nginx` container helps us to restarts Nginx every 6 hours and downloads new SSL certificates (if there are);
- `entrypoint` directive for `certbot` container helps us to checking every 12 hours to see if new SSL certificates are needed;

### Configuration for production environment

OK! Time to `docker-compose.prod.yml` 👌 

> It's override file with production environment (which might be stored in a different git repo or managed by a different team) [...] When you run `docker-compose up` it reads the overrides automatically.

```yaml
# ./docker-compose.prod.yml

version: "3.7"

services:
  frontend:
    container_name: frontend
    build:
      context: ./frontend
    volumes:
      - static:/frontend/build

  nginx:
    volumes:
      - static:/usr/share/nginx/html # 💡
      - ./webserver/nginx/default.conf:/etc/nginx/conf.d/default.conf
      - ./webserver/nginx/site.com.conf:/etc/nginx/sites-enabled/site.com.conf # ⚠️
      - ./webserver/nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./webserver/certbot/conf:/etc/letsencrypt
      - ./webserver/certbot/www:/var/www/certbot
    ports:
      - 80:80
      - 443:443
    depends_on:
      - frontend # 💡

volumes:
  static:
```

💡 **It's useful to know:**

- We're put all build files to `/usr/share/nginx/html/`, but you may choose any folder on your container;
- Good practice to start Nginx after build all frontend files, therefore we set `depends_on` directive with name of container, whose creation to wait for;

⚠️ **Don't forget:** 

- Change `site.com` to your domain (or project name);

![docker](https://thepracticaldev.s3.amazonaws.com/i/typs7u7loyu4u9fpvhcu.jpg)

## Nginx and Certbot

---

```console
$ tree ./webserver
.
├── nginx
│   ├── default.conf
│   ├── nginx.conf
│   └── site.com.conf
└── register_ssl.sh
```

---

> The script for obtaining and updating SSL certificates ([register_ssl.sh](https://github.com/koddr/example-static-website-docker-nginx-certbot/blob/master/webserver/register_ssl.sh)) is the most interesting. But I leave it to your own study (as homework).

For more understand, I separate Nginx configs to three files: main (`nginx.conf`), for get SSL (`default.conf`) and for a production domain (`site.com.conf`).

In order not to increase the already long article, I suggest that you read only the last two configs. Main Nginx configuration [see here](https://github.com/koddr/example-static-website-docker-nginx-certbot/blob/master/webserver/nginx/nginx.conf).

- Config for get SSL and redirect from HTTP to HTTPS (`default.conf`):

```conf
# ./webserver/nginx/default.conf

# Config for get SSL and redirect to HTTPS
server {
  listen      80;
  server_name .site.com;

  # Allow only for register SSL (Certbot)
  location ^~ /.well-known/acme-challenge { root /var/www/certbot; }

  # Redirect to HTTPS
  location / { return 301 https://site.com$request_uri; }
}
```

- Config for production domain (`site.com.conf`):

```conf
# ./webserver/nginx/site.com.conf

# Redirect to non-WWW
server {
  listen      443 ssl http2;
  server_name www.site.com;

  # SSL
  ssl_certificate     /etc/letsencrypt/live/site.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/site.com/privkey.pem;

  # Additional Nginx options
  include /etc/letsencrypt/options-ssl-nginx.conf;

  # Diffie-Hellman parameter for DHE ciphersuites
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

  # Redirect to HTTPS
  location / { return 301 https://site.com$request_uri; }
}

# Config for HTTPS
server {
  listen      443 ssl http2;
  server_name site.com;

  # Root & index.html
  root /usr/share/nginx/html;
  index index.html;

  # SSL
  ssl_certificate     /etc/letsencrypt/live/site.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/site.com/privkey.pem;

  # Additional Nginx options
  include /etc/letsencrypt/options-ssl-nginx.conf;

  # Diffie-Hellman parameter for DHE ciphersuites
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

  # Security headers
  add_header X-Frame-Options "SAMEORIGIN" always;
  add_header X-XSS-Protection "1; mode=block" always;
  add_header X-Content-Type-Options "nosniff" always;
  add_header Referrer-Policy "no-referrer-when-downgrade" always;
  add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

  # dot files
  location ~ /\.(?!well-known) { deny all; }

  # SEO files
  location = /robots.txt { log_not_found off; }
  location = /sitemap.xml { log_not_found off; }
  location = /favicon.ico { log_not_found off; }

  # Assets, media
  location ~* \.(?:css(\.map)?|js(\.map)?|jpe?g|png|gif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv)$ {
    expires 7d;
  }

  # SVG, fonts
  location ~* \.(?:svgz?|ttf|ttc|otf|eot|woff2?)$ {
    add_header Access-Control-Allow-Origin "*";
    expires 7d;
  }

  # Frontend files
  location / {
    try_files $uri $uri/ /index.html;
  }
}
```

⚠️ **Don't forget:** 

- Change `site.com` to your domain;

![frontend](https://thepracticaldev.s3.amazonaws.com/i/xsjpvbt25h2lnir6ah07.jpg)

## Frontend (static website)

---

```console
$ tree ./frontend
.
├── .dockerignore
├── Dockerfile
├── package.json
└── src
    ├── common
    │   ├── robots.txt
    │   └── sitemap.xml
    ├── css
    │   ├── reset.css
    │   └── style.css
    ├── html
    │   └── index.html
    ├── images
    │   └── logo.png
    └── js
        └── index.js
```

---

Let's dwell on some files in more detail. 

- List of ignored files and folders for excluding from container (`.dockerignore`):

```bash
# ./frontend/.dockerignore

# Files
.dockerignore
Dockerfile
*.log

# Folders
.cache/
node_modules/
build/
```

- Docker container instructions (`Dockerfile`):

```dockerfile
# ./frontend/Dockerfile

FROM node:alpine

LABEL maintainer="Your Name"

WORKDIR /frontend
COPY package*.json ./
RUN npm install --only=production
COPY . .
RUN npm run build:prod
```

- And finally, Node.js instructions & dependencies (`package.json`):

```js
// ./frontend/package.json

{
  "name": "frontend",
  "version": "1.0.0",
  "description": "Your project description.",
  "main": "./src/js/index.js",
  "scripts": {
    "build": "parcel build ./src/html/*.html -d ./build",
    "copy": "cp -R ./src/common/* ./build", // 💡
    "build:prod": "npm run build && npm run copy"
  },
  "author": "Your Name",
  "dependencies": {
    "parcel-bundler": "^1.12.4"
  }
}
```

💡 **It's useful to know:**

- `copy` command helps us to copy files (which not be into final bundle, but important too) from `./src/common` to `./build` folder;

## Push project to git

It's considered a **good practice** to store code in a Version Control System (VCS), like GitHub/Bitbucket/etc or your own, for example, [Gitea](https://gitea.io/en-us/).

So, following the best practices above:

✅ Create repository on your VCS;
✅ Add all changes to commit;
✅ Push commit to repository;

## Deploy to DigitalOcean

- Enter to your DO account;

> Don't have an account? Join DigitalOcean by my **[referral link](https://m.do.co/c/b41859fa9b6e)** (your profit is **$100** and I get $25). This is my bonus for you! 😉

- Click to green button "_Create_" on top and choose "_Droplets_":

![DigitalOcean Deploy 1](https://thepracticaldev.s3.amazonaws.com/i/ia04jujfrpyitpm8x8le.png)

- Choose "_Marketplace_" tab and then "_Docker_":

![DigitalOcean Deploy 2](https://thepracticaldev.s3.amazonaws.com/i/f895tna4iqzlwyaaf1zy.png)

- Scroll down, choose plan, storage, additional options and datacenter region (any, by your desire);
- OK, scroll to "_Authentication_" section and click to "_New SSH key_":

![DigitalOcean Deploy 3](https://thepracticaldev.s3.amazonaws.com/i/vi5y0nubf4lxsxj318tm.png)

> ☝️ **Tip:** I recommend to create new SSH key for each new droplet, because it's more secure, than use same key for every droplets!

- Follow instruction (on right), generate new SSH key and fill form:

![DigitalOcean Deploy 4](https://thepracticaldev.s3.amazonaws.com/i/z7k6blggq4c7uhw8k5a6.png)

- Re-check droplet's options and click to "_Create Droplet_" on bottom 👍
- Next, go to "_Droplets_" list and add your domain:

![DigitalOcean Deploy 5](https://thepracticaldev.s3.amazonaws.com/i/oqfl1gziw12d410ltnfq.png)

- Type domain name and choose droplet:

![DigitalOcean Deploy 6](https://thepracticaldev.s3.amazonaws.com/i/u6hue2oe8qo0fdia2dmx.png)

- Add two "_A_" records for domain ("_@_" and "_www_"):

![DigitalOcean Deploy 7](https://thepracticaldev.s3.amazonaws.com/i/afw0q2n4znofr5skidzl.png)

- Connect via SSH to your droplet:

```console
$ ssh root@<droplet IP>
```

- Clone your repository and go to project's folder:

```console
$ git clone https://github.com/user/project-name.git
$ cd project-name
```

- Check configuration of Certbot by start the process of obtaining SSL certificate in `test mode`:

```console
$ make certbot-test DOMAINS="site.com www.site.com" EMAIL=mail@site.com
```

> Specify `DOMAINS` variable with your domains (WWW and non-WWW).

- If you see `Congratulations!`, start the process of obtaining SSL in `production mode`:

```console
$ make certbot-prod DOMAINS="site.com www.site.com" EMAIL=mail@site.com
```

- And now, check Nginx and frontend configuration:

```console
$ make deploy-test
```

- No errors in console? Your static website is ready to production:

```console
$ make deploy-prod
```

**That's all!** We're dockerized static website with Nginx + Certbot and deployed them to DigitalOcean! 🎉

## Photo by

[Title] chuttersnap https://unsplash.com/photos/9cCeS9Sg6nU
[1] Jeffrey Blum https://unsplash.com/photos/FQ06bmigBqg
[2] John Barkiple https://unsplash.com/photos/l090uFWoPaI

<a name=""></a>
## P.S.

If you want more articles (like this) on this blog, then post a comment below and subscribe to me. Thanks! 😻

And of course, you can help me make developers' lives even better! Just connect to one of my projects as a contributor. It's easy!

My projects that need your help (and stars) 👇

- 🔥 [gowebly][gowebly_url]: A next-generation CLI tool for easily build amazing web applications with Go on the backend, using htmx & hyperscript and the most popular atomic/utility-first CSS frameworks on the frontend.
- ✨ [create-go-app][cgapp_url]: Create a new production-ready project with Go backend, frontend and deploy automation by running one CLI command.
- 🏃 [yatr][yatr_url]: Yet Another Task Runner allows you to organize and automate your routine operations that you normally do in Makefile (or else) for each project.
- 📚 [gosl][gosl_url]: The Go Snippet Library provides snippets collection for working with routine operations in your Go programs with a super user-friendly API and the most efficient performance.
- 🏄‍♂️ [csv2api][csv2api_url]: The parser reads the CSV file with the raw data, filters the records, identifies fields to be changed, and sends a request to update the data to the specified endpoint of your REST API.
- 🚴 [json2csv][json2csv_url]: The parser can read given folder with JSON files, filtering and qualifying input data with intent & stop words dictionaries and save results to CSV files by given chunk size.

<!-- Footer links -->

[gowebly_url]: https://github.com/gowebly
[cgapp_url]: https://github.com/create-go-app
[yatr_url]: https://github.com/koddr/yatr
[gosl_url]: https://github.com/koddr/gosl
[csv2api_url]: https://github.com/koddr/csv2api
[json2csv_url]: https://github.com/koddr/json2csv