Deploy a Full Stack Next.js App to a DigitalOcean VPS With Docker
Tags: nextjs, digialocean, docker, webdev
Published: 2023-10-06 19:46:25

## Project Info

In this tutorial, we will be creating a simple full stack note taking app using:

- Next.js + Tailwind
- Drizzle ORM
- PostgreSQL
- DBeaver
- DigitalOcean
- Docker

While the app itself is very simple, the main focus here is to learn how to:

- Setup a PostgreSQL database locally
- Deploy the app to a DigitalOcean VPS using Docker containers

The functionality of the app is:

- Have a page to view all notes
- On the same page, have form to create a note using server actions

So, not full CRUD, and no auth.

It’s a great pipeline to know how to navigate, but there will be some cost involved. I’ll try to keep costs as low as I can.

Let’s get started!

## Setting Up PostgreSQL & DBeaver

Download page PostgreSQL [**here**](https://www.postgresql.org/download/)

Download page for DBeaver [**here**](https://dbeaver.io/download/)

I am going to provide the steps I took to get PostgreSQL with DBeaver up and running locally, you might need to Google for your specific OS (I am on Ubuntu).

First, we need to install PostgreSQL:

```bash
sudo apt update
sudo apt install postgresql postgresql-contrib
```

You can check the installation was successful by running:

```bash
psql --version
# psql (PostgreSQL) 16.0 (Ubuntu 16.0-1.pgdg22.04+1)
```

During the installation, PostgreSQL will create a default user called `postgres`. You can set a password for this user by running this command:

```bash
sudo -u postgres psql postgres
```

You’ll enter the postgres console. Once there, enter:

```bash
\password postgres
```

Since this is just a local database to play around with, I'm not too concerned about security at this point. So for the local db, the username, password, and database name will all be `postgres`.

Enter a password, then enter this to quit the psql console:

```bash
\q
# or 
exit
```

I will be using DBeaver as my database admin GUI of choice. It’s free, and available on all 3 major OS’s(Windows, Mac, & Linux). If you already have a tool you prefer to use, then of course stick with that.

Open DBeaver, and in the top menu bar, select **Database**, then **New Database Connection**

Settings:

- Connect by **Host**
- URL, Host, and Port can all be left alone
- Database, username, and password should all be **postgres**

Click **Test Connection**

If you see a response like this:

![DBeaver Local Postgres Connection Test](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g8zrxl9su0i7afpbp8kx.png)

You’re good to go!

**FYI**: If you did not set a password for the default `postgres` user, the connection test would always fail.

Click **Ok**, then click **Finish**

## Setup Next.js App

Then in a folder of your choice, download and setup the latest Next.js TypeScript starter:

```bash
npx create-next-app@latest --ts .
```

If you are getting warnings in your CSS file complaining about unknown CSS rules, follow **[these steps here](https://github.com/tailwindlabs/tailwindcss/discussions/5258#discussioncomment-1979394)**.

Still in `globals.css`, update the code with this reset from **[Josh Comeau](https://www.joshwcomeau.com/css/custom-css-reset/)**.

```css
/* src/app/globals.css */

@tailwind base;
@tailwind components;
@tailwind utilities;

*, *::before, *::after {
  box-sizing: border-box;
}

* {
  margin: 0;
	padding: 0;
}

body {
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

img, picture, video, canvas, svg {
  display: block;
  max-width: 100%;
}

input, button, textarea, select {
  font: inherit;
}

p, h1, h2, h3, h4, h5, h6 {
  overflow-wrap: break-word;
}

#root, #__next {
  isolation: isolate;
}
```

Update `tsconfig.json` to this:

```json
{
  "compilerOptions": {
    "target": "esnext",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}
```

Update `src/app/page.tsx` to this:

```tsx
// src/app/page.tsx

const HomePage = () => {
  return (
    <div>
      <h1>HomePage</h1>
    </div>
  );
};

export default HomePage;
```

Update `src/app/layout.tsx` to this:

```tsx
import { Inter } from "next/font/google";

import type { Metadata } from "next";
import type { ReactNode } from "react";

import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  description: "Generated by create next app",
  title: "VPS Demo"
};

type RootLayoutProps = {
  children: ReactNode;
};

const RootLayout = ({ children }: RootLayoutProps) => {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  );
};

export default RootLayout;
```

## Setup GitHub Repo

I’m a fan of setting up a GitHub repo early and committing + pushing often in smaller chunks.

I’m going to assume that you know how to do that, so please do so before continuing.

## Setup Drizzle

Link to Drizzle ORM docs [**here**](https://orm.drizzle.team/docs/overview)

Install the necessary packages:

```bash
npm i drizzle-orm postgres dotenv
npm i -D drizzle-kit
```

At the root level of the project, create a file called `drizzle.config.ts`

Add this code to it:

```tsx
// drizzle.config.ts

import dotenv from "dotenv";
import type { Config } from "drizzle-kit";

// this is needed for pushing
dotenv.config({ path: ".env.local" });

const config: Config = {
  schema: "./src/drizzle/schema.ts",
  out: "./src/drizzle",
  driver: "pg",
  dbCredentials: {
    connectionString: process.env.DATABASE_URL!
  }
};

export default config;
```

We don’t have this `drizzle` folder and `schema.ts` yet, so create them at the specified location. 

The `schema.ts` file can be blank for now.

We also don’t have an environment variable called `DATABASE_URL` yet either, so also at the root level, create a `.env.local` file.

Then in this file, add this:

```
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
```

Next, in the `drizzle` folder, create a file called `config.ts`

Add this code to it:

```tsx
// src/drizzle/config.ts

import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";

import * as schema from "@/drizzle/schema";

const connectionString = process.env.DATABASE_URL || "";
const client = postgres(connectionString);
const db = drizzle(client, { schema });

export { client, db };
```

At this point, you should get a red underline for `import * as schema`, which makes sense since the file is currently blank. Let’s create a `note` table in the schema.

A `note` will have:

- `id` (uuid)
- `name` (text)
- `content` (text)
- `color` (text - will select from yellow, green, blue, purple in the ui)
- `created_at` (timestamp)

Back in `schema.ts`, add the following:

```tsx
// src/drizzle/schema.ts

import { timestamp, pgTable, text, uuid } from "drizzle-orm/pg-core";

export const notes = pgTable("notes", {
  id: uuid("id").defaultRandom().notNull().primaryKey(),
  name: text("name").notNull(),
  content: text("content").notNull(),
  color: text("color").notNull(),
  created_at: timestamp("created_at").notNull().defaultNow()
});
```

## Migrate & Push

Link to Migrations in the Drizzle docs [**here**](https://orm.drizzle.team/docs/migrations)

Now that we have a model in our schema, we can migrate our database to your schema.

Add a `generate` script to `package.json`:

```json
{
  // ...
  "scripts": {
    // ...
    "generate": "drizzle-kit generate:pg"
  }, 
}
```

Then in the terminal, run:

```json
npm run generate
```

We can also push our schema changes directly to the database.

Add a `push` script to `package.json`:

```json
{
  // ...
  "scripts": {
    // ...
    "push": "drizzle-kit push:pg"
  }, 
}
```

Then run the command:

```bash
npm run push
```

If the push was successful, you should now see a notes table in DBeaver!

![Notes table appearing in local postgres db](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7j614rwo72y6uzkygtlo.png)

And if you double click on it, and switch to the Data tab, you can see the table!

![Viewing the notes table in local postgres database](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ozibcta90ytsg160ts7b.png)

It’s empty for now, but that’s ok! This is a great start!

Pro-tip: create a single `db` command to run the `generate` and `push` commands one after the other:

```tsx
"scripts": {
  // ...
  "generate": "drizzle-kit generate:pg",
  "push": "drizzle-kit push:pg",
  "db": "npm run generate && npm run push"
},
```

## Setting Up Site Structure

For this simple app, we will have the following pages:

- `home`
- `notes`

You can copy and paste these commands to create all the files and folders at once:

```bash
cd src/app/
mkdir notes
cd notes/
touch page.tsx
cd ../../../
```

Here’s the boilerplate code for the `page.tsx`:

```tsx
// src/app/notes/page.tsx

const NotesPage = () => {
  return (
    <div>
      <h1>NotesPage</h1>
    </div>
  );
};

export default NotesPage;
```

Next, create a folder called `components` in the `src` folder.

In the `components` folder, create a file called `navbar.tsx` and add the following code to it:

```tsx
// src/components/navbar.tsx

import Link from "next/link";

const Navbar = () => {
  return (
    <nav className="border-b-black border-b-2 p-2">
      <ul className="flex items-center gap-x-4">
        <li>
          <Link className="hover:text-sky-500 hover:underline" href="/">
            Home
          </Link>
        </li>

        <li>
          <Link className="hover:text-sky-500 hover:underline" href="/notes">
            Notes
          </Link>
        </li>
      </ul>
    </nav>
  );
};

export { Navbar };
```

Then update the root `layout.tsx` file to use it (as well as wrap `children` within the `main` element and add some `padding`):

```tsx
// src/app/layout.tsx

const RootLayout = ({ children }: RootLayoutProps) => {
  return (
    <html lang="en">
      <body className={inter.className}>
        <Navbar />

        <main className="p-2">{children}</main>
      </body>
    </html>
  );
};
```

## Create a Form to Create a Note

Link to Server Actions in Next.js docs [**here**](https://nextjs.org/docs/app/api-reference/functions/server-actions)

Let’s add a `form` to the notes `page.tsx` that will add a Note into the database:

```tsx
// src/app/notes/page.tsx

const NotesPage = () => {
  return (
    <div>
      <h1>NotesPage</h1>

      <form
        className="inline-flex items-start flex-col space-y-4 border-solid border-black border-2 p-4 mt-2 w-80"
        action={createNote}
      >
        <div className="w-full">
          <label htmlFor="name" className="block">
            Name:
          </label>

          <input
            id="name"
            name="name" // IMPORTANT: MAKE SURE TO INCLUDE A NAME FOR INPUTS
            type="text"
            className="border-solid border-black border-2 block w-full"
            required
          />
        </div>

        <div className="w-full">
          <label htmlFor="content" className="block">
            Content:
          </label>

          <textarea
            id="content"
            name="content"
            className="border-solid border-black border-2 block w-full"
            required
          />
        </div>

        <div className="w-full">
          <label htmlFor="color" className="block">
            Color:
          </label>

          <select
            id="color"
            name="color"
            className="border-solid border-black border-2 block w-full"
            required
          >
            <option value="">Select Color</option>
            <option value="yellow">Yellow</option>
            <option value="green">Green</option>
            <option value="blue">Blue</option>
            <option value="purple">Purple</option>
          </select>
        </div>

        <button
          className="border-solid border-black border-2 py-1 px-4 hover:bg-black hover:text-white w-full"
          type="submit"
        >
          Create Note
        </button>
      </form>
    </div>
  );
};
```

Previously, we would have to abstract this form out into its own component and add `"use client"` at the top.

Now, thanks to [**server actions**](https://nextjs.org/docs/app/api-reference/functions/server-actions), we can keep everything in this file and write an `async` function that will be used as the form’s `action`.

To get started, we need to enable Server Actions in our Next.js project by adding the experimental `serverActions` flag to the `next.config.js` file:

```tsx
// next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    serverActions: true
  }
};

module.exports = nextConfig;
```

Server Actions can be defined in two places:

- Inside the component that uses it (Server Components only).
- In a separate file (Client and Server Components), for reusability. You can define multiple Server Actions in a single file.

Before we setup our server action, let’s install `zod` as we can use it to validate the data from the form:

```tsx
npm i zod
```

To keep things simple, we’ll define our action function here in the `NotesPage` component:

```tsx
// src/app/notes/page.tsx

import { revalidatePath } from "next/cache";
import { z } from "zod";

import { db } from "@/drizzle/config";
import { notes } from "@/drizzle/schema";

const schema = z.object({
  name: z.string().min(1),
  content: z.string().min(1),
  color: z.string().min(1)
});

const NotesPage = () => {
  const createNote = async (formData: FormData) => {
    "use server";

    // validate data
    const validated = schema.parse({
      name: formData.get("name"),
      content: formData.get("content"),
      color: formData.get("color")
    });

    try {
      await db.insert(notes).values({
        name: validated.name,
        content: validated.content,
        color: validated.color
      });

      revalidatePath("/notes");

      return {
        message: "Note created successfully!",
        revalidated: true,
        now: Date.now()
      };
    } catch (error) {
      return {
        message: "Something went wrong when creating the note!"
      };
    }
  };

  return (
    <div>
      <h1>NotesPage</h1>

      <form
        className="inline-flex items-start flex-col space-y-4 border-solid border-black border-2 p-4 mt-2 w-80"
        action={createNote}
      >
        {/* ... */}
      </form>
    </div>
  );
};
```

Now, open up the DevTools and go to the Network tab and clear everything out.

Next, try to submit the form with something like this:

![Sample note being entered into form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d1y8e57hgkwpsv1f84io.png)

When you click **Create Note**, open up the DevTools and check the Network tab:

![POST request being sent to notes route](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qu3vpf0h44po2akr86bd.png)

You’ll see it sends a `POST` request to the URL that we are currently on.

Not only that, but if you head over to DBeaver, you should see the note has been added to the database as well (you might need to click **Refresh** along the bottom of the window):

![Sample note appearing in local postgres db](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9vgjjmu7yodunefeg1h5.png)

## Fetch and Display Notes

Now let’s add another async function, this time outside the component body, to query the database and get all the notes:

```tsx
// src/app/notes/page.tsx

const schema = z.object({
  // ...
});

const getNotes = async () => {
  try {
    const nts = await db.select().from(notes);

    return nts;
  } catch (error) {
    throw new Error("Something went wrong when fetching notes!");
  }
};
```

Then let’s update the page component to use it, as well as set up `colorVariants` for dynamic background color. I personally don't like this approach. I would much rather just use string interpolation within the `className` prop, but this is just how it's done with Tailwind:

```tsx
// src/app/notes/page.tsx

// ...

const colorVariants = {
  blue: "bg-blue-200",
  green: "bg-green-200",
  purple: "bg-purple-200",
  yellow: "bg-yellow-200"
};

const NotesPage = async () => {
  const createNote = async (formData: FormData) => {
    "use server";

    // ...
  };

  const nts = await getNotes();

  return (
    <div>
      <h1 className="text-4xl">NotesPage</h1>

      <form
        className="inline-flex items-start flex-col space-y-4 border-solid border-black border-2 p-4 mt-2 w-80"
        action={createNote}
      >
	      {/*  */}
      </form>

      <div className="mt-4">
			  <h2 className="text-2xl">Notes</h2>
			
				<ul className="grid gap-4 grid-cols-12">
				  {nts.length
				    ? nts.map((nt) => {
				        // @ts-ignore
				        const colorClasses = colorVariants[nt.color];
				
				        return (
				          <li key={nt.id} className={`${colorClasses} col-span-2 p-4 rounded-lg`}>
				            <h3 className="text-xl font-semibold mb-1">{nt.name}</h3>
				            <p>{nt.content}</p>
				          </li>
				        );
				      })
				    : null}
				</ul>
			</div>
    </div>
  );
};
```

Now our notes will appear in a nice little grid!

![Sample notes appearing in a grid](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ns8dovoc45lxypfsie0b.png)

## Deploy - Prerequisites

Now time for the main attraction: deployment!

If you’ve never deployed a full-stack app like this before, it’s a great learning experience. It certainly was for me!

First, there are a few prerequisites:

1. **Docker:** Make sure you have Docker installed on your local machine.
2. **Docker Compose:** Install Docker Compose, which is a tool for defining and running multi-container Docker applications.
3. **DigitalOcean Account:** Set up a DigitalOcean account and create a VPS (Virtual Private Server) instance.

I’ll walk you through each of these steps now.

## Docker and Docker Compose

You can check if `docker` is already installed on your machine by going to the root directory of your machine, and running this command:

```bash
docker --version
# Docker version 24.0.6, build ed223bc
```

If not, then you will need to check the [**installation instructions online**](https://docs.docker.com/engine/install/) to install Docker for your machine.

Similarly, you can run this command to check if `docker compose` is installed on your machine or not:

```bash
docker compose version
# Docker Compose version v2.20.3
```

You can find installation instructions for Docker Compose [**here**](https://docs.docker.com/compose/install/).

## DigitalOcean

Next, head over to [**DigitalOcean’s website**](https://www.digitalocean.com/) and click **Sign Up** if don’t have an account, or **Login** if you already have one. I choose to **Sign up with GitHub**

If you chose to Sign up as well, here’s what I selected in the welcome screen:

- **What do you plan to build on DigitalOcean?** A web or mobile application
- **What is your role or business type?** Hobbyist or Student
- **What is your monthly spend on cloud infrastructure across cloud platforms? (Provide an estimate):** $0 - $50
- **How many employees work at your company?** I work alone

Click **Submit**

Add a payment method of your choice.

Once logged in, you should land on a page that looks like this:

[https://cloud.digitalocean.com/welcome](https://cloud.digitalocean.com/welcome?i=90aab7)

I would suggest removing the `/welcome` part, and go straight to https://cloud.digitalocean.com/

This should redirect you to a `/projects` page, with a default project already created called `first-project`

First, we need to **Create a Droplet**

To do so, click the green **Create** button in the top right corner, and click **Droplets** from the dropdown menu

Choose a **Region** that is closest to your location

Leave **Datacenter** selection alone

For **Choose an image**, I stuck with the default **Ubuntu** on **Version 23.04 x64**

For **Droplet Type**, Basic (Plan selected) is the cheapest choice

For **CPU options**, I selected:

- Premium Intel (Disk: NVMe SSD)
- $8/mo - 1GB / 1 Intel CPU, 35GB NVMe SSDs, 1000GB transfer

For **Choose Authentication Method**, we will be using an **SSH Key**.

Thankfully DigitalOcean has a nice prompt saying “We can walk you through setting up your first SSH key”, which I will walk you through now. Click on **Add SSH Key**.

## Creating an SSH Key

Open a terminal and run the following command (at the root level):

```bash
sudo ssh-keygen
```

You will be prompted to save and name the key.

I will save mine in the `/usr/local/bin` directory. Again, I am on Ubuntu, so you will need to pick a location that works for your machine.

```bash
/usr/local/bin/digital_ocean_ssh
```

Next you will be asked to create and confirm a passphrase for the key. This is highly recommended, and you can use a site [**like this one**](https://www.strongpasswordgenerator.org/) to generate a strong passphrase.

```bash
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
```

Store what you’ve entered into a text file, or the `.env.local` file as a comment, for now

You should see the following output:

```bash
Your identification has been saved in digital_ocean_ssh
Your public key has been saved in digital_ocean_ssh.pub
The key fingerprint is:
.....
The key's randomart image is:
.....
```

Copy and paste this output into the same location that you pasted the filename and password from the previous step.

We need to copy the contents of the `.pub` file into the SSH ley field in the modal

We can do that by running:

```bash
cat PATH_TO_YOUR_PUB_FILE
# ex: cat /usr/local/bin/digital_ocean_ssh.pub
```

Either way, once you run the command, you should see an output that starts like this:

```bash
ssh-rsa REALLY_LONG_ASS_STRING_HERE
```

Copy this, and paste it into the **SSH key content** input field back on DigitalOcean

Give it a name. I named mine **DigitalOcean Next.js SSH**

Click **Add SSH Key**

## Finish Droplet Setup

Under options, you can click to **Add improved metrics monitoring and alerting (free)**, since it is, well, free. The others are up to you.

Scroll down to **Finalize Details**

Add some tags if you want, I added a few:

- `next_js`
- `digital_ocean`
- `postgres`

Click **Create Droplet**

Wait for the blue progress bar to finish, and eventually you should see a notification that the Droplet was added to the project.

## Test Droplet Connection

Once your Droplet is created, you can access it via SSH.

Click on it, and copy the `ipv4` address to the same text file / `.env.local` file as a comment.

Run this command at root level (double check the path):

```bash
ssh -i /PATH_TO_YOUR_SSH_FILE root@YOUR_DROPLET_IP
# ex: ssh -i /usr/local/bin/digital_ocean_ssh root@##.###.###.##
```

If you should see this:

```bash
Are you sure you want to continue connecting (yes/no/[fingerprint])?
Please type 'yes', 'no' or the fingerprint:
```

Enter in `fingerprint`, then `yes`

You will then see this:

```bash
Enter passphrase for key '/home/andrew/digital_ocean_ssh':
```

Copy and paste the passphrase you created earlier and hit **Enter**

You should now see your terminal change to the root of your droplet:

```bash
root@ubuntu-s-1vcpu-1gb-35gb-intel-sfo3-01:~#
```

You can exit anytime by running the `exit` command.

## Create a **PostgreSQL Database Cluster**

Back at the main dashboard, click the green **Create** button in the top right corner, and click **Databases** from the dropdown menu.

Choose the **datacenter region** closest to you.

For the **database engine**, select **PostgreSQL (v15)**.

Choose whichever **database configuration** that best suits your budget. The same goes for **storage size**. I went with 20GB.

Make sure the project selected is **first-project**.

When you’re ready, click the **Create a Database Cluster**.

It’ll take approximately 5 minutes for the database to finish provisioning.

## Setup `Dockerfile` and `docker-compose.yml`

While the database is provisioning, let’s Dockerize our Next.js app

Head back to the code and add these 2 files at the root level of the project:

```bash
touch Dockerfile docker-compose.yml
```

In the `Dockerfile`, add the following code:

```docker
# Dockerfile

# Use an official Node.js runtime as the base image
FROM node:18-alpine

# Set the working directory in the container
WORKDIR /usr/src/app

# Copy package.json and package-lock.json to the container
COPY package.json package-lock.json ./

# Install dependencies
RUN npm install

# Copy the rest of the application files to the container
COPY . .

# Build the Next.js application for production
RUN npm run build

# Expose the application port (assuming your app runs on port 3000)
EXPOSE 3000

# Start the application
CMD ["npm", "start"]
```

And in the `docker-compose.yml` file, add the following:

```yaml
# docker-compose.yml

version: "3"

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production

  db:
    image: postgres:latest
    environment:
      POSTGRES_USER: "${POSTGRES_USERNAME}"
      POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
      POSTGRES_DB: "${POSTGRES_DATABASE}"
```

As you can see, we are using environment variables for some of our values related to Postgres.

We want to keep this information secure, and not have it in plain sight.

While we already have a `.env.local`, the `docker-compose.yml` reads from `.env` only.

So, duplicate the `.env.local` file, and rename it to `.env`.

You will also want to make sure to update the `.gitignore`:

```
# local env files
.env
.env*.local
```

In the `.env` file, have the following:

```
# .env

DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"

POSTGRES_USERNAME="postgres"
POSTGRES_PASSWORD="postgres"
POSTGRES_DATABASE="postgres"
```

Next, make sure to push these changes up to GitHub!

## Finish DigitalOcean Postgres Setup

Head back over to DigitalOcean, and under the Getting Started section, skip ahead a bit and click on **4 Next Steps**.

This will contain the migrate command that we will need shortly. Click the blue `show-password` link, and then copy / paste the command somewhere. You can click **Hide**.

## Import Local DB To DigitalOcean

Since we already have a local PostgreSQL database, lets export it and then import that into DigitalOcean.

To export an existing database, we can use the `pg_dump` command (run this at the root level of your machine):

```bash
cd ~
pg_dump -h localhost -p 5432 -U postgres -d postgres -F c -b -v -f ~/Documents/db_backup.pgsql
```

When you run this command, it will prompt you for the password, which is also just `postgres`. I am on Ubuntu, so I chose the Documents folder for convenience sake. Feel free to update the path as needed.

Here’s a quick explanation of the command options:

- **`h localhost`**: Specifies the host where your PostgreSQL server is running.
- **`p 5432`**: Specifies the port number on which PostgreSQL is listening.
- **`U postgres`**: Specifies the username to connect to the database.
- **`d postgres`**: Specifies the name of the database you want to export.
- **`F c`**: Specifies the custom format for the dump file.
- **`b`**: Includes large objects in the dump.
- **`v`**: Enables verbose mode to see the progress.
- **`f ~/Documents/db_backup.dump`**: Specifies the output file path and name. In this case, the dump file will be saved in the Documents folder with the name **`db_backup.dump`**.

And there it is!

![db_backup.pgsql dump file in documents folder](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8xkswgqj2br8t9748c94.png)

Now we need to import this into DigitalOcean.

But before we do, we can add a connection to our DigitalOcean PostgreSQL database in DBeaver. I think this is just nice to have, so we can make sure things are working properly.

In DBeaver, click **Database** and then **New Database Connection**

Select **PostgreSQL**

Have **Connect by** set to **Host**

The **URL** field will not be adjustable, but that’s ok as it will auto update with the information we provide.

Simply copy and paste the information from DigitalOcean into their respective field. When you copy + paste the `host`, you should see the URL update.

You will need to update the:

- Host
- Database
- Port
- Username
- Password

Once you do, click **Test Connection**. If you see something like this:

![DigitalOcean PostgreSQL db connection success](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/enhnuwjgh06j2crrn5ud.png)

You’re good to proceed! I'm simply cutting off most of the information within the popup.

Click **Ok** to close this little popup, and then click **Finish**

Once the connection has been added to your sidebar, you can rename it if you like to make it clearer what the connection is for:

![Renaming DigitalOcean Postgres db to note_taker](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vhn99zuer0qbirpcqc6y.png)

You can also see here there are no tables, but like before with the local setup, that’s ok! 

You can view the DigitalOcean docs on Importing a database [**here**](https://docs.digitalocean.com/products/databases/postgresql/how-to/import-databases/) if you like.

To import the database, we’ll take the migrate command copied from earlier, and just add on the path to our `db_backup.pgsql` file:

```bash
PGPASSWORD=[YOUR_DB_PASSWORD] pg_restore -U [YOUR_DB_USERNAME] -h [YOUR_DB_HOST] -p [YOUR_DB_PORT] -d [YOUR_DB_NAME] ~/Documents/db_backup.pgsql
```

Once you run this, you should see an output like this:

```bash
pg_restore: error: could not execute query: ERROR:  must be member of role "postgres"
Command was: ALTER TABLE public.notes OWNER TO postgres;

pg_restore: warning: errors ignored on restore: 1
```

Which seems like something went wrong, but don’t worry! If you head over to DBeaver, you should see that the `notes` table has been added!

![notes table added to digitalocean postgres db](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pfopxcx9kb4aar2g9sx5.png)

## **Deploy to Droplet Using Docker Compose**

To get started, in a terminal window, SSH into your DigitalOcean VPS like before. Within our VPS, we’ll need to install the following:

- Docker
- Docker Compose
- PostgreSQL

Thankfully, it’s very easy to do so.

Install Docker:

```bash
# Update the system packages
sudo apt update

# Install necessary packages to allow apt to use a repository over HTTPS
sudo apt install apt-transport-https ca-certificates curl software-properties-common

# Add Docker's official GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

# Add Docker repository
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Update the system packages again
sudo apt update

# Install Docker
sudo apt install docker-ce docker-ce-cli containerd.io
```

Verify Docker installation:

```bash
docker --version
# Docker version 24.0.6, build ed223bc
```

Install Docker Compose:

```bash
# Download the latest version of Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

# Make Docker Compose executable
sudo chmod +x /usr/local/bin/docker-compose
```

Verify Docker Compose installation:

```bash
docker compose version
# Docker Compose version v2.21.0
```

Install PostgreSQL:

```bash
sudo apt update
sudo apt install postgresql postgresql-contrib
```

Verify PostgreSQL installation:

```bash
psql --version
# psql (PostgreSQL) 15.4 (Ubuntu 15.4-0ubuntu0.23.04.1)
```

`cd` back to the root level, and if you run the command `ls`, all you should see listed is `snap`

Create a new directory called `repos` and `cd` into it

```bash
mkdir repos
cd repos/
```

In this `repos` folder, `clone` the GitHub repo you setup at the start by running:

```bash
git clone https://github.com/[YOUR_GITHUB_USERNAME]/[YOUR_REPO_LINK].git
```

Once the clone is complete, `cd` into the directory, and run `ls` to verify the contents:

```bash
cd [YOUR_REPO_NAME]
ls

# output:

# ls
Dockerfile  docker-compose.yml  next.config.js     package.json       public  tailwind.config.ts
README.md   drizzle.config.ts   package-lock.json  postcss.config.js  src     tsconfig.json
```

Great!

One issue here though is we don’t have an `.env` file, which makes sense since we added it to the `.gitignore`. Thankfully, it’s very easy to not only create this file, but write to it as well.

You can create it by running:

```bash
touch .env
```

And if you run `ls` again, it’s not there? What gives??

The `.env` file is considered a hidden file. To view hidden files only within the current directory, you can run this command:

```bash
ls -ld .?*
```

You should now see `.env` listed, along with `.git` and `.gitignore`.

What do we need to write the `.env` file? 

We will need `DATABASE_URL` for our Drizzle config + schema, and as well as `POSTGRES_USERNAME`, `POSTGRES_PASSWORD`, and `POSTGRES_DATABASE` for our `docker-compose.yml`.

To write to the `.env` file, you can run the following command:

```bash
echo "DATABASE_URL=[YOUR_CONNECTION_STRING_URI]" > .env
echo "POSTGRES_DATABASE=[YOUR_DATABASE_NAME]" >> .env
echo "POSTGRES_USERNAME=[YOUR_DATABASE_USERNAME]" >> .env
echo "POSTGRES_PASSWORD=[YOUR_DATABASE_PASSWORD]" >> .env
```

You can get all the necessary info from your DigitalOcean dashboard. I’d recommend copying and pasting the above command into a text file so you can easily swap out the placeholders for your values, and then copy and paste all 4 lines at once with the correct values into your terminal to run them one after another.

You can then verify the write was successful by running the `cat` command:

```bash
cat .env

# output:
# DATABASE_URL=...
# POSTGRES_DATABASE=...
# POSTGRES_USERNAME=...
# POSTGRES_PASSWORD=...
```

Finally, we can start our services using Docker Compose in detached mode:

```bash
docker compose up -d
```

It may take a bit of time to start up the services. but after a while you should see something like this:

```bash
[+] Running 3/3
 ✔ Network [YOUR_REPO_NAME]_default  Created
 ✔ Container [YOUR_REPO_NAME]-db-1   Started
 ✔ Container [YOUR_REPO_NAME]-web-1  Started
```

Now, if you open a new browser tab and go to the following address:

```bash
http://[YOUR_DROPLET_IPV4_ADDRESS]:3000
```

Voila! Your app is now running in a VPS! 🎉

## Outro

I really hope you enjoyed this post and found it helpful! Some next steps to consider would be to deploy the containers as a Kubernetes cluster on DigitalOcean. However, I am a Docker noobie, and setting up a cluster with HTTPS seems like it’s a bit out of reach for me at this time. I am very happy I went through this process, and I hope you enjoyed it as well. And going through this process has definitely motivated me to learn more about Docker and Kubernetes.

As always, here is a link to my [**repo**](https://github.com/andrews1022/demo-nextjs-13-digitalocean-and-docker) with the full source code you can use as a reference in case you get stuck.

Cheers, and happy coding!