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 articles (like this) on this blog, then post a comment below and subscribe to me. Thanks! 😻
❗️ You can support me on Boosty, both on a permanent and on a one-time basis. All proceeds from this way will go to support my OSS projects and will energize me to create new products and articles for the community.
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 main projects that need your help (and stars) 👇
- 🔥 gowebly: A next-generation CLI tool that makes it easy to create amazing web applications with Go on the backend, using htmx, hyperscript or Alpine.js and the most popular 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.
Top comments (6)
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
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?
Hi, thanks for reply!
We are only restarting Nginx, not stopping him and starting again. For users (clients, which already connected) it goes unnoticed.
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!