DEV Community

Cover image for 🐳 How to dockerize your static website with Nginx, automatic renew SSL for domain by Certbot and deploy it to DigitalOcean?
Vic Shóstak
Vic Shóstak

Posted on • Updated on

🐳 How to dockerize your static website with Nginx, automatic renew SSL for domain by Certbot and deploy it to DigitalOcean?

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 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:

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


$ 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
Enter fullscreen mode Exit fullscreen mode

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 logo koddr / example-static-website-docker-nginx-certbot

Example static website with Docker, Nginx and Certbot

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:

# ./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
Enter fullscreen mode Exit fullscreen mode

💡 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.

# ./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:
Enter fullscreen mode Exit fullscreen mode

💡 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

Nginx and Certbot


$ tree ./webserver
.
├── nginx
│   ├── default.conf
│   ├── nginx.conf
│   └── site.com.conf
└── register_ssl.sh
Enter fullscreen mode Exit fullscreen mode

The script for obtaining and updating SSL certificates (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.

  • Config for get SSL and redirect from HTTP to HTTPS (default.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; }
}
Enter fullscreen mode Exit fullscreen mode
  • Config for production domain (site.com.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;
  }
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Don't forget:

  • Change site.com to your domain;

frontend

Frontend (static website)


$ 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
Enter fullscreen mode Exit fullscreen mode

Let's dwell on some files in more detail.

  • List of ignored files and folders for excluding from container (.dockerignore):
# ./frontend/.dockerignore

# Files
.dockerignore
Dockerfile
*.log

# Folders
.cache/
node_modules/
build/
Enter fullscreen mode Exit fullscreen mode
  • Docker container instructions (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
Enter fullscreen mode Exit fullscreen mode
  • And finally, Node.js instructions & dependencies (package.json):
// ./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"
  }
}
Enter fullscreen mode Exit fullscreen mode

💡 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.

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 (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

  • Choose "Marketplace" tab and then "Docker":

DigitalOcean Deploy 2

  • 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

☝️ 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

  • Re-check droplet's options and click to "Create Droplet" on bottom 👍
  • Next, go to "Droplets" list and add your domain:

DigitalOcean Deploy 5

  • Type domain name and choose droplet:

DigitalOcean Deploy 6

  • Add two "A" records for domain ("@" and "www"):

DigitalOcean Deploy 7

  • Connect via SSH to your droplet:
$ ssh root@<droplet IP>
Enter fullscreen mode Exit fullscreen mode
  • Clone your repository and go to project's folder:
$ git clone https://github.com/user/project-name.git
$ cd project-name
Enter fullscreen mode Exit fullscreen mode
  • Check configuration of Certbot by start the process of obtaining SSL certificate in test mode:
$ make certbot-test DOMAINS="site.com www.site.com" EMAIL=mail@site.com
Enter fullscreen mode Exit fullscreen mode

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

  • If you see Congratulations!, start the process of obtaining SSL in production mode:
$ make certbot-prod DOMAINS="site.com www.site.com" EMAIL=mail@site.com
Enter fullscreen mode Exit fullscreen mode
  • And now, check Nginx and frontend configuration:
$ make deploy-test
Enter fullscreen mode Exit fullscreen mode
  • No errors in console? Your static website is ready to production:
$ make deploy-prod
Enter fullscreen mode Exit fullscreen mode

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

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: 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: Create a new production-ready project with Go backend, frontend and deploy automation by running one CLI command.
  • 🏃 yatr: 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: 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: 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: 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.

Oldest comments (6)

Collapse
 
acventor profile image
Roman Kosov

Thanks, very informative!

Collapse
 
espoir profile image
Espoir Murhabazi

Thanks for this, adding an SSL certificate to docker is always a trouble and gives me headaches and nightmares, let give this a try

Collapse
 
koddr profile image
Vic Shóstak

Oh, yeah.. I know, this is huge trouble at every project :D hope it helps!

Collapse
 
sonicoder profile image
Gábor Soós

Useful one with the SSL setup!

The only thing I can't get over is that is this complexity really necessary? My setup for frontend development seems like hammer compared to this :)

Collapse
 
aimanzaheb profile image
aimanzaheb

If we are restarting nginx every 6 hours.. How will we keep our server 24 hrs up? Can't we update ssl certificate without restarting server?

Collapse
 
koddr profile image
Vic Shóstak

Hi, thanks for reply!

We are only restarting Nginx, not stopping him and starting again. For users (clients, which already connected) it goes unnoticed.