---
url: https://docs.hostly.ninja/use-cases/web-app-with-postgres.md
---
# Example: Web app with PostgreSQL

This example deploys a web app that stores its data in a PostgreSQL database. The app is public, the database is only reachable by the app.

```text
Internet ──HTTPS──> app (app.example.hostly.ninja, port 3000)
                     │
                     └──> db:5432 (internal, volume)
```

The examples use the workspace domain fragment `example`. Replace it and the image of the app with your own.

::: info Paid tier
This example runs two services in one workspace, and the database needs a volume to keep its data across restarts. Both require the paid tier: the free tier allows one service per workspace and has no volumes.
:::

## 1. Deploy the database

Create a new service with these settings:

| Step | Setting | Value |
|---|---|---|
| Subdomain | Only reachable internally | on |
| | Service name | `db` |
| | High availability | off |
| Image | Image | `postgres:18` |
| | Container app port | `5432` |
| Configuration | Secret | `DB_PASSWORD` |
| | Environment variables | see below |
| | Volume | mount path `/var/lib/postgresql`, 5 GB |

In the configuration step, first create the database password: click **Add** in the **Secrets** panel next to the environment variables, name the secret `DB_PASSWORD`, and enter a strong password. Secrets belong to the workspace, so the app can use the same secret later. The password is never written into an environment variable in plain text.

::: tip
Use only letters and digits in the password. Special characters such as `@`, `/` or `:` would have to be URL-encoded in the connection string of the app.
:::

Then enter the environment variables. Reference the secret by dragging it into the editor or by typing `${DB_PASSWORD}`:

```dotenv
POSTGRES_USER=app
POSTGRES_DB=app
POSTGRES_PASSWORD=${DB_PASSWORD}
```

The service name `db` is part of the address the app uses and cannot be changed later.

After the deployment, the service details page shows **Internally reachable at `db:5432`**. The service has no public URL.

::: warning
Do not enable high availability for the database. The three instances would share one volume and corrupt the data.
:::

## 2. Deploy the web app

Create a second service:

| Step | Setting | Value |
|---|---|---|
| Subdomain | Only reachable internally | off |
| | Subdomain | `app` |
| | Service name | `app` (prefilled from the subdomain) |
| | SSL | on |
| Image | Image | `registry.hostly.ninja/example/app:1.0.0` |
| | Container app port | `3000` |
| Configuration | Environment variables | see below |

```dotenv
NODE_ENV=production
DATABASE_URL=postgres://app:${DB_PASSWORD}@db:5432/app
```

The secret `DB_PASSWORD` created with the database already appears in the **Secrets** panel, so reference it the same way instead of creating it again. Hostly resolves `${DB_PASSWORD}` from the secret store when the container starts. The host `db` is the service name of the database.

The app is now reachable at `https://app.example.hostly.ninja` and connects to the database internally.

## Good to know

* **Start order:** Deploy the database first. If the app starts before the database is ready, it should retry the connection instead of exiting. Otherwise, Hostly restarts it until the database accepts connections.
* **Pausing and stopping:** Pausing the database keeps the volume and its data. Stopping the database deletes the volume and all data.
* **Operations:** The database is a regular container. Upgrades of the PostgreSQL major version and backups are up to you, e.g. with a worker that runs `pg_dump` regularly.
* **Other databases** work the same way: MySQL or MariaDB on `3306`, MongoDB on `27017`. Check the image documentation for the data directory to mount the volume at.

## Related pages

* [Use cases overview](./index.md)
* [What is a Service?](../concepts/service.md)
* [Deploy your first service](../getting-started/deploy-service.md)
