Setup blog
This is the sharing process, that I do the blog setup, will detail as much as possible in writing.
Prerequisite
Firstly, you should order a VPS, you can use any trusted vps out there like digital ocean or aws... I'm having a vps running on my country server (Viet Nam). Once you have your server running on the vps, make sure these libs/packages are installed:
- Docker
- Docker compose
- Git
- A web blog repository
Why?
Why Docker/Docker compose?
With Docker, the reason i'm using the Docker is because it help me to move the configuration easily whenever i want to change or re-setup the vps. All I need to do is writing the script that help me to automate the docker setup for the website, so I can bring that configuration anywhere for later use without messing the environment.
With Docker compose, due to the multiple apps/projects that need for https/ssl and also the environment request setup, we will configure multiple services with docker compose.
Why Git?
In this guide, we'll use the github actions to automate the cloning and building the project, so Git here is for the use of the cloning.
Make sure all the packages are installed by running the command:
Why repository?
Simple, it is a thing you want it over the internet, in my case, it is a Next.Js project due to its built-in support for SEO and other useful features that help me to set up easily
git --version
docker compose version
docker --v
The expected output should be:
git version 2.43.0
Docker Compose version v2.35.1
Docker version 28.1.1, build 4eba377
The version in this blog might be different as the time I'm writing.
Actions
Suppose, your project repo will be something like this:
|___ blog
| |___ src
| |___ package.json
| |___ tsconfig.json
| |___ app
| |___ page.tsx
| |___ ...
Set up the Docker file
Firstly, we need to set up a Dockerfile which is helpful for creating the images and containers for the project.
# ---------- Build Stage ----------
FROM node:18-alpine AS builder
# Install pnpm
RUN npm install -g pnpm
# Set working directory
WORKDIR /app
# Copy dependency definitions
COPY package.json pnpm-lock.yaml ./
# Install all dependencies (including dev)
RUN pnpm install
# Copy all source code
COPY . .
# Build the Next.js app
RUN pnpm build
# ---------- Production Stage ----------
FROM node:18-alpine AS runner
# Install pnpm and pm2 globally
RUN npm install -g pnpm pm2
# Set working directory
WORKDIR /app
# Copy only production dependencies
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --prod
# Copy built app and necessary runtime files from builder
COPY --from=builder /app/.next ./.next
# COPY --from=builder /app/public ./public
COPY --from=builder /app/next.config.mjs ./next.config.mjs
COPY --from=builder /app/package.json ./package.json
# Expose port 9000
EXPOSE 9000
# Set the environment variable for the port
ENV PORT=9000
# Start the app using pm2-runtime
CMD ["pm2-runtime", "start", "pnpm", "--", "start"]
Put this .Dockerfile to the blog directory:
|___ blog
| |___ src
| |___ package.json
| |___ tsconfig.json
| |___ app
| |___ page.tsx
| |___ ...
| Dockerfile <-- this
If you haven't well-known with the Docker syntax, you can take a look at this link to have a better understanding the syntax sugar, my Dockerfile's content is just a set of simple commands that automate the build and run the blog app.
Set up the Docker compose
The docker compose is a set of scripts that support to run the Docker scripts creating and managing multiple services for the Docker, you can see it as a set of the Docker containers, instead of creating and managing multiple Dockerfile files for multiple projects/libs, you can use the docker compose as the superset one in single script.
Put this docker-compose.yml to the blog directory:
|___ blog
| |___ src
| |___ package.json
| |___ tsconfig.json
| |___ app
| |___ page.tsx
| |___ ...
| Dockerfile
| docker-compose.yml <--this
And update the file to:
version: '3.8'
services:
app:
build:
context: . # Build the Next.js app from the current directory
container_name: nextjs-app # Name of the container
expose:
- '9000' # Exposes port 9000 to the other containers, but does not expose it to the host
networks:
- my_network # Ensures both containers are on the same Docker network (can be defined later)
nginx:
image: nginx:alpine # Using the official Nginx image
container_name: nginx # Name of the container
ports:
- '80:80' # Exposes HTTP (port 80) to the host machine
- '443:443' # Exposes HTTPS (port 443) to the host machine
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/ssl.conf:/etc/nginx/conf.d/ssl.conf:ro # this one is mounted only after certs exist
- letsencrypt:/etc/letsencrypt
- webroot:/var/www/html
depends_on:
- app # Ensures that the Nginx container starts after the app container
networks:
- my_network # Ensures both containers are on the same Docker network (can be defined later)
certbot:
image: certbot/certbot
container_name: certbot
volumes:
- letsencrypt:/etc/letsencrypt
- webroot:/var/www/html
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --quiet && nginx -s reload; sleep 12h; done;'"
depends_on:
- nginx
networks:
- my_network
volumes:
letsencrypt:
webroot:
networks:
my_network: # This ensures that both the app and Nginx containers are connected to the same network
driver: bridge
Be careful with spacing, the docker compose treats the spacing with respect ident that represents for each level of the script.
Talk briefly about the docker compose file, we have three services:
- app
This is the service that run our main blog app, which is running on 9000 as the configured (you can choose any port you like), the context . here is indicating to the Dockerfile
above which will run the Dockerfile in the container named nextjs-app
- nginx
We use the nginx:alpine
image from the Docker hub to install the container named nginx
which will be mainly using for reversed proxy, which will firstly receive any request as the first stage before getting into to the main app. We'll mount couple of files and folder to the shared host locations (vps in this case).
This nginx depends on the app
to be running first.
And both of them will use the same network named my_network
for traffic routing and networking firewall rules.
- certbot
This one mainly uses to creating/extending the SSL certificate files, which will ensure our request is secured with HTTPS/TLS. The entry point is a script that run every 12 hours to renew the certificate to prevent the expiration issue. Also because this service is mainly used with nginx for the the traffic, so it is required the
nginx
to be running before them.
Basically, we'll later generate the certificate at the first time, and map the files for later use to connect with nginx. I also give explanation to each line of the script to make you more understand about the process.
Set up the Nginx
We'll configure the nginx files inside of the nginx
folder which will basically create two conf
files that configure both http and routing to the secured https connection.
-
Http connection
Create the
default.conf
which is used for the default configuration setting one, it will indicate the servername and also the certificate location.Put this default.conf to the blog/nginx directory:
|___ blog | |___ src | |___ package.json | |___ tsconfig.json | |___ app | |___ page.tsx | |___ ... | |___ nginx | |___ default.conf <--this | Dockerfile | docker-compose.yml
And the content:
server { listen 80; server_name your_domain www.your_domain; # Serve Certbot verification files location /.well-known/acme-challenge/ { root /var/www/html; try_files $uri $uri/ =404; } # Redirect all other HTTP traffic to HTTPS (after certificates are obtained) location / { return 301 https://$host$request_uri; } }
The
your_domain
should be your real domain, in my case, I purchased a Namecheap domain. Don't forget to set up the Namecheap domain your real public VPS IP address. Follow the link for configuration. -
Https connection: Create the
ssl.conf
which is used for the secured configuration setting, it will indicate the servername and also the certificate location.Put this ssl.conf to the blog/nginx directory:
|___ blog | |___ src | |___ package.json | |___ tsconfig.json | |___ app | |___ page.tsx | |___ ... | |___ nginx | |___ default.conf | |___ ssl.conf <--this | Dockerfile | docker-compose.yml
And the content:
# ssl.conf (only copy this after certs are available) server { listen 443 ssl; server_name your_domain www.your_domain; ssl_certificate /etc/letsencrypt/live/your_domain/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your_domain/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; location / { proxy_pass http://app:9000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }
Setup Github workflows
My git client is Github
, so I'm using the Github workflows actions for auto deploying the process. You can do any other cloud actions, or more advanced with Jenkins, in my case, I simplized the process with Github workflows.
Create the docker-build.yml
and put it inside of the .github/workflows
folder:
|___ blog
| |__ .github/workflows
| |__ docker-build.yml <--this
| |___ src
| |___ package.json
| |___ tsconfig.json
| |___ app
| |___ page.tsx
| |___ ...
| |___ nginx
| |___ default.conf
| |___ ssl.conf
| Dockerfile
| docker-compose.yml
And update the content to:
name: Deploy to VPS
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v0.1.5
with:
host: ${{ secrets.VPS_IP }}
username: ${{ secrets.VPS_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
port: 22
script: |
PROJECT_DIR="/home/${{ secrets.VPS_USERNAME }}/projects/blog"
REPO_URL="your_repo_link"
DOMAIN="your_domain"
EMAIL="your_email"
echo "=== Cleaning up existing project ==="
rm -rf "$PROJECT_DIR"
echo "=== Cloning latest code ==="
git clone "$REPO_URL" "$PROJECT_DIR"
cd "$PROJECT_DIR"
echo "=== Starting Nginx and App (HTTP only) ==="
docker compose down || true
docker compose up --build -d nginx app
echo "=== Waiting for Nginx to boot ==="
sleep 5
echo "=== Generating SSL certificates via Certbot ==="
docker compose run --rm --entrypoint "" certbot certbot certonly \
--webroot -w /var/www/html \
-d "$DOMAIN" -d "www.$DOMAIN" \
--email "$EMAIL" \
--agree-tos --no-eff-email \
--force-renewal
echo "=== Reloading Nginx with HTTPS ==="
docker compose exec -T nginx nginx -s reload || true
echo "=== Restarting all services to finalize ==="
docker compose restart
Basically, the simple things are done in the github action, all they need to do is about cloning, and setting up the project with docker compose as I said before. By printing out the step by step comments, hope you understand clearly about the process. Don't forget to update your own credentials information.
Stay tune until I'm up to a new blog.