VOOZH about

URL: https://blog.logrocket.com/implement-coolify-self-hosted-alternative-heroku/

⇱ How to implement Coolify, the self-hosted alternative to Heroku - LogRocket Blog


2024-08-28
4366
#node#postgresql#react
David Omotayo
194725
114
👁 Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

If you’ve been active on Twitter or Reddit lately, you’ve likely seen discussions surrounding Vercel’s pricing model, which has turned many success stories into nightmares.

👁 Image

For example, Cara, a creative platform, experienced a surge of 650,000 users overnight, resulting in a $95,000 bill from Vercel. This outraged many developers, so they started looking for cheaper alternatives that let them self-host their deployments.

Alternatives such as Heroku were previously and widely favored among developers due to its user-friendly nature and the provision of a free hobbyist tier. However, since Heroku discontinued its free plans, it’s no longer the go-to choice for most developers.

One alternative that has recently gained widespread adoption and popularity is Coolify, a free, open-source, self-hostable platform that provides the convenience of cloud services and enables users to host their applications effortlessly. With Coolify, you can avoid the risk of unforeseen charges, all while maintaining full control over your infrastructure.

In this article, I’ll list the benefits of using Coolify, walk you through the process of setting up a server and hosting Coolify, and demonstrate how to deploy resources on it.

If you’re already familiar with Coolify and are looking to implement it, then you can go ahead and skip to the getting started section.

🚀 Sign up for The Replay newsletter

The Replay is a weekly newsletter for dev and engineering leaders.

Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.

What to know before implementing Coolify

This article assumes you have the following:

  • Basic knowledge of React and Express
  • Node.js and Git installed on your computer

Coolify as an IaC solution

Managing infrastructure manually is no longer feasible in today’s fast-paced development environment. As projects scale and evolve, maintaining consistency, reducing human error, and ensuring efficient deployment processes become paramount. This is where Infrastructure as Code (IaC) comes into play.

IaC is a DevOps practice that manages infrastructure using code. While this explains what IaC is, understanding its importance requires knowing why it’s needed and what problems it solves in relation to Coolify’s purpose.

Before IaC, IT infrastructure was managed manually. Developers or operations teams had to set up servers and configure them before deploying applications. This process was time-consuming and had issues like high cost, scalability problems, and inconsistencies.

The advent of Cloud computing relieved some of these discrepancies, allowing IT professionals to build and maintain data centers at lower costs. However, inconsistencies persisted when different people manually made configurations.


Over 200k developers use LogRocket to create better digital experiences

👁 Image
Learn more →

IaC revolutionizes how we handle infrastructure by enabling the management and provisioning of resources through code, fostering automation, version control, and collaboration. Tools like Terraform, Ansible, and Chef let developers define infrastructure (like servers and databases) in configuration files. This allows them to edit, copy, distribute, and repeat the setup without inconsistencies.

So, how does Coolify fit into all of this?

What is Coolify?

Coolify bridges the gap between traditional IaC tools and user-friendly application deployment.
While tools like Terraform, Chef, and Ansible focus on managing and provisioning infrastructure through code, they can be complex and require significant expertise. Coolify, on the other hand, offers a simpler, more accessible interface for deploying and managing applications, making it easier for developers who may not be experts in IaC to still benefit from its concepts.

In addition to aligning with IaC principles, Coolify offers several key features that embrace the IaC approach to modern development workflows:

  • Declarative Configuration — Coolify allows you to define the desired state of your applications and services through a web interface, which is a key principle of IaC
  • Version Control Integration — you can integrate Coolify with Git repositories, enabling you to store your application and infrastructure configurations as code. This supports the IaC principle of version-controlled configurations
  • Automated Deployments — Coolify automates the deployment process, ensuring consistency and repeatability, which are core benefits of IaC
  • Infrastructure Management — Coolify provides tools to manage and monitor your deployed services, similar to how IaC tools manage infrastructure states

Additionally, Coolify supports a Terraform provider, which enables you to manage its resources through Terraform code, thus bringing IaC capabilities into the mix.

Getting started with Coolify

To start using Coolify, you’ll need a server. This can be a Virtual Private Server (VPS) or any server you can access remotely. The server will host Coolify, allowing you to manage your applications and services.

There are many VPS providers to choose from, such as Digital Ocean, Linode, and Hetzner. Your decision will likely depend on who offers the best price and performance.

Coolify recommends Hetzner for its affordable and high-performance servers. However, their account verification process can be challenging, often rejecting new accounts without clear reasons.

After several rejections, I chose to go with Digital Ocean, which also offers a good balance of cost and performance. If you think you can successfully register with Hetzner, please consider using the referral link in the Coolify documentation to support the project.

Setting up a VPS

To set up a VPS, start by creating an account with your preferred provider, and then create a server. Pay attention to these key details when configuring your server:

Region

Choose a server location close to your main users to ensure fast and reliable connections. For example, I chose London:

👁 Create Droplets page showing region selection options, including New York, San Francisco, London, Singapore, and others, with London selected.

Operating system

Select an operating system for your server. Ubuntu is a popular choice, or you can use one recommended by Coolify:

👁 Interface for choosing an operating system image. The options include Ubuntu, Fedora, Debian, CentOS, AlmaLinux, and Rocky Linux. The selected image is Ubuntu 24.04 (LTS) x64.

Hardware resources

Choose the amount of CPU, RAM, and storage your applications will need. Coolify requires at least 2 CPUs, 2GB memory, and 30GB of storage. For my example, I chose a server with 2 CPUs, 4GB RAM, and 80GB storage:

👁 Screenshot of DigitalOcean CPU options showing pricing for different droplet configurations. The selected option is "Regular" with a disk type of SSD, featuring various plans ranging from $6/mo to $96/mo.

Access method

Decide how you will access the server. You can use either SSH keys or a password. SSH keys are recommended for better security:

👁 A screen showing the option to choose an authentication method for a Droplet. The options include using an SSH key or a password. The SSH Key option is selected, and there is an option to add a public SSH key, with a note that SSH keys are more secure than passwords.

To set up SSH key access, click **Add SSH key** on your server setup page and a modal will pop up. I believe Hetzner has the same UX flow:

👁 A user interface for adding a public SSH key. The screen displays a large input box labeled "SSH key content" where users are expected to paste their SSH key. Below the input box, there's another field labeled "Name" for naming the SSH key. On the right side, there is a guide titled "SSH Keys," providing instructions on how to create or add SSH keys, with a command example for generating a new key pair.

Next, open your terminal and run the following command to generate a key pair:

ssh-keygen

If you’re using Windows with Git installed, use the bash terminal to generate a key pair using the following command instead:

ssh-keygen -t rsa

Alternatively, you can install Puttygen and follow this instruction on how to generate a key pair.

After running the command, you’ll be asked to provide a path to save the key. Press enter to use the default path:

Generating public/private ed25519 key pair.
Enter file in which to save the key (/c/Users/david/.ssh/id_ed25519):

Then, you’ll be prompted to enter a passphrase. Choose a strong password and save it securely:

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

Once you’ve set the passphrase, you’ll see a confirmation message indicating the SSH key pair was created:

Your identification has been saved in /c/Users/david/.ssh/id_rsa
Your public key has been saved in /c/Users/david/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:j9HUkzKvqJoxGQYXCwjAnYMu9c/0fabrYosgMrvk160 david@Enigmae
The key's randomart image is:
+---[RSA 3072]----+
|*.+ o |
|.o.= o . . |
|....+ + + |
|.. o. . o + . |
|. o+ .S.. . |
| . oo .=..o |
|o.. * . o o+ |
|o+ o * +o . |
|oo. oE+..+o. |
+----[SHA256]-----+

Next, run the following command to display your public key:

cat ~/.ssh/id_rsa.pub

The output should look something like this:

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCjNZ2iUSZ6zS8MbQvF2CchdNxL52a0ovTKc2ikFAzIdOnVm+zt1r8k1+TEQaWAZqod1TZp21VdyQ9tdLwRRUDWwATBuyW+XGUlSzZU+EZuxKzno4V8R4X7qM57Of3r9U03v/PMv22z7k6aCdoEW/YNm0j+IzgN3nOL0zsWjkll79XKfJpUi+wvkmcm8D+4vQrXZyV46tilmLZPPek9T2fY/2HPtg4kuQKuJgTvAsWOLHdIRQuKCVhTeRgKy8ekxp+Q1T05DeopUHcz/uLg0kRbtc+SuvWqo0cQmwToTdjqBxic0sJBS3dYyhIim6I2BXYbC/6LrXMVXYrOPo9SqgXeiHKdvY+LY3dJIZCUFWMKi5JELaA3fm+hohwDe5+wtlMuP0jJLk+YTIbrDNcRWohQRmsbHXOgGHGraXxM8t6IcG55Uc4uNXysmriZf0DYG58qMzdeWgBzps4AnMRGYk1Lrbg8hK38nzYDwvQWpIhckCm1VtkiB/yXcovBSIUH+8U= david@Enigmae

Copy the output and paste it into the modal on your server setup page, then click Save or Add SSH Key:

👁 Screenshot of the "Add public SSH key" interface. The image shows a form where a user can paste their public SSH key into a text area and name it "sshkey." There is a blue button labeled "Add SSH Key" below the form. On the right side, there are instructions for generating a new key pair using the `ssh-keygen` command, with a copy button next to the command.

After completing these steps, review your server’s configuration. If everything looks good, click the create button to create your server:

👁 Screenshot showing the pricing for a droplet on a cloud platform. The price is $24.00 per month or $0.036 per hour. There are two buttons: one labeled "CREATE VIA COMMAND LINE" and another labeled "Create Droplet."

Installing Coolify

Once your virtual server is up and running, open your terminal and use this command to access it via SSH:

ssh root@<server-ip-address>

Replace <server-ip-address> with your server’s IP address, which you can find here:

👁 Screenshot of the Coolify_server Web Application page. The Resources tab is selected, showing a droplet named "ubuntu-s-2vcpu-4gb-lon1-01" with the associated IP address "134.122.96.44" highlighted by a red arrow. The page includes options for upgrading and managing resources.

After running the command, you’ll be asked if you want to add the IP address to the list of known IPs. Type "``yes" and press enter:

$ ssh [email protected]
The authenticity of host '134.122.96.44 (134.122.96.44)' can't be established.
ED25519 key fingerprint is SHA256:yfaoc/1er+eRO404gd83jRqQfh0Vo+hME1ONgYMasqA.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes

The next prompt will ask you to enter the passphrase of your SSH key or your server’s password if you used password authentication:

Enter passphrase for key '/Users/david/.ssh/id_rsa':

Once authenticated, you will have remote access to your server:

👁 Terminal screenshot displaying system information for Ubuntu 24.04 LTS, including system load, memory usage, processes, and network details. The system shows that security updates are available and provides links to Ubuntu documentation and management.

With remote access, we can run commands on the remote server directly from our computer. Let’s install Coolify on the server.

Paste the following command into your terminal and press enter to start the installation:

curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash

The installation process takes one to two minutes. Do not cancel it.

Once completed, the terminal will show an IP address where you can access Coolify. In my case, it was: http://134.122.96.44:8000:

👁 Terminal screenshot showing the output of starting various Coolify containers, such as coolify-redis and coolify-db, with statuses like "Starting," "Healthy," and "Started." The final message indicates that the Coolify instance is ready to use and provides a URL to access it: http://134.122.96.44:8000.

Copy the IP address and enter it into your browser. This will take you to a registration page where you register to access the Coolify instance:

👁 Coolify registration page with fields for name, email, password, and a button to register or log in if already registered.

After registering, you’ll be redirected to a welcome page. Click Get Started to proceed through the onboarding pages:

👁 A welcome screen for the Coolify platform. The screen features a large, centered message saying "Welcome to Coolify" followed by a smaller text that reads "Let me help you set up the basics." Below this message is a prominent "Get Started" button. At the bottom of the screen, there are options to "Skip onboarding," "Restart onboarding," and provide "Feedback."

Follow the onboarding steps until you reach the “Server” page, where you’ll choose to deploy resources on Localhost or a Remote Server:

👁 Screenshot of the server deployment options in Coolify, asking whether to deploy resources to Localhost or a Remote Server.

Remember, resources are components like your application, database, server, or other services you want to set up on your server. Here, Coolify is asking if you want to set these up on the same server where Coolify is installed (i.e. Localhost) or on a different remote server. If you don’t have another remote server, choose Localhost and create a new project:

👁 Screenshot of the Coolify interface prompting the user to create a new project. The text reads "Let's create an initial project for you. You can change all the details later on." Below the text is a button labeled "Create new project!".

After clicking Create new project, you’ll finish onboarding. Click Let``'``s do it! to access your Coolify dashboard:

👁 Screenshot of the Coolify dashboard showing an overview of the self-hosted infrastructure, including one project and one server. The dashboard also includes options for settings, resource addition, and deployment management.

Et Voila! Coolify is now installed on your VPS. We’ll create and deploy a sample application in the next section.

Creating a sample application

To demonstrate deploying resources on Coolify, we’ll create the quintessential to-do application that features creating, reading, updating, and deleting (CRUD) tasks.

Ideally, we would use Next.js to deploy the server and frontend together. However, to demonstrate how to deploy standalone resources, we’ll deploy each one individually: a frontend (Next.js), a server (Node.js), and a database (Postgres).

If you already have a project or want to go straight to deploying resources, you can skip this section.

Creating the Node.js server

Let’s start with the Node.js server. Create a folder on your computer, open a terminal in that folder, and run this command:

mkdir backend && cd backend

This command will create a “backend” folder and navigate into it.

Next, initialize a new Node project and install the required dependencies:

npm init -y
npm install express pg cors body-parser dotenv

After installing the dependencies, set up the project with this structure:

backend/
├── controllers/
│ └── tasksController.js
├── db/
│ └── db.js
├── routes/
│ └── tasks.js
├── index.js
└── package.json

With the project structure ready, we can start building the app’s business logic. Start by opening the db.js file in the db folder and adding the following code:

// db/db.js
const { Pool } = require('pg');

const pool = new Pool({
 user: 'postgres',
 host: 'localhost',
 database: 'todos',
 password: 'pass',
 port: 5432,
});

const query = (text, params) => pool.query(text, params);

module.exports = {
 query,
};

This code establishes a connection pool to a PostgreSQL database and provides a function for executing SQL queries.

Leave the db.js file for now, and open the tasksController.js file in the controllers folder. Add this code:

// controllers/tasksController.js
const db = require("../db/db");

const getTasks = async (req, res) => {
 try {
 const result = await db.query("SELECT * FROM todos ORDER BY id");
 res.json(result.rows);
 } catch (err) {
 res.status(500).json({ error: err.message });
 }
};

const createTask = async (req, res) => {
 const { title, description } = req.body;
 try {
 const result = await db.query(
 "INSERT INTO todos (title, description) VALUES ($1, $2) RETURNING *",
 [title, description]
 );
 res.json(result.rows[0]);
 } catch (err) {
 res.status(500).json({ error: err.message });
 }
};

const updateTask = async (req, res) => {
 const { id } = req.params;
 const { title, description, completed } = req.body;
 try {
 const result = await db.query(
 "UPDATE todos SET title = $1, description = $2, completed = $3 WHERE id = $4 RETURNING *",
 [title, description, completed, id]
 );
 res.json(result.rows[0]);
 } catch (err) {
 res.status(500).json({ error: err.message });
 }
};

const deleteTask = async (req, res) => {
 const { id } = req.params;
 try {
 await db.query("DELETE FROM todos WHERE id = $1", [id]);
 res.sendStatus(204);
 } catch (err) {
 res.status(500).json({ error: err.message });
 }
};

module.exports = {
 getTasks,
 createTask,
 updateTask,
 deleteTask,
};

This code creates controller functions to interact with the Postgres database using prepared statements to:

  • Retrieve all tasks (getTasks)
  • Create a new task (createTask)
  • Update an existing task (updateTask)
  • Delete a task (deleteTask)

Next, add routes for the controller methods by opening the tasks.js file in the routes folder and adding this code:

// routes/tasks.js
const express = require('express');
const router = express.Router();
const tasksController = require('../controllers/tasksController');

router.get('/', tasksController.getTasks);
router.post('/', tasksController.createTask);
router.put('/:id', tasksController.updateTask);
router.delete('/:id', tasksController.deleteTask);

module.exports = router;

Finally, open the index.js file and add this code:

// index.js
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const tasksRoutes = require('./routes/tasks');

const app = express();
const port = process.env.PORT || 5000;

app.use(cors());
app.use(bodyParser.json());

app.use('/tasks', tasksRoutes);

app.listen(port, () => {
 console.log(`Server running on port ${port}`);
});

Our server is halfway done. We just need to set up and connect to a Postgres database to make it functional.

Creating a Postgres database

Setting up a Postgres database on Coolify is fairly straightforward because Coolify offers a ton of one-click resources for instant deployment, with Postgres being one of them.

First, log into your Coolify dashboard and click add resource next to the project name:

👁 Screenshot of the Coolify dashboard showing the "My first project" section with an arrow pointing to the "+ Add Resource" option, indicating where to add a new resource. The dashboard also displays the localhost server and a cleanup queues button.

This will take you to a page with various templates. Scroll down and select Postgres:

👁 Coolify Projects page displaying various database options such as PostgreSQL, Redis, DragonFly, KeyDB, Clickhouse, MongoDB, MySQL, and MariaDB.

On the configuration page, enter a name for the database and a password (or use the default). Click the start button at the top right to launch the database:

👁 Screenshot of the PostgreSQL database configuration page with a red status indicator showing the database has exited, and an arrow pointing to the start button.

Once the database is up and running, scroll down and check the make it publicly available box. This provides a public connection URL for connecting to the database remotely:

👁 CoolifyDB interface showing the option to make the PostgreSQL URL publicly available with a checkbox and a public port set to 5432.

Next, install a Postgres client like pgAdmin or DBeaver on your computer. Open it, click add new server, and a modal will appear:

👁 Register Server dialog box in a database management tool with an error message indicating that either Host name or Service must be specified.

Add any name of your choice the server, click the connection tab, and enter the database host address. You can find this between @ and : in the public connection string:

👁 Screenshot showing the Postgres URL details with both internal and public URLs. The internal URL is obscured, while the public URL is partially visible, highlighting the IP address and port used for the database connection.

Then, enter your Postgres instance password in the password field, and click on the save button:

👁 Connection tab in the Register Server dialog box, showing the fields for entering the host name, port, maintenance database, username, and password for connecting to a PostgreSQL server.

If successful, you’ll see the following dashboard, confirming that pgAdmin is successfully connected to the remote database:

👁 CoolifyDB dashboard displaying system statistics, including server sessions, transactions per second, tuples in and out, and block I/O performance metrics.

Creating a table

The next step is to create a table for our data. To do this, click the arrow icon next to the server name on the sidebar. Click Databases, select the database name (e.g., Postgres), and click the query tool icon on the top bar:

👁 The interface shows a PostgreSQL management dashboard with various panels. On the left side, a navigation tree displays the hierarchy of objects within a database named "CoolifyDB," specifically highlighting the "postgres" database. The right side shows the "Dashboard" tab with "General" and "System Statistics" sub-tabs. The System Statistics tab is visible, showing metrics like database sessions (Total, Active, Idle) and transaction rates. The "Query Tool" option is highlighted as well.

This opens the query page where you can create a table using query scripts:

👁 Screenshot showing the pgAdmin interface connected to a remote PostgreSQL database, highlighting the query tool and available database objects in the navigation pane.

Copy and paste the query script below into the field, and click the play icon to run it:

CREATE TABLE tasks (
 id SERIAL PRIMARY KEY,
 title VARCHAR(255) NOT NULL,
 description TEXT,
 completed BOOLEAN DEFAULT FALSE
);

👁 A screenshot of a SQL query editor connected to a PostgreSQL database named "CoolifyDB". The query editor shows a script to create a table called "tasks" with columns for id, title, description, and completed status. The script is ready to be executed with the "Execute script" option highlighted, which is mapped to the F5 key.

After running the script, you should get a similar response as below which means the table has been successfully created:

Query returned successfully in 252 msec.

Connecting the server and database

To connect the Node server to the database, create a .env file in the Node server’s root directory, add the following code to the file, and replace the long string with your database’s password:

DB_PASSWORD="aIxEt6GJPu1DUjgKTT1g5VJv1jp25X3qYduFqI0zCGB04UnzcQfthgcutDmJPWFf"

Then, update the db.js file with your database credentials and port like so:

const { Pool } = require("pg");

const pool = new Pool({
 user: "postgres",
 host: "134.122.96.44",
 database: "postgres",
 password: "aIxEt6GJPu1DUjgKTT1g5VJv1jp25X3qYduFqI0zCGB04UnzcQfthgcutDmJPWFf",
 port: 5432,
});

const query = (text, params) => pool.query(text, params);

module.exports = {
 query,
};

We updated the host, database, and password fields with the database’s hostname, database name, and password, respectively.

You can find the database name in the public connection URL string we exposed earlier. It appears after the last forward slash in the URL. For example, postgres, which is after the last forward slash, is the database name in the example below:

postgres://postgres:aIxEt6GJPu1DUjgKTT1g5VJv1jp25X3qYduFqI0zCGB04UnzcQfthgcutDmJPWFf@209.97.189.179:5432/postgres

The database password can also be found on the configuration page:

👁 Coolify interface displaying the PostgreSQL database configuration settings, including fields for database name, username, password, and network configurations.

Now, start the development server to verify if it successfully connects to the database:

👁 Screenshot of a POST request made in Postman to the endpoint http://localhost:5000/tasks with a JSON body containing "title": "New Task" and "description": "This is a new task." The response shows a JSON object with "id": 6, "title": "New Task", "description": "This is a new task.", and "completed": false.

Creating a Next.js application

Setting up the frontend application is straightforward, so we won’t spend too much time on it.

Open your terminal and navigate to the “frontend” directory. Run the following command to install Next.js:

npx create-next-app@latest

After the installation, run the following commands to install Axios, and start the development server:

npm i axios && npm run dev

Next, open the page.js file in the app folder, clean up the boilerplate code, and add the following code:

import React, { useState, useEffect } from "react";
import axios from "axios";

const App = () => {
 const [tasks, setTasks] = useState([]);
 const [title, setTitle] = useState("");
 const [description, setDescription] = useState("");

 useEffect(() => {
 fetchTasks();
 }, []);

 const fetchTasks = async () => {
 const res = await axios.get("http://localhost:5000/tasks");
 setTasks(res.data);
 };

 const addTask = async () => {
 const res = await axios.post("http://localhost:5000/tasks", {
 title,
 description,
 });
 setTasks([...tasks, res.data]);
 setTitle("");
 setDescription("");
 };

 const updateTask = async (id, completed, title, description) => {
 const res = await axios.put(
 `http://vow8owk.209.97.189.179.sslip.io/tasks/${id}`,
 {
 title,
 description,
 completed,
 }
 );
 setTasks(tasks.map((task) => (task.id === id ? res.data : task)));
 };

 const deleteTask = async (id) => {
 await axios.delete(`http://localhost:5000/tasks/${id}`);
 setTasks(tasks.filter((task) => task.id !== id));
 };

 return (
 <div>
 <h1>Add a task...</h1>
 <div>
 <input
 type="text"
 value={title}
 placeholder="Title"
 onChange={(e) => setTitle(e.target.value)}
 />
 <textarea
 cols="45"
 rows="8"
 placeholder="Description"
 onChange={(e) => setDescription(e.target.value)}
 />
 <br />
 <button onClick={addTask}>Add Task</button>
 </div>
 </div> {/* Close the div properly */}
 );
}; 
export default App;

Here, we use Axios to interact with the database through the endpoints provided by our Node server.

Lastly, add the specified styles to the CSS file and go to localhost:3000 in your browser:

@import url('https://fonts.googleapis.com/css2?family=Nanum+Pen+Script&display=swap');

:root {
 font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
 line-height: 1.5;
 font-weight: 400;

 color-scheme: light dark;
 color: #111c25;
 background-color: #ffffff;

 font-synthesis: none;
 text-rendering: optimizeLegibility;
 -webkit-font-smoothing: antialiased;
 -moz-osx-font-smoothing: grayscale;
}

body {
 margin: 0;
 min-height: 100vh;
 color: #111c25;
 background-color: #ffffff;
}

h1 {
 font-size: 3.2em;
 line-height: 1.1;
}

button {
 border-radius: 8px;
 border: 1px solid transparent;
 padding: 0.6em 1.2em;
 font-size: 1em;
 font-weight: 500;
 font-family: inherit;
 background-color: #646cff;
 cursor: pointer;
 transition: border-color 0.25s;
}

li {
 list-style-type: none;
}

ul {
 padding: 0 15px;
}

p,
h1 {
 margin: 0;
}

button:hover {
 background-color: #d80b0b;
}

.add-task-button:hover {
 background-color: #414491;
}

button:focus,
button:focus-visible {
 outline: 4px auto -webkit-focus-ring-color;
}

input[type=text] {
 max-width: 100%;
 border-radius: 5px;
 background-color: white;
 color: #1a1a1a;
 border: solid #0060DF 2px;
 padding: 10px;
 margin-bottom: 10px;
}

textarea {
 border-radius: 5px;
 background-color: white;
 border: solid #0060DF 2px;
 padding: 10px;
 max-width: 100%;
 margin-bottom: 10px;
 color: #1a1a1a;
}

.task-form {
 display: flex;
 flex-direction: column;
 max-width: 400px;
}

.app-container {
 width: 100%;
 display: flex;
 justify-content: space-around;
 align-items: center;
}

.task-card {
 border: solid #0060DF 2px;
 border-radius: 5px;
 padding: 10px;
 width: 400px;
 margin-bottom: 10px;
}

.task-title {
 margin-bottom: 5px;
 display: flex;
 align-items: center;
 justify-content: space-between;
}

h1 {
 margin-bottom: 60px;
 font-family: "Nanum Pen Script", cursive;
}

.task-list-header {
 text-align: center;
 color: rgb(96, 96, 96);
 font-family: "Nanum Pen Script", cursive;
 font-size: 30px;
}

.task-checkbox {
 width: 15px;
 height: 15px;
 background-color: #f0f0f0;
 border: 1px solid #ccc;
 border-radius: 15px;
}

.task-completed {
 opacity: 0.5;
}

.delete-task-button {
 opacity: 1;
 background: #d80b0b;
 margin-top: 10px;
 display: block;
}

.task-list-section {
 border: #0060DF 0.2px solid;
 height: 700px;
 overflow-y: scroll;
 padding: 0 20px;
}

.task-actions {
 position: relative;
}

.arrow-icon {
 position: absolute;
 top: -30px;
 left: -50px;
 rotate: -30deg;
}

span {
 position: absolute;
 top: -45%;
 left: -100px;
 rotate: -40deg;
 font-size: 17px;
 font-family: "Nanum Pen Script", cursive;
 color: gray;
}

.show-text{
 display: flex;
 font-family: "Nanum Pen Script", cursive;
 justify-content: center;
 align-items: center;
 font-size: 40px;
 color: gray;
 width: 100%;
 height: 500px;
}

.hide-text{
 display: none;
}

@media only screen and (max-width: 868px) {
 .app-container {
 flex-direction: column;
 overflow: hidden;
 }

 .task-list-section {
 margin-top: 50px;
 border: none;
 border-top: 2px solid #747bff;
 }

 .task-card {
 width: 400px;
 }
}

If everything is set up correctly, the application should work as shown in the GIF below:

👁 A user interface screen for adding a task in a task management application. On the left side, there is a form titled "Add a task..." with fields for "Title" and "Description," followed by an "Add Task" button. On the right side, there is a section labeled "Tasks" with a message saying "It's quiet in here," indicating that no tasks have been added yet.



That’s it! All three resources are working together and running smoothly. You can find the code for the NextJs and Node server in my GitHub repository.

The next step is to deploy both the Node server and React application to our Coolify instance.

Deploying resources on Coolify

Coolify supports git-based deployment using Nixpacks, an open-source Buildpack tool that creates container images from a source directory for deployment anywhere on the web.

In other words, we don’t have to go through the trouble of containerizing our resources before deploying them on Coolify — we simply need to create and push our resources to their respective Git repositories, and Coolify will handle the rest.

Go ahead and push the Node server and Next.js app to their respective repositories Then, in your Coolify dashboard, click the add resource button and select the Public Repository option:

👁 A screen showing the "New Resource" section in a web application, under the "Environment: production" setting. It displays three options for Git-based applications: Public Repository, Private Repository (with GitHub App), and Private Repository (with deploy key).

Return to the Node server’s repository, copy the repository link from the address bar, and paste it into the field on Coolify:

👁 Screenshot of the "Create a new Application" interface in Coolify. The interface shows a field for entering the Repository URL, with the URL https://github.com/david4473/Coolify-application/tree/main/server entered. A button labeled "Check repository" is on the right.

Once Coolify loads the repository, choose Nixpacks as the buildpack and set the port to 3000. Then click Continue:

👁 A screenshot of a form used to create a new application from a public Git repository. The form includes fields for entering the Repository URL, Rate Limit, Branch, Port, and selecting a Build Pack. The URL provided points to a GitHub repository for a Coolify Node server. The Branch is set to "main," the Port is set to "3000," and the selected Build Pack is "Nixpacks." There is a "Continue" button at the bottom of the form.

On the next page, navigate to Environment > +New, and add the environment variable for the database password. Click save:

👁 A screen showing the creation of a new environment variable. The variable name is "DB_PASSWORD" and the value is a long alphanumeric string. The options to mark the variable as a build variable, multiline, or literal are available but not selected.

After saving the environment variables, click Deploy at the top right corner of the screen to deploy the resource.

To monitor the deployment process, click the show debug button for detailed logs:

👁 Deployment log screen showing the progress of a Docker image build. The log includes details about the Nixpacks version used, the setup commands, and the installation of npm packages. A red arrow points to the "Show Debug Logs" button, indicating where users can click to view additional logs.

Once the application is successfully deployed, click the Links tab to find the domain and see your app live:

👁 Screenshot of the configuration page for a Coolify Node.js server. The page shows the server is running, with options to redeploy, restart, or stop the server. The server's URL is displayed in a purple box.

Repeat the same process to deploy the Next.js application, but remember to use the database’s private (internal) connection URL and uncheck the make it publicly available checkbox. This ensures secure internal communication within your Coolify setup:

👁 Screenshot of the PostgreSQL configuration interface showing the internal Postgres URL, public port settings, and an option to make the port publicly available.

If everything is set up correctly, all three services should be running as expected:

👁 Screenshot of the Coolify resources dashboard showing deployed resources including a Next.js app, Node server, and PostgreSQL database in a production environment.

Note that the connection to the application is not secure yet. Normally, this would be addressed with a custom domain, but for now, you can enable a free, temporary SSL certificate for the generated domain by adding an s after http in the address on the configuration page, like this:

👁 Configuration screen showing the domain settings for a "Next app" deployment. The "Domains" field is populated with the URL https://w8ko08s.209.97.189.179.sslip.io, and there's an arrow pointing to the URL field. The "Build Pack" is set to Nixpacks, and the "Direction" is set to allow both www and non-www domains.

Managing Coolify resources

When deploying for production, it’s important to be able to make changes to our resources, such as upgrading to a newer version or switching to a different version. Coolify makes this easy.

Upgrading resources like the Node server and the Next.js application is as simple as pushing the update to the source repository and redeploying the instance on Coolify.

Manually doing this back and forth can quickly become tedious. Thankfully, Coolify allows us to automate deployments with webhooks. This means Coolify will automatically redeploy a resource if it detects new updates in the project’s repository.

To set this up, go to your resource’s webhook menu. You can find this on the sidebar of your resource’s configuration page.

In the webhooks menu, click on the webhook configuration on GitHub button:

👁 Screenshot of the GitHub App registration page in Coolify. The image shows a warning message stating "You must complete this step before you can use this source!" with a "Register Now" button to register the GitHub App. The webhook endpoint is set to "http://209.97.189.179:8000" with options for "Mandatory" and "Preview Deployments" checked.

Change the webhook endpoint to your Coolify instance’s custom domain, if you have one set up, to avoid exposing your server IP, as shown in the image above.

Next, click the Register Now button to log into your GitHub account, authenticate the GitHub app, and give it access to your repositories:

👁 Installation screen for a GitHub app named "bright-barracuda-nc4ogs0" on the personal account of Omotayo David. The options include installing the app on all repositories or only selected repositories. Permissions include read access to administration, code, metadata, and email addresses, as well as read and write access to pull requests.

Make sure to select All repositories and click the install button.

After the installation, you’ll be redirected back to the GitHub app page, where you’ll see the information needed to trigger a rebuild when GitHub detects a new update:

👁 GitHub App settings screen for configuring a private GitHub App for repositories. The screen includes fields for App Name, Organization, API URL, User, and several secret credentials such as Client ID, Client Secret, and Webhook Secret. Permissions settings and update options are also visible.

Everything is hooked up! If we go back to the Coolify dashboard, we should be able to create new resources from our newly added source.

The process is similar to how we deployed the initial resources. The only difference is that instead of choosing the public Repository option on the “New Resources” page, we choose the Private Repository (with GitHub app) option:

👁 Screenshot of the "New Resource" page showing options to deploy resources like Applications, Databases, and Services. The "Applications" section lists three options: "Public Repository," "Private Repository (with GitHub App)," and "Private Repository (with deploy key)." Each option is for deploying public or private repositories from supported git providers.

On the next page, choose the GitHub app we just created, and then select the repository you wish to load from the dropdown menu:

👁 Screen for creating a new application by selecting a GitHub repository. The interface shows a dropdown list of available repositories, with one repository currently highlighted. Options to add a GitHub App or change repositories on GitHub are also available at the top of the screen.

This will take you to a familiar page — the deployment configuration page. Click the deploy button to complete the process and deploy the application. Now, if you push an update to that repository, Coolify will be notified and automatically redeploy the resource.

Conclusion

So, is Coolify the ultimate alternative to the status quo? This depends on your requirements and the vision you have for your application. If you see your application scaling significantly in the future, setting up a self-hosted deployment might be a worthwhile investment. Otherwise, solutions like Heroku and Vercel will likely suffice.

While this guide will get you started with Coolify, there are even more features that I didn’t get into, such as setting up custom domains with wildcards for your Coolify instance and deployed resources or how to open up and close ports using a firewall.

What’s something you’d like to see with Coolify? Let me know in the comments. Thanks for reading, and happy building.

200s only 👁 Image
Monitor failed and slow network requests in production

Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.

👁 LogRocket Network Request Monitoring

LogRocket lets you replay user sessions, eliminating guesswork around why bugs happen by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks.

LogRocket's Galileo AI watches sessions for you, instantly identifying and explaining user struggles with automated monitoring of your entire product experience.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.

Get set up with LogRocket's modern React error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID
  2. Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not server-side

    $ npm i --save logrocket 
    
    // Code:
    
    import LogRocket from 'logrocket'; 
    LogRocket.init('app/id');
     
    // Add to your HTML:
    
    <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
     
  3. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • NgRx middleware
    • Vuex plugin
Get started now
👁 Image
👁 Image
👁 Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

What is TSRX?: What JSX would look like if it were designed today

TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension — no new framework required.

👁 Image
Ikeh Akinyemi
Jun 12, 2026 ⋅ 6 min read

How to add authentication to a React Native app with Better Auth

Learn how to build a full React Native auth system using Better Auth and Expo — with email/password login, Google OAuth, session persistence, and protected routes.

👁 Image
Chinwike Maduabuchi
Jun 9, 2026 ⋅ 13 min read

AI dev tool power rankings & comparison [June 2026]

Compare the top AI development tools and models of June 2026. View updated rankings, feature breakdowns, and find the best fit for you.

👁 Image
Chizaram Ken
Jun 8, 2026 ⋅ 11 min read

How to check username availability at scale with Bloom filters

Learn how Bloom filters reduce database lookups for username availability checks while preserving correctness at scale.

👁 Image
Rosario De Chiara
Jun 8, 2026 ⋅ 6 min read
View all posts

Would you be interested in joining LogRocket's developer community?

Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.

Sign up now