English
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.
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/appThe 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_dumpregularly. - Other databases work the same way: MySQL or MariaDB on
3306, MongoDB on27017. Check the image documentation for the data directory to mount the volume at.
