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:
- Quick meet with Docker Compose (not "deep", but close);
- Configure Docker containers for Nginx, Certbot and frontend;
- Write simple static website (using
Parcel.js
as bundler); - Push finished project to git repository;
- 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? π
- Define your appβs environment with a
Dockerfile
; - Define the services that make up your app in
docker-compose.yml
so they can be run together in an isolated environment; - 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:
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 fornginx
container helps us to restarts Nginx every 6 hours and downloads new SSL certificates (if there are); -
entrypoint
directive forcertbot
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);
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 (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":
- Choose "Marketplace" tab and then "Docker":
- 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":
βοΈ 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:
- Re-check droplet's options and click to "Create Droplet" on bottom π
- Next, go to "Droplets" list and add your domain:
- Type domain name and choose droplet:
- Add two "A" records for domain ("@" and "www"):
- 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 inproduction 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! π
Discussion
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 :)
Frontend Development with Docker simplified
GΓ‘bor SoΓ³s γ» Jan 16 γ» 4 min read
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!