Introduction
Docker Compose lets you define a multi-container application — a web app, its database, a cache, and a reverse proxy — in a single declarative file, then bring it all up with one command. Instead of running long docker run invocations by hand, you describe the desired state once and Compose reconciles it for you.
In this tutorial you will deploy a small but realistic stack on a Linux server: a web application, a PostgreSQL database, and a Caddy reverse proxy that terminates HTTPS automatically. By the end you will have a running app reachable over TLS at your own domain.
We will deploy on a Skyline Cloud VPS. Because Skyline runs in-Kingdom infrastructure in Saudi Arabia, your data and your customers' data stay under PDPL, NCA and SDAIA jurisdiction — which matters when you host workloads for KSA and GCC organisations. If you do not have a server yet, create one in minutes.
Prerequisites
- A Linux server (Ubuntu 22.04/24.04 or Debian 12) with a public IP. A 1–2 vCPU / 2 GB RAM VPS is enough to start.
- A non-root user with
sudoprivileges, and SSH access to the server. - A domain name with an A record pointing to your server's public IP. With a .sa domain and managed DNS from Skyline, create a record like
app.example.sa → 203.0.113.10.
Step 1 — Install Docker Engine and the Compose Plugin
Modern Docker ships Compose as a plugin (docker compose, not the legacy docker-compose). The official convenience script installs the engine and the plugin together:
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
Add your user to the docker group so you can run Docker without sudo, then re-open your session:
sudo usermod -aG docker $USER
newgrp docker
Verify both the engine and the Compose plugin:
docker --version
docker compose version
Step 2 — Lay Out the Project
Create a directory for your stack and move into it:
mkdir -p ~/myapp && cd ~/myapp
We will keep three files here: a Dockerfile for the app image, a compose.yaml that wires the services together, and a .env file for secrets and configuration.
Step 3 — Write the Application Dockerfile
For a typical Node.js app, a small multi-stage build keeps the final image lean. Adjust the commands for your own stack (Python, Go, PHP, etc.):
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Step 4 — Define Secrets in an .env File
Never hard-code credentials in your compose file. Put them in .env, which Compose reads automatically:
POSTGRES_USER=appuser
POSTGRES_PASSWORD=change-me-to-a-strong-secret
POSTGRES_DB=appdb
APP_DOMAIN=app.example.sa
Restrict its permissions and keep it out of version control:
chmod 600 .env
echo ".env" >> .gitignore
Step 5 — Write the Compose File
Create compose.yaml. This defines three services, a private network, and named volumes so your data and TLS certificates survive restarts:
services:
app:
build: .
restart: unless-stopped
environment:
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
depends_on:
db:
condition: service_healthy
expose:
- "3000"
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
depends_on:
- app
volumes:
db-data:
caddy-data:
caddy-config:
A few details worth noting:
- The database port is not published with
ports:; it is only reachable on the internal Compose network, so it is never exposed to the internet. depends_onwithcondition: service_healthymakes the app wait until PostgreSQL actually accepts connections, not just until its container starts.restart: unless-stoppedbrings your services back automatically after a reboot or crash.
Step 6 — Configure the Reverse Proxy
Create a file named Caddyfile next to your compose file. Caddy obtains and renews a Let's Encrypt certificate automatically, so you get HTTPS with no manual certificate handling:
app.example.sa {
reverse_proxy app:3000
}
Replace app.example.sa with your real domain. Caddy resolves app to your application container over the internal network and proxies requests to port 3000.
Step 7 — Launch the Stack
Build the images and start everything in the background:
docker compose up -d --build
Check that all services are healthy:
docker compose ps
Follow the logs to watch the app start and Caddy issue your certificate:
docker compose logs -f
Open https://app.example.sa in a browser. Caddy will already have provisioned a valid TLS certificate.
Step 8 — Operate and Update
Common day-to-day commands:
| Task | Command |
|---|---|
| View running services | docker compose ps |
| Tail logs for one service | docker compose logs -f app |
| Restart one service | docker compose restart app |
| Stop the stack (keep data) | docker compose down |
| Pull newer base images | docker compose pull |
| Rebuild and redeploy | docker compose up -d --build |
To ship a new version of your code, pull your changes and run docker compose up -d --build. Compose recreates only the containers that changed and leaves the database untouched, giving you near-zero downtime for stateless services.
To remove everything including volumes (this deletes your database data), run docker compose down -v — use it with care.
Step 9 — Back Up Your Data
Your application data lives in the db-data volume. Take regular dumps and store them off the server. A simple PostgreSQL backup:
docker compose exec db pg_dump -U appuser appdb > backup-$(date +%F).sql
Schedule this with cron and push the dumps to Skyline Cloud object storage or cloud backup so a single-server failure never costs you your data.
Conclusion
You now have a multi-container application deployed with Docker Compose, fronted by an automatic-HTTPS reverse proxy, running on a server you control. From here you can add a Redis cache, scale stateless services with docker compose up -d --scale app=3 behind the proxy, or graduate to orchestration — see our managed Kubernetes in Saudi Arabia guide when you outgrow a single host.
If you also need professional mailboxes for your domain alongside your app, Skyline business email hosting keeps your mail in-Kingdom too.
Ready to deploy? Create your Skyline Cloud server and get started.
Comments
0 total · 0 threads