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;