---
title: "Docker Compose on a VPS: an app and a database in one compose.yaml | StreetHosting"
description: "Run an app with a database using Docker Compose on your VPS: compose.yaml, volumes, the .env file, restart policy, updates and the catch with ports and UFW."
url: "https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide"
type: "page"
language: "en-US"
---

VPS · 9 min · Intermediate

Published on Nov 13, 2026 · Updated on Nov 13, 2026

# Docker Compose on a VPS: one compose.yaml with an app, a database and data that survives

One file describes the app, the database and the volumes, and one command brings everything up. Here is the full compose.yaml, the everyday commands, how to update without losing data and the two traps that catch everyone.

By [Equipe StreetHosting](https://streethosting.com.br/en/autores#equipe-streethosting) · StreetHosting infrastructure and support team

[Containers and Docker](https://streethosting.com.br/en/guides/topics/docker) [Security and hardening](https://streethosting.com.br/en/guides/topics/security) [Databases](https://streethosting.com.br/en/guides/topics/databases)

Summarize with:

[](https://chat.openai.com/?q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fdocker-compose-vps-guide.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "ChatGPT") [](https://claude.ai/new?q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fdocker-compose-vps-guide.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Claude") [](https://www.google.com/search?udm=50&aep=11&q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fdocker-compose-vps-guide.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Google AI Mode") [](https://x.com/i/grok?text=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fdocker-compose-vps-guide.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Grok") [](https://www.perplexity.ai/search/new?q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fdocker-compose-vps-guide.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Perplexity")

Share:

[](https://x.com/intent/tweet?text=Docker%20Compose%20on%20a%20VPS%3A%20an%20app%20and%20a%20database%20in%20one%20compose.yaml&url=https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fdocker-compose-vps-guide "Share on X") [](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fdocker-compose-vps-guide "Share on Facebook") [](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fdocker-compose-vps-guide "Share on LinkedIn") [](https://wa.me/?text=Docker%20Compose%20on%20a%20VPS%3A%20an%20app%20and%20a%20database%20in%20one%20compose.yaml%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fvps%2Fdocker-compose-vps-guide "Share on WhatsApp")

For agents: Copy as Markdown [.md](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide.md)

In this guide 8 sections

* [Why use Compose](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#por-que-compose)
* [The compose.yaml](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#arquivo)
* [Secrets in the .env file](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#env)
* [Everyday commands](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#comandos)
* [Named volume or bind mount](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#volumes)
* [Updating the app](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#atualizar)
* [Two traps: ports and down -v](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#armadilhas)
* [Where to run this](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#onde-rodar)

Quick answer

**Docker Compose** describes the app, the database and the volumes in a single file, the `compose.yaml`, and brings everything up with `docker compose up -d`. Use a named volume for the database data, `restart: unless-stopped` so everything comes back after a reboot, and a `.env` file for passwords. Two warnings: a port published by Docker bypasses UFW, so publish on 127.0.0.1 whatever should not be public, and `docker compose down -v` deletes the volumes.

## Why use Compose[](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#por-que-compose)

Running one container with `docker run` works until the day you have to repeat the command. An app with a database already needs two containers, a network between them, a volume, environment variables and a restart policy. Keeping all that in a terminal line is a recipe for forgetting a flag on the next deploy.

Compose fixes this with a file that lives next to the project, can go into Git and documents exactly how the environment comes up. The same file works on your computer and on the VPS. This guide starts from a VPS with Docker already installed, as shown in the guide on [installing Docker on Ubuntu](https://streethosting.com.br/en/guides/vps/install-docker-ubuntu-vps). Confirm it with `docker compose version`.

## The compose.yaml[](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#arquivo)

The example below runs a web app that listens on port 3000 and a PostgreSQL database. Create a folder for the project, for example `~/meuapp`, and save the file in it as `compose.yaml`.

`services: app: build: . restart: unless-stopped env_file: .env ports: - "127.0.0.1:3000:3000" depends_on: db: condition: service_healthy db: image: postgres:16 restart: unless-stopped environment: POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_DB: ${DB_NAME} volumes: - dbdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] interval: 5s timeout: 3s retries: 10 volumes: dbdata:`

What each part does:

* **build: .** builds the app image from the `Dockerfile` in the folder. If you use a ready made image, swap it for `image: name/image:tag`.
* **restart** with the value `unless-stopped` restarts the container if it crashes and brings it back up when the VPS reboots, unless you stopped it on purpose.
* **ports** publishes the app port. Note the `127.0.0.1:` in front: it makes the port reachable only from inside the VPS itself, where Nginx will fetch it. The section on traps explains why.
* **The database publishes no port at all.** Both services sit on the same network Compose creates, and the app finds the database by the service name, `db`, on port 5432. Nobody outside the VPS can reach the database.
* **depends\_on with a healthcheck** makes the app wait for PostgreSQL to accept connections, instead of starting alongside it and failing on the first attempt.
* **volumes** declares the `dbdata` volume, where PostgreSQL keeps its data. Without it, the data would die with the container.

Do not write the `version:` line at the top. It is obsolete in current Compose. To check that the file is valid and see how the variables were substituted, run `docker compose config`.

## Secrets in the .env file[](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#env)

Compose automatically reads a file named `.env` in the same folder and uses its values in the `${NAME}` expressions of the compose.yaml. The `env_file: .env` line of the app service passes those same variables into the container. That way the passwords live in a single file, outside the compose.yaml.

`DB_USER=meuapp DB_PASSWORD=troque_por_uma_senha_longa DB_NAME=meuapp DATABASE_URL=postgres://meuapp:troque_por_uma_senha_longa@db:5432/meuapp`

Three precautions for this file:

1. Never push it to Git. Put `.env` in `.gitignore` and, if you like, commit a `.env.example` without the real values.
2. Restrict reading on the VPS with `chmod 600 .env`.
3. Generate a strong password, for example with `openssl rand -hex 24`. Hexadecimal avoids characters like @, : and / that would break the DATABASE\_URL.

PostgreSQL only reads POSTGRES\_USER, POSTGRES\_PASSWORD and POSTGRES\_DB the first time, when the volume is empty. If you change the password in the `.env` later, the database keeps the old one, because the data already exists in the volume. The change has to be made inside the database, with ALTER USER.

## Everyday commands[](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#comandos)

All the commands below run inside the project folder, where the compose.yaml is.

| Command                               | What it does                                                                                         |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| docker compose up -d                  | Creates and starts everything in the background. If it already exists, it only applies what changed. |
| docker compose ps                     | Lists the services and shows whether they are running and healthy.                                   |
| docker compose logs -f app            | Follows the app service log in real time. Ctrl+C leaves without stopping the container.              |
| docker compose logs --tail 100 db     | Shows the last 100 lines of the database log.                                                        |
| docker compose restart app            | Restarts only the app service.                                                                       |
| docker compose exec db psql -U meuapp | Opens a PostgreSQL terminal inside the database container.                                           |
| docker compose stop                   | Stops the containers without deleting anything.                                                      |
| docker compose down                   | Stops and removes containers and network. The volumes stay.                                          |

On the first run, use `up -d` and then `logs -f` to watch the app connect to the database. If it falls into a crash loop, the log shows the error and `ps` shows the status as restarting.

Docker keeps each container's log in a file, and by default that file grows without limit. On an app that writes a lot of logs, it can fill the disk. Inside each service, the `logging` option with the `json-file` driver, plus the `max-size` and `max-file` limits, fixes it. If the disk is already tight, the guide on [finding what takes up space on the VPS](https://streethosting.com.br/en/guides/vps/find-what-uses-disk-space-on-vps) helps you find the culprit.

## Named volume or bind mount[](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#volumes)

There are two ways to keep data outside the container, and the choice changes what you can see and what can go wrong.

|                        | Named volume                                     | Bind mount                                              |
| ---------------------- | ------------------------------------------------ | ------------------------------------------------------- |
| How to declare         | dbdata:/var/lib/postgresql/data                  | ./dados:/var/lib/postgresql/data                        |
| Where it lives on disk | Managed by Docker, under /var/lib/docker/volumes | In the folder you chose                                 |
| Permissions            | Docker sorts them out                            | You must match the container user with the folder owner |
| Editing from the host  | Awkward                                          | Direct, with any editor                                 |
| Best for               | Database and application data                    | Config files and code during development                |

The rule of thumb: databases go in a named volume. A file you want to open and edit, like an `nginx.conf` or a site's uploads directory, goes in a bind mount. For a bind mount of a config file the container only reads, add `:ro` at the end, as in `./nginx.conf:/etc/nginx/nginx.conf:ro`.

To see the existing volumes use `docker volume ls`. Compose prefixes the name with the project folder, so the example volume shows up as something like `meuapp_dbdata`. A volume is not a backup: it protects against recreating the container, not against a failing disk or a wrong command. Take database copies with `docker compose exec db pg_dump` and keep them off the VPS.

## Updating the app[](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#atualizar)

The update cycle depends on where the image comes from.

**A ready made image from a registry** (like PostgreSQL, or one you published):

`docker compose pull docker compose up -d docker image prune -f`

`pull` downloads the new versions. `up -d` recreates only the services whose image changed, keeping the volumes. `image prune` deletes old images left unused.

**An app built from your own code** (the `build: .` in the example): update the code with `git pull` and run `docker compose up -d --build`.

Two tips so you do not regret it. Pin the database version in the compose.yaml, like `postgres:16`, instead of `latest`: jumping from one major version to another without migrating the data makes PostgreSQL refuse to start. And before updating the database, copy the data with pg\_dump. To automate the deploy on every push, see [deploying with GitHub Actions to your VPS](https://streethosting.com.br/en/guides/vps/github-actions-deploy-to-vps).

## Two traps: ports and down -v[](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#armadilhas)

**1\. A published port bypasses UFW.** Many people turn on UFW, block everything, run `ports: - "5432:5432"` in Compose and find out days later that the database is open to the internet. The reason is that Docker inserts its own rules into iptables, and traffic to a published port is diverted to the container before it reaches the UFW rules. `ufw deny` has no effect on it.

The defense is not to publish what does not need to be public:

* **Database, cache and queues:** do not declare `ports`. The other services in the same compose reach them by name, on the internal network.
* **An app that sits behind Nginx:** publish only on 127.0.0.1, like `127.0.0.1:3000:3000`, and let Nginx do the proxying, as in the guide on [Nginx as a reverse proxy](https://streethosting.com.br/en/guides/vps/nginx-reverse-proxy-vps).
* **When you really want the port public:** publish it, but know that it is public. To filter who gets in, the UFW rules are not enough and you need the DOCKER-USER chain of iptables.

After bringing it up, check from outside the VPS. From another computer, test the port with a port checker or with `nc -zv IP_DA_VPS 5432`. On the VPS, the command `sudo ss -tlnp` shows which address each port listens on: 0.0.0.0 is public, 127.0.0.1 is local. The guide on the [UFW firewall on a VPS](https://streethosting.com.br/en/guides/vps/ufw-firewall-ubuntu-vps) covers the rest of the rules.

**2\. down with the `-v` option deletes the volumes.** `docker compose down` is safe for the data. `docker compose down -v` also removes the project's named volumes, that is, the whole database, without asking for confirmation and with no way to undo it. The same risk shows up with `docker volume prune -a` after a down: with no container using the volume, it gets cleaned up.

Use `-v` only when you really want to start from scratch, like in a test environment. In production, take a pg\_dump before any command that has down, prune or rm in it.

## Where to run this[](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#onde-rodar)

Docker needs a full Linux kernel and root access to create networks, volumes and firewall rules. On [StreetHosting VPS](https://streethosting.com.br/en/vps) the virtualization is KVM, with its own kernel and root access, so Docker and Compose run the way they are shown in this guide.

* **A small app with a database:** the Xeon VPS from R$ 26.00 per month, with 2 vCPU, 2 GB DDR4 and 20 GB NVMe, fits a stack like the one in this guide. Watch memory with Compose running, because every service adds to the total.
* **Several services or heavier load:** the [Ryzen 9 9950X VPS](https://streethosting.com.br/en/vps/ryzen) starts at R$ 43.00 per month with 1 vCPU and 2 GB DDR5 and grows up to 64 GB, always with NVMe.

All of them are in São Paulo, with AntiDDoS included, up to 5 TB of traffic per month and activation in about 60 seconds after Pix or card approval.

In this guide

* [Why use Compose](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#por-que-compose)
* [The compose.yaml](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#arquivo)
* [Secrets in the .env file](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#env)
* [Everyday commands](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#comandos)
* [Named volume or bind mount](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#volumes)
* [Updating the app](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#atualizar)
* [Two traps: ports and down -v](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#armadilhas)
* [Where to run this](https://streethosting.com.br/en/guides/vps/docker-compose-vps-guide#onde-rodar)

## Frequently asked questions

What is the difference between docker-compose and docker compose?

The hyphenated docker-compose is the old version, written in Python and now discontinued. The spaced docker compose is the current Docker plugin and the one the install guide brings. The commands and the file are nearly the same, and the recommended file name today is compose.yaml.

Do I need to write version at the top of compose.yaml?

No. The version field is obsolete and current Compose only prints a warning if it is in the file. Start straight with services.

Does the database disappear when I restart the containers?

No, as long as the data is in a volume. Stopping, restarting or recreating the container keeps the volume. The data only goes away if you delete the volume, for example with docker compose down -v.

Why does UFW not block the port Docker published?

Docker writes its own firewall rules, and traffic to published ports is handled before the UFW rules. To keep a port from being public, publish it only on the local address, like 127.0.0.1:3000:3000, or do not publish it and let the containers talk over the internal network.

How do I update a service image without losing data?

Run docker compose pull to download the new image and then docker compose up -d. Compose recreates only the services whose image changed, and the volumes stay intact. If the app is built from your own code, use docker compose up -d --build.

Next step

See VPS plans

Root VPS in Brazil with NVMe and Anti-DDoS.

[See VPS plans](https://streethosting.com.br/en/vps)

[See Ryzen VPS Ryzen 9 9950X VPS in São Paulo with root access, NVMe and gamer Anti-DDoS.](https://streethosting.com.br/en/vps/ryzen)

## Related guides

[VPS Intermediate How to install Docker on Ubuntu 22.04 or 24.04 on a VPS (straight to the point) A stable Docker install on an Ubuntu VPS depends on the right package source and step by step validation. In this guide you set up the official repository, install Engine and Compose, test the daemon and apply basic security measures before deploying applications. 3 min Read guide](https://streethosting.com.br/en/guides/vps/install-docker-ubuntu-vps) [VPS Intermediate How to set up Nginx as a reverse proxy on a VPS Your app runs on an internal port and you want to serve it on a domain with HTTPS. Nginx as a reverse proxy solves that and also brings several apps together in one place. 3 min Read guide](https://streethosting.com.br/en/guides/vps/nginx-reverse-proxy-vps) [VPS Beginner UFW on Ubuntu VPS: firewall rules without losing SSH UFW makes the Ubuntu firewall simpler, but one rule in the wrong order locks you out of your VPS. Learn how to enable it without losing SSH, open only what you need, deal with Docker, and get back in through the console if something goes wrong. 10 min Read guide](https://streethosting.com.br/en/guides/vps/ufw-firewall-ubuntu-vps)

[← Back to the Guide Center](https://streethosting.com.br/en/guides)
