This site contains affiliate links — we may earn a commission if you sign up through them.

Docker Compose Guide on DigitalOcean Droplet 2026

เครื่องมือปฏิบัติการสำหรับรัน multi-container Docker Compose stacks บน DigitalOcean Droplet ตั้งแต่ติดตั้ง การตั้งค่า และการตั้งค่า reverse proxy ไปจนถึงแนวทางปฏิบัติการสำรองข้อมูลที่ดีที่สุด

Docker Compose Guide on DigitalOcean Droplet 2026

Docker Compose is the standard tool for running multi-container applications on a single DigitalOcean Droplet without relying on complex orchestration like Kubernetes. This guide walks you through the entire process: installing Docker Engine on your Droplet, writing a real docker-compose.yml file, connecting volumes for persistent data storage, setting up a reverse proxy with Nginx, and tackling common problems you'll encounter in production.

Installing Docker + Docker Compose

DigitalOcean Droplets running Ubuntu 24.04 LTS are the most popular choice for Docker because the kernel and packages work seamlessly with the latest Docker Engine versions. For small-to-medium stacks (web app + database + cache), a Basic 2 GiB RAM / 2 vCPU Droplet at $18/month is usually sufficient; single test containers can use 1 GiB RAM at $6/month (pricing as of July 2026—verify current rates on the provider's website). The recommended installation method is Docker's convenience script, which pulls Docker Engine, CLI, and the Compose plugin in a single command: curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh After installation completes, add your current user to the docker group to avoid typing sudo every time: sudo usermod -aG docker $USER Then log out and back in once to activate the permissions. One important detail: the current version of Docker Compose is no longer a standalone program called docker-compose (with hyphen). Instead, it's a plugin built into Docker CLI and invoked via docker compose (with space). If you install via the convenience script or official apt repository, this plugin comes automatically. Verify it works with docker compose version alongside docker --version. If you're using a different distribution like CentOS or Alpine, add Docker's own repository according to their official guide because many distros' default package collections ship older or forked versions with slightly different syntax. Finally, enable the Docker service to start automatically whenever the Droplet reboots: sudo systemctl enable --now docker and configure DigitalOcean's Cloud Firewall (free—no extra cost) to allow only necessary ports such as 22 (SSH), 80/443 (HTTP/HTTPS) before running containers that expose services to the internet.

  1. Ubuntu 24.04 LTS + Docker convenience script recommended for fast installation with Compose plugin included in one command
  2. Basic 2 GiB RAM / 2 vCPU Droplet ($18/month) suits typical web+DB+cache stacks; 1 GiB ($6/month) sufficient for single test containers
  3. Add your user to the docker group with usermod -aG docker $USER then log back in to drop the sudo requirement

Writing docker-compose.yml with Multiple Containers

Based on real-world use, the current Compose Specification no longer requires the version: key at the top of the file because it now consolidates 2.x/3.x syntax into a single standard. The main file structure consists of the services: key listing individual containers, volumes: for persistent storage, and networks: for additional internal networking if needed beyond the default network Compose creates per project. Here's a sample stack with a web app and PostgreSQL database: services: web: build: . ports: - 3000:3000 depends_on: - db env_file: .env restart: unless-stopped db: image: postgres:16 volumes: - db_data:/var/lib/postgresql/data environment: POSTGRES_PASSWORD: ${DB_PASSWORD} restart: unless-stopped volumes: db_data: Notice that the web service uses build: . to build an image from a Dockerfile in the current directory, while the db service uses image: postgres:16 to pull a pre-built image directly from the registry with the version pinned explicitly instead of relying on latest. The ports: value maps host:container ports, and environment: sets variables the container reads on startup. If you don't want to commit secrets directly into the compose file, separate them into a .env file and reference it with env_file: .env or substitute values inline using ${DB_PASSWORD} syntax. A common misconception is that depends_on only controls the order containers start—it doesn't wait for the application inside to actually be ready to accept connections. For example, the db container might finish starting but PostgreSQL is still initializing. To wait for readiness, add a healthcheck: to the db service and use depends_on.condition: service_healthy in the web service. At the end of the file, volumes: db_data: declares a named volume so Docker manages the storage separately from the container's lifecycle.

  1. No need to include version: at the top of the file anymore—the current Compose Specification unifies all syntax into one standard
  2. build: . builds an image from your Dockerfile in the current directory; image: pulls a ready-made image from a registry
  3. depends_on controls startup order only, not whether the app inside is actually ready to accept requests—add healthcheck if you need to wait for true readiness
  4. env_file: .env keeps secrets like database passwords out of the compose file you commit to git
  5. Declare named volumes like db_data: at the end in the volumes: block so data persists even when containers are deleted

Running, Stopping, and Updating Containers

Once you've written docker-compose.yml, start the stack with docker compose up -d, which builds (if applicable), pulls missing images from registries, and runs all services in detached mode (background). Check the status of all containers in the project with docker compose ps and stream real-time logs from any service with docker compose logs -f web—extremely useful for debugging when an app doesn't respond or crashes after startup. When updating to a newer image version, the safe sequence is docker compose pull to fetch new images for all services with image: specified, then docker compose up -d --build to recreate only containers whose config or image actually changed—services that didn't change stay untouched, minimizing overall downtime. If you only modified a service's Dockerfile and want to rebuild it, use docker compose build --no-cache web && docker compose up -d web to force a complete rebuild without old cache layers. Stopping has two very different outcomes: docker compose down halts and removes containers and the project's network but keeps all named volumes intact—your data doesn't vanish. docker compose down -v also deletes volumes, suitable only when you truly want to wipe everything (like a test environment)—be extremely careful not to use this on production databases. For minor maintenance like restarting a single service without recreating it, use docker compose restart web, and to examine a running container's internals, use docker compose exec web sh. Over time, unused images, layers, and networks accumulate and consume your Droplet's SSD space, so periodically run docker system prune -f to clean them up.

  1. docker compose up -d runs all services in background (detached mode)
  2. docker compose ps and docker compose logs -f web check status and stream real-time logs
  3. docker compose pull && docker compose up -d --build fetches new images and recreates only changed containers
  4. docker compose down preserves volumes, unlike docker compose down -v which deletes them—watch out for data loss
  5. docker system prune -f removes unused images and layers periodically to prevent your Droplet's SSD from filling up

Connecting Volumes for Persistent Data

Containers are inherently stateless—when you delete a container, its filesystem data vanishes with it. Databases or uploaded files that must survive need to bind to a volume always. Docker offers two main types: named volumes like db_data:/var/lib/postgresql/data where Docker manages the storage itself in /var/lib/docker/volumes, and bind mounts like ./data:/var/lib/mysql that tie to a real path on the host, useful when you want to view or edit files directly from the host without entering the container. If your Droplet's main SSD begins to run low (a 2 GiB Droplet has 60 GB SSD, for example), DigitalOcean offers Block Storage Volumes you can attach separately without resizing the whole Droplet—$0.10/GiB/month, so 100 GiB costs $10/month. The steps are: create a Volume from the control panel or doctl, attach it to the Droplet, format it, and mount it at a path like /mnt/volume_sgp1_01 via fstab so it auto-mounts on reboot. Then use that path as the host side of a bind mount in your compose file: /mnt/volume_sgp1_01/pgdata:/var/lib/postgresql/data. This separates data from the Droplet's primary disk, making future backups, resizes, or migrations easier because the volume moves independently. Whether using named volumes or Block Storage Volumes, always maintain a separate backup process. Never rely on a single volume as your only copy of critical data. The simplest approach is to spin up a temporary container that tar's the data out of the volume and keeps it offline: docker run --rm -v db_data:/data -v $(pwd):/backup busybox tar czf /backup/db_data.tar.gz /data then move the backup file elsewhere, such as Spaces Object Storage.

Key takeaway: Named volumes (e.g., db_data:) let Docker manage storage in /var/lib/docker/volumes and are portable across docker operations only

Reverse Proxying with Nginx

This is important — instead of publishing each app's port directly to the internet, a more secure and manageable approach is to run Nginx as the single entry point (reverse proxy) and keep other containers on Compose's internal network. The simplest way is to add an nginx:alpine service to your compose file and mount your own config over the default: nginx: image: nginx:alpine ports: - 80:80 volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro depends_on: - web Inside nginx.conf, use proxy_pass http://web:3000; directly because containers on the same Compose network resolve service names to IPs automatically without needing to know the actual address. The advantage of this approach is that services like web or db don't need to publish ports to the host at all (remove ports: from those services) because only nginx exposes ports 80/443 to the internet—a significant reduction in attack surface. On DigitalOcean, enable Cloud Firewall (free, no extra charge) to allow only 80/443 inbound and block all other container ports from external access entirely. For HTTPS, a common setup pairs Nginx with Certbot to issue free certificates from Let's Encrypt and auto-renew every 90 days. Alternatively, use pre-built images like nginx-proxy with acme-companion that handle certificate issuance and renewal automatically based on environment variables for each container, all within your single compose file without writing nginx.conf manually. Either way, always test the config with nginx -t before reloading to catch syntax errors that could accidentally take your site offline.

When to Use This (Real Use Cases)

Docker Compose on a single Droplet works best for side projects, staging environments, internal tools, or MVPs with few services and small development teams—setup is quick, control is straightforward, and you don't need to learn complex orchestration concepts like pods, service meshes, or ingress controllers (Kubernetes). Costs stay low, limited to a single Droplet plus any additional Volumes or Load Balancers you add; there's no control-plane fee unlike some platforms. The critical limitation is that Docker Compose runs on one machine. If that Droplet crashes or reboots unexpectedly, all services in the compose file stop together—no automatic failover across nodes. This makes it unsuitable for systems requiring true high availability or traffic high enough to need horizontal scaling across multiple instances. Once your business grows to that point, consider migrating to Managed Kubernetes (DOKS) where the control plane is free and you pay for nodes starting at $12/month each, or App Platform if you'd rather not manage infrastructure yourself. When you do have multiple Droplets that need to communicate internally, use DigitalOcean's VPC (private networking) for free without per-connection charges. For new DigitalOcean accounts (never used a trial before), you receive a $200 trial credit valid for 60 days after signup (requires a credit card or PayPal per the current terms, as of July 2026). Sign up through this link to get the $200: DigitalOcean $200 Free Credit—more than enough to create a test Droplet, deploy the stack from this guide, and experiment for several weeks.

Common Errors and Solutions

The most frequent error is Error: port is already allocated, caused when a host port in ports: is already in use by another service on the Droplet (for instance, Nginx installed outside containers using port 80). Check which ports are in use with ss -tulpn, then either change the port mapping in your compose file or stop the conflicting service first. Another common startup issue is permission denied while trying to connect to the Docker daemon socket, usually because your user either wasn't added to the docker group or the SSH session hasn't picked up the new group membership yet. Log out and back in (or run newgrp docker temporarily). A later problem that accumulates over time is no space left on device, caused by old images, build cache, and unused layers piling up until the Droplet's SSD fills. Fix it with docker system prune -a to remove everything unused, or if your actual data is too large and you keep running out of room, attach a Block Storage Volume to add permanent space instead of repeatedly deleting. If a container enters a restart loop, don't just keep restarting it—open docker compose logs -f service_name to find the actual error first. Environment variables in .env not being read usually means the .env file isn't in the same directory as docker-compose.yml or the variable name doesn't match what compose expects. A rare but difficult issue is containers unable to reach the internet even though the network looks fine—sometimes Docker's iptables rules conflict with UFW rules on the host. In that case, add rules to Docker's DOCKER-USER chain instead of relying on UFW alone.

  1. Error "port is already allocated": another service on the host already uses that port; check with ss -tulpn and change the host mapping
  2. "permission denied ... docker.sock": user not in the docker group or session hasn't refreshed permissions; log out and back in
  3. "no space left on device": old images and layers pile up; run docker system prune -a or attach Block Storage Volume for permanent space

Best Practices

Always pin image versions explicitly, such as postgres:16 or nginx:1.27-alpine instead of latest, because latest can update to a newer version with breaking changes when you pull the next time without warning. Every service that runs continuously should have restart: unless-stopped so Docker automatically restarts the container if the Droplet reboots or the container crashes unexpectedly, and add healthcheck: so Compose knows not just "the container is running" but "the app inside is actually ready to handle requests." On security, always run processes inside containers as a non-root user by specifying USER in your Dockerfile—don't leave them running as root (the default for many base images) because a container breach becomes much more severe. Keep secrets like API keys and database passwords in a .env file that you never commit to git. For data safety, back up your Droplet with periodic Snapshots ($0.06/GiB/month) and additionally back up the actual data separately using the tar method mentioned earlier, because Snapshots capture the whole Droplet and aren't ideal for recovering just one database quickly. For visibility, enable DigitalOcean's free Monitoring (included with every Droplet) to graph CPU, RAM, and disk usage, so you can spot containers consuming memory dangerously close to OOM or disk almost full before the system crashes. Finally, make cleanup routine: use docker compose down --remove-orphans when removing services from your compose file to prevent orphaned containers hanging around after config, and run docker system prune periodically so your Droplet doesn't accumulate garbage images, networks, and old build cache that choke real usage.

Get $200 Free Credit →

Frequently Asked Questions

What's the difference between docker compose and docker-compose (with hyphen)?
docker-compose (with hyphen) is the older standalone Python program now deprecated, while docker compose (with space) is a plugin built into Docker CLI as of Compose V2, written in Go and faster with continuous updates. The docker-compose.yml file syntax is nearly identical between them—just the command changes. If you install via the convenience script or official repository, docker compose comes automatically.
What size Droplet do I need to run Docker Compose?
It depends on your services' count and weight. For a single test container or small app, Basic 1 GiB RAM / 1 vCPU at $6/month works fine. A typical stack with web app, database, and cache benefits from 2 GiB RAM / 2 vCPU at $18/month or higher. For many heavy services running together, start at 4 GiB at $24/month (pricing as of July 2026—verify current rates on the provider's website).
How do I keep database data from disappearing when I delete a container?
Always bind the path where the database stores files (e.g., /var/lib/postgresql/data) to a named volume or bind mount, never leave it in the container's bare filesystem. Additionally, maintain a separate backup process to store copies outside the volume, like tar'ing to Spaces Object Storage, because a single volume is still a single point of failure if the Droplet has a catastrophic problem.
When should I switch from Docker Compose to Kubernetes (DOKS)?
Docker Compose on one Droplet suits systems that don't yet need high availability across multiple machines or automatic scaling. Once traffic grows enough to require scaling across multiple nodes simultaneously, or if you need the system to stay running even when one node fails, consider Managed Kubernetes (DOKS) where the control plane is free and nodes start at $12/month each. You don't have to start with Kubernetes if your system isn't there yet.
How do I set up HTTPS for containers without extra cost?
Run Nginx as a reverse proxy paired with Certbot to issue free TLS certificates from Let's Encrypt and auto-renew every 90 days, or use a ready-made image like nginx-proxy with acme-companion that automates cert issuance and renewal inside your compose file. Both approaches have zero extra cost from DigitalOcean since Cloud Firewall allowing port 80/443 is already free.
What should I do if my Droplet runs out of space due to old Docker images?
Run docker system prune -a periodically to remove all unused images, containers, networks, and build cache. If your actual data (databases, uploads) is what's taking up space rather than old images, consider attaching a Block Storage Volume at $0.10/GiB/month instead of repeatedly deleting to make room, since it's more flexible and survives Droplet resizes.