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

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

koddr profile image Vic ShΓ³stak Updated on ・7 min read

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

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

πŸ’‘ 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:

πŸ’‘ 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

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; }
}
  • 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;
  }
}

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

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/
  • 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
  • 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"
  }
}

πŸ’‘ 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>
  • Clone your repository and go to project's folder:
$ 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:
$ 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:
$ make certbot-prod DOMAINS="site.com www.site.com" EMAIL=mail@site.com
  • And now, check Nginx and frontend configuration:
$ make deploy-test
  • No errors in console? Your static website is ready to production:
$ 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

P.S.

If you want more β€” write a comment below & follow me. Thx! 😘

Posted on by:

koddr profile

Vic ShΓ³stak

@koddr

Hey! πŸ‘‹ I'm founder and full stack web developer (Go, JavaScript, Docker & automation) at True web artisans. Golang lover, UX evangelist, DX philosopher & UI Dreamer with over 12+ years of experience.

Discussion

pic
Editor guide
 

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

 

Thanks, very informative!

 

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

 

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