Setting up AWS Lightsail

Some notes for setting up my new AWS Lightsail instance with a Next.js + prisma + postgreSQL app.

Creating the instance

Im assuming that I can change the instance size later so for now Ill start with the 5$ per month! The Apps+OS blueprints doesn’t really cover my use case so I’ll start with a centOS 8 instance. (Considered the amazon linux 2 OS for easier integration with AWS services, but I dont want to get locked into the AWS environment too much, and centos 8 has a longer support period.

Initial setup of the server

When the instance is setup, ssh into it.

1ssh -i .ssh/<private-key>.pem centos@<ip-address>

Install zsh, Oh-my-zsh, and vim

I can’t survive without zsh, Oh-my-zsh, and vim so lets install it and set it up to be the default shell.

 1sudo yum install zsh -y
 2sudo dnf install util-linux-user
 3sudo chsh -s /bin/zsh centos
 4sudo chsh -s /bin/zsh root
 5sudo yum install wget git -y
 6wget https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | zsh
 7
 8/bin/cp ~/.oh-my-zsh/templates/zshrc.zsh-template ~/.zshrc
 9source ~/.zshrc
10
11# Install Vim
12sudo yum install vim-enhanced

Install Docker and Docker-compose

 1# Install docker
 2curl -sSL https://get.docker.com | sh
 3
 4# Don't require sudo for docker commands
 5sudo usermod -aG docker centos
 6
 7# Restart the docker service
 8sudo systemctl restart docker
 9
10# Install docker-compose
11sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
12sudo chmod +x /usr/local/bin/docker-compose
13sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
14

Create some folder and add a build script

Create a folder for the app and a folder to hold scripts that we will run from github actions.

1mkdir app

Clone down the apps repository

In order to clone down the app we have to setup the github ssh key.

I also like adding the key o the the ssh config

1vim ~/.ssh/config
2
3Host github.com gist.github.com
4        User <username>
5        Hostname github.com
6        PreferredAuthentications publickey
7        IdentityFile ~/.ssh/<key-name>
8

And set the permission for the config file if it didn’t exist

1chmod 600 ~/.ssh/config

We’ll run it from the app folder so after repository in the git clone command add app/ (also don’t forget to clone it with ssh!!)

1git clone <repository> app/

Docker and docker-compose

Docker-compose

The docker-compose for production will be somewhat similar to the development one. We change the nginx image to nginx-certbot which is an “Almost fully autonomous Nginx server using Let’s Encrypt to get SSL certificates.” And we also change the DB credentials to something super secret that we will put in the .env file.

 1version: "3.8"
 2
 3services:
 4  postgres:
 5    image: postgres:14.1
 6    container_name: postgres
 7    env_file:
 8      - .env
 9    logging:
10      options:
11        max-size: 10m
12        max-file: "3"
13    ports:
14      - "5432:5432"
15    volumes:
16      - ./postgres-data:/var/lib/postgresql/data
17  app:
18    container_name: app
19    restart: on-failure
20    build:
21      context: .
22      dockerfile: dockerfile.prod
23    volumes:
24      - ./src:/app/src
25    ports:
26      - "3000:3000"
27    env_file:
28      - .env
29    depends_on:
30      - postgres
31  nginx:
32    restart: unless-stopped
33    image: jonasal/nginx-certbot:latest
34    env_file:
35      - ./.env.nginx
36    volumes:
37      - nginx_secrets:/etc/letsencrypt
38      - ./nginx/user_conf.d:/etc/nginx/user_conf.d
39    ports:
40      - "80:80"
41      - "443:443"
42
43volumes:
44  nginx_secrets:
45

Dockerfile

For the Dockerfile we’ll create a Dockerfile.prod and add the recommended dockerfile from vercel. Because of lack of resources on the lightsail servers we will build it on github actions and send up the built files. So in the Dockerfile we will only install and copy over files.

 1# Install dependencies only when needed
 2FROM node:14-alpine AS deps
 3# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
 4RUN apk add --no-cache libc6-compat
 5WORKDIR /app
 6COPY package.json package-lock.json prisma/ ./
 7RUN npm install --frozen-lockfile
 8RUN rm -r node_modules/@next/swc-linux-x64-gnu
 9RUN npx prisma generate
10
11# Production image, copy all the files and run next
12FROM node:14-alpine AS runner
13WORKDIR /app
14
15ENV NODE_ENV production
16
17# You only need to copy next.config.js if you are NOT using the default configuration
18COPY next.config.js ./
19COPY public/ ./public
20COPY .next/ ./.next
21COPY --from=deps /app/node_modules ./node_modules
22COPY package.json ./package.json
23
24EXPOSE 3000
25
26ENV PORT 3000
27
28# Next.js collects completely anonymous telemetry data about general usage.
29# Learn more here: https://nextjs.org/telemetry
30# Uncomment the following line in case you want to disable telemetry.
31ENV NEXT_TELEMETRY_DISABLED 1
32
33CMD ["node_modules/.bin/next", "start"]

Nginx config

I previously created a nginx folder in the root of the repository to be the config for the development docker-compose environtment. In that same folder create a new file called nginx.conf inside ./nginx/user_conf.d/.

 1# Redirect all http traffic to https
 2upstream webapp {
 3    server app:3000;
 4}
 5
 6server {
 7    # Listen to port 443 on both IPv4 and IPv6.
 8    listen 443 ssl default_server reuseport;
 9    listen [::]:443 ssl default_server reuseport;
10
11    # Domain names this server should respond to.
12    server_name viviewd.com;
13
14    # Load the certificate files.
15    ssl_certificate         /etc/letsencrypt/live/viviewd/fullchain.pem;
16    ssl_certificate_key     /etc/letsencrypt/live/viviewd/privkey.pem;
17    ssl_trusted_certificate /etc/letsencrypt/live/viviewd/chain.pem;
18    
19    location / {
20        proxy_pass http://webapp;
21        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
22        proxy_set_header Host $host;
23        proxy_redirect off;
24    }
25}
26

Environment files

Lets also set up our environment files. We’ll need a .env file to handle envronment variabled for the application. And also a .env.nginx that will hold the variables for the certbot service. Keep STAGING as 1 untill you made sure that your DNS is setup correctly, and later change it to 0 when you are sure everything is working correctly.

1DATABASE_URL="postgresql://postgres:postgres@postgres:5432/mydb?schema=public"
2POSTGRES_USER=postgres
3POSTGRES_PASSWORD=postgres

Test the environment first with STAGING=1 when it’s accessible and its all good change STAGING to 0.

 1# Required
 2CERTBOT_EMAIL=paru@example.com
 3
 4# Optional (Defaults)
 5STAGING=1
 6DHPARAM_SIZE=2048
 7RSA_KEY_SIZE=2048
 8ELLIPTIC_CURVE=secp256r1
 9USE_ECDSA=0
10RENEWAL_INTERVAL=8d
11

Add a swap file

Since the instance has very low ram memory, we’ll add a swap file to help out.

Setting up github actions

Since our servers resources arent enough to run the next apps build, we create a github action workflow that will build and send the .next folder up to the server.

 1# This is a basic workflow to help you get started with Actions
 2
 3name: CD
 4
 5# Controls when the workflow will run
 6on:
 7  # Triggers the workflow on push events but only for the main branch
 8  push:
 9    branches: [ main ]
10
11  # Allows you to run this workflow manually from the Actions tab
12  workflow_dispatch:
13
14# A workflow run is made up of one or more jobs that can run sequentially or in parallel
15jobs:
16  # This workflow contains a single job called "build"
17  build:
18    # The type of runner that the job will run on
19    runs-on: ubuntu-latest
20
21    # Steps represent a sequence of tasks that will be executed as part of the job
22    steps:
23      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
24      - uses: actions/checkout@v2
25
26      - uses: actions/setup-node@v2.5.1
27        with:
28          node-version: 14
29      - run: npm install -g yarn
30      - run: npm install --frozen-lockfile
31      - run: yarn build
32      
33      - name: Copy build files via ssh
34        uses: appleboy/scp-action@master
35        with:
36          host: ${{ secrets.SSH_AWS_SERVER_IP }}
37          username: ${{ secrets.SSH_SERVER_USER }}
38          passphrase: ${{ secrets.SSH_SERVER_PASSPHRASE }}
39          key: ${{ secrets.SSH_PRIVATE_KEY }}
40          source: ".next"
41          target: "app/"

Deploy script to restart docker-compose

And lets create a deploy script that will pull changes from the repo, build the docker image, and run the docker-compose command.

1vim .scripts/docker-deploy.sh
2# Change permissions to allow execute
3sudo chmod +x docker-deploy.sh
 1#!/usr/bin/env bash
 2
 3TARGET='main'
 4
 5cd ~/app || exit
 6
 7ACTION='\033[1;90m'
 8NOCOLOR='\033[0m'
 9
10# Checking if we are on the main branch
11
12echo -e ${ACTION}Checking Git repo
13BRANCH=$(git rev-parse --abbrev-ref HEAD)
14if [ "$BRANCH" != ${TARGET} ]
15then
16  exit 0
17fi
18
19# Checking if the repository is up to date.
20
21git fetch
22HEADHASH=$(git rev-parse HEAD)
23UPSTREAMHASH=$(git rev-parse ${TARGET}@{upstream})
24
25if [ "$HEADHASH" == "$UPSTREAMHASH" ]
26then
27  echo -e "${FINISHED}"Current branch is up to date with origin/${TARGET}."${NOCOLOR}"
28  exit 0
29fi
30
31# If that's not the case, we pull the latest changes and we build a new image
32
33git pull origin main;
34
35# Docker
36
37docker-compose -f docker-compose.prod.yml up -d --build
38
39exit 0;