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?

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


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.


  • 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.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
    β”‚   └──

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"

    container_name: nginx
    image: nginx:alpine
      - 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
      - 80:80
    restart: unless-stopped
    command: /bin/sh -c "while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g 'daemon off;'" # πŸ’‘

    container_name: certbot
    image: certbot/certbot
      - nginx_net
      - ./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;" # πŸ’‘
      - nginx

    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:

  • 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 πŸ‘Œ

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.

# ./

version: "3.7"

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

      - static:/usr/share/nginx/html # πŸ’‘
      - ./webserver/nginx/default.conf:/etc/nginx/conf.d/default.conf
      - ./webserver/nginx/ # ⚠️
      - ./webserver/nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./webserver/certbot/conf:/etc/letsencrypt
      - ./webserver/certbot/www:/var/www/certbot
      - 80:80
      - 443:443
      - frontend # πŸ’‘


πŸ’‘ 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 to your domain (or project name);


Nginx and Certbot

$ tree ./webserver
β”œβ”€β”€ nginx
β”‚   β”œβ”€β”€ default.conf
β”‚   β”œβ”€β”€ nginx.conf
β”‚   └──

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

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;

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

  # Redirect to HTTPS
  location / { return 301$request_uri; }
  • Config for production domain (
# ./webserver/nginx/

# Redirect to non-WWW
server {
  listen      443 ssl http2;

  # SSL
  ssl_certificate     /etc/letsencrypt/live/;
  ssl_certificate_key /etc/letsencrypt/live/;

  # 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$request_uri; }

# Config for HTTPS
server {
  listen      443 ssl http2;

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

  # SSL
  ssl_certificate     /etc/letsencrypt/live/;
  ssl_certificate_key /etc/letsencrypt/live/;

  # 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 to your domain;


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

# Folders
  • 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
$ cd project-name
  • Check configuration of Certbot by start the process of obtaining SSL certificate in test mode:
$ make certbot-test DOMAINS=""

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=""
  • 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
[1] Jeffrey Blum
[2] John Barkiple


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


Editor guide
blacksonic 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 :)

acventor profile image

Thanks, very informative!

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

koddr profile image
Vic ShΓ³stak Author

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