How to Install and Configure Docker Compose for a Web App (Nginx + App + Database)

How to Install and Configure Docker Compose for a Web App (Nginx + App + Database)

 Docker Compose is one of the easiest ways to run a production-style web stack on a VPS or cloud VM: reverse proxy (Nginx) + application + database, all isolated in containers with repeatable configuration.

This guide is written for beginners but follows professional best practices and stays AdSense-safe. It covers: container/image/volume/network concepts, correct Docker + Compose installation, project structure (Dockerfile vs prebuilt images), Compose fundamentals (
services
,
ports
,
env
,
depends_on
), Postgres/MySQL with persistent volumes, Nginx reverse proxy for multi-service, healthchecks/restart policies, logging/backups/safe updates, and common troubleshooting.

1) Core Concepts: Container, Image, Volume, Network

Before touching commands, understand these four terms:

Container

A container is a running instance of an image. It’s your app process in a lightweight isolated environment.

Image

An image is the build artifact (blueprint) used to create containers.
Images can be:
pulled from registries (Docker Hub, GHCR) like
postgres:16
built from your own
Dockerfile

Volume

A volume stores data outside the container’s lifecycle. This is critical for databases.
If you delete a database container without a volume, your data can be lost. With a volume, the data persists.

Network

A Docker network lets containers talk to each other by service name (DNS). In Docker Compose, you usually get this automatically.
Example: the app can reach Postgres at host
db
(service name), not
localhost
.

2) Install Docker and Docker Compose (The Right Way)

Most modern Linux distros support Docker installation via official repos. For VPS/cloud, use Docker’s official packages rather than random scripts from unknown sources.

A) Ubuntu quick approach (common, reliable)

1) Update packages:
sudo apt update sudo apt upgrade -y
2) Install Docker:
Follow the official Docker docs for Ubuntu (recommended), or your provider’s official instructions.
3) Confirm Docker is installed:
docker --version sudo systemctl status docker
4) Enable Docker on boot:
sudo systemctl enable --now docker

B) Install Docker Compose

Today Compose is typically included as the Docker CLI plugin:
Check:
docker compose version
If you see a version output, you’re good.

C) (Optional) Run Docker without sudo

Add your user to the docker group:
sudo usermod -aG docker $USER
Log out and log back in, then test:
docker ps
Security note: Docker group membership is effectively root-level power. Only grant it to trusted users.

3) Project Structure: Dockerfile vs Ready-Made Image

A standard structure for a Compose-based web app:
myproject/ docker-compose.yml .env nginx/ default.conf app/ Dockerfile package.json (or requirements.txt, etc.) src/...

When you use a Dockerfile

Use a
Dockerfile
when:
you have your own application code
you need custom dependencies
you want repeatable builds

When you use a prebuilt image

Use prebuilt images when:
the service is standard infrastructure (Postgres/MySQL/Redis/Nginx)
you don’t need custom builds
In most stacks:
Nginx: image from Docker Hub + custom config file mounted
Database: official image + volume
App: built via Dockerfile (your code)

4) Docker Compose File Basics (services, ports, env, depends_on)

A Compose file defines services and how they run together.
Key fields you’ll use:
services:
list of containers
build:
build from Dockerfile
image:
run a published image
ports:
publish container ports to host (public access)
environment:
set environment variables
env_file:
load environment variables from
.env
volumes:
persist data or mount config
depends_on:
start order (not readiness)
healthcheck:
define when a service is “healthy”
restart:
auto-restart behavior

5) Example Stack: Nginx + App + Postgres (Production-Style)

Below is a practical Compose setup you can adapt.

A) Create
.env

In
myproject/.env
:
APP_PORT=3000 POSTGRES_DB=appdb POSTGRES_USER=appuser POSTGRES_PASSWORD=change_this_strong_password # Used by app to connect DATABASE_URL=postgres://appuser:change_this_strong_password@db:5432/appdb
Do not commit
.env
to public repos.

B) App Dockerfile example (Node.js)

In
myproject/app/Dockerfile
:
FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . ENV NODE_ENV=production EXPOSE 3000 CMD ["node", "server.js"]
Your Node app should listen on
0.0.0.0:3000
inside the container.

C) Nginx reverse proxy config

In
myproject/nginx/default.conf
:
server { listen 80; server_name _; # Basic security headers (optional but helpful) add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options SAMEORIGIN always; add_header Referrer-Policy strict-origin-when-cross-origin always; location / { proxy_pass http://app:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # For websockets (if your app needs it) proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }
Notice
proxy_pass http://app:3000;
app
is the Compose service name.

D)
docker-compose.yml
(Nginx + App + Postgres + healthchecks)

In
myproject/docker-compose.yml
:
services: nginx: image: nginx:1.26-alpine container_name: nginx_proxy ports: - "80:80" volumes: - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro depends_on: app: condition: service_started restart: unless-stopped app: build: ./app container_name: app_service env_file: - ./.env environment: - DATABASE_URL=${DATABASE_URL} depends_on: db: condition: service_healthy restart: unless-stopped healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"] interval: 10s timeout: 3s retries: 5 db: image: postgres:16 container_name: postgres_db environment: - POSTGRES_DB=${POSTGRES_DB} - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} volumes: - pg_data:/var/lib/postgresql/data restart: unless-stopped healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 10s timeout: 3s retries: 5 volumes: pg_data:

Notes on this setup

depends_on
controls startup order, but healthcheck makes it more reliable by waiting for DB health before app starts.
You need your app to implement an endpoint like
/health
returning
200 OK
.

6) Alternative: Using MySQL Instead of Postgres

If you want MySQL:
db: image: mysql:8.4 environment: - MYSQL_DATABASE=appdb - MYSQL_USER=appuser - MYSQL_PASSWORD=change_this_strong_password - MYSQL_ROOT_PASSWORD=change_this_root_password volumes: - mysql_data:/var/lib/mysql restart: unless-stopped healthcheck: test: ["CMD-SHELL", "mysqladmin ping -h localhost -u appuser -p$MYSQL_PASSWORD --silent"] interval: 10s timeout: 5s retries: 10
And update your app’s DB connection string accordingly.

7) Reverse Proxy Nginx for Multi-Service (Multiple Apps / Routes)

If you want to expose multiple internal services behind one domain:
/api
→ API service
/
→ frontend service
/admin
→ admin service
Example Nginx routes:
location /api/ { proxy_pass http://api:4000/; } location /admin/ { proxy_pass http://admin:5000/; } location / { proxy_pass http://frontend:3000/; }
This pattern keeps only port 80/443 public and isolates internal ports.

8) Restart Policy, Healthcheck, and “depends_on” Best Practices

Restart policy

Common safe choice:
restart: unless-stopped
for long-running services
Avoid restart loops masking real issues. Always check logs if a container keeps restarting.

Healthcheck

Use healthchecks to detect readiness and failure:
DB:
pg_isready
or
mysqladmin ping
App: call
localhost
endpoint inside container

depends_on

Great for startup order
Not a full “wait until ready” solution unless combined with healthchecks (Compose v2 supports conditions in many environments)

9) Logging, Backups, and Safe Updates

A) Logging

View logs:
docker compose logs -f docker compose logs -f app
Check running containers:
docker compose ps

B) Backing up volumes (database)

Postgres backup example

Run a one-time dump:
docker exec -t postgres_db pg_dump -U appuser appdb > backup.sql
For full cluster backup:
docker exec -t postgres_db pg_dumpall -U appuser > backup_all.sql
Best practice: store backups off-server (object storage) and test restores.

C) Safe update workflow (minimal risk)

1) Pull latest images:
docker compose pull
2) Rebuild app if needed:
docker compose build
3) Restart safely:
docker compose up -d
4) Remove unused images:
docker image prune

Pin image versions

Avoid relying on
latest
for critical services. Prefer:
postgres:16
nginx:1.26-alpine
This prevents surprise breaking changes.

10) Common Troubleshooting (Port Conflicts, Permissions, Env Errors)

Problem 1: Port conflict (80 already in use)

Error example: “bind: address already in use”
Fix:
Stop other web servers on host (Nginx/Apache installed outside Docker), or
Change host port mapping:
ports: - "8080:80"
Then access via
http://YOUR_SERVER_IP:8080
.
Check what’s using port 80:
sudo ss -tulpn | grep :80

Problem 2: Permission issues with volumes

Symptoms:
DB fails to start
“Permission denied” when writing to mounted folders
Fixes:
Prefer named volumes for databases (like
pg_data:
), not bind mounts
If you bind mount a host directory, ensure correct ownership/permissions
Avoid running containers as root unnecessarily

Problem 3: Wrong environment variables / app can’t connect to DB

Common mistakes:
Using
localhost
inside container (wrong)
Incorrect credentials
DB not ready yet
Correct approach:
DB host should be the service name:
db
Example:
postgres://user:pass@db:5432/appdb
Check env inside container:
docker exec -it app_service env | grep DATABASE

Problem 4: App healthcheck failing

If
/health
doesn’t exist, your app will show unhealthy.
Fix:
implement
/health
endpoint
or adjust healthcheck to match your app’s route

Problem 5: Container keeps restarting

Check logs:
docker logs app_service --tail 200 docker logs postgres_db --tail 200
Usually caused by:
bad config
missing env vars
DB migration errors
port already bound
app exiting immediately (wrong start command)

Conclusion

Docker Compose is one of the best tools for deploying a web app stack on a VPS/cloud because it gives you:
consistent environments (containers/images)
safe persistent storage (volumes)
clean service-to-service communication (networks)
repeatable deployments (
docker compose up -d
)
For a strong foundation, focus on:
1) correct installation of Docker + Compose
2) a clean project structure (Dockerfile for app, official images for infra)
3) persistent volumes for databases
4) Nginx reverse proxy to expose only ports 80/443
5) healthchecks + restart policies for resilience
6) safe updates, logs, and a real backup plan
This setup scales surprisingly far for small-to-medium production workloads—and it’s a solid stepping stone toward more advanced orchestration later

Komentar

Postingan populer dari blog ini

How to Install Let’s Encrypt SSL on Nginx/Apache + Auto-Renew (Without Common Errors)

Complete Beginner’s Guide to Domains & DNS: A Records, CNAME, TTL, Propagation, and Subdomains

How to Set Up a VPS From Scratch (Ubuntu): SSH Login, Secure User, Firewall, and Your First Web Deploy