Home Knowledge base Skyline Cloud How to Deploy an App with Docker Compose KNOWLEDGE BASE

How to Deploy an App with Docker Compose

A practical, step-by-step guide to deploying a multi-container application on a Linux server with Docker Compose — install Docker, write a production-ready compose file, add a reverse proxy with HTTPS, and run it reliably on Skyline Cloud in-Kingdom infrastructure.

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 sudo privileges, 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_on with condition: service_healthy makes the app wait until PostgreSQL actually accepts connections, not just until its container starts.
  • restart: unless-stopped brings 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.

SKYLINE Engineering

@skyline

The engineering team at SKYLINE Industrial Solutions. We publish field-tested guides drawn from real KSA and GCC deployments.

See author profile
SKYLINE engineering services

Need this implemented for you?

Reading is free — building it right takes a team. SKYLINE engineers ship Skyline Cloud for Aramco vendors, banks, hospitals and government agencies across Saudi Arabia. Talk to us before you start.

Aramco Approved Contractor ISO 9001 · ISO 27001 SAMA CSF aligned NCA ECC ready 247+ KSA clients

Comments

0 total · 0 threads
Be the first to leave a comment.