July 12, 2026 · Docker HQ Team

How to Deploy Docker Compose to a Remote Server

To deploy Docker Compose to a remote server, copy your compose.yaml to the server over SSH, then run docker compose up -d on the host. If you'd rather stay on your laptop, point a Docker context at the remote host and run the same command locally against it. Both work; the context approach keeps your files in one place and skips the copy step.

This guide covers both, plus the parts that actually bite you: environment secrets, image builds, zero-downtime restarts, and how to tell whether the deploy worked.

Before you start

You need a few things in place:

  • SSH access to the server with a key (not a password). If you're still typing a password, set up key auth first.
  • Docker Engine and the Compose plugin installed on the server. Check with docker compose version over SSH.
  • A compose.yaml (or the older docker-compose.yml) that runs cleanly on your machine.

Quick check that the remote has what it needs:

ssh deploy@your-server 'docker version && docker compose version'

If Compose isn't there, install the plugin. On Debian/Ubuntu with Docker's official repo it ships as docker-compose-plugin.

Method 1: copy the file and run it on the server

This is the one most people start with, and it's fine for a single server.

Copy your compose file (and any config it references) to the server:

scp compose.yaml deploy@your-server:/opt/myapp/compose.yaml

Then SSH in and bring the stack up:

ssh deploy@your-server
cd /opt/myapp
docker compose up -d

The -d flag runs everything detached so the containers keep running after you log out. Compose pulls the images, creates a network, and starts the services in dependency order.

You can collapse that into a single command if you don't want an interactive session:

ssh deploy@your-server 'cd /opt/myapp && docker compose up -d'

When you push a new version, update the file and re-run the same up -d. Compose diffs the running state against the file and only recreates containers whose config or image changed. Services you didn't touch stay up.

Handling environment variables and secrets

Don't bake secrets into the compose file and don't commit them. Keep an .env file next to the compose file on the server. Compose reads it automatically:

scp .env.production deploy@your-server:/opt/myapp/.env

Lock down its permissions once it's there:

ssh deploy@your-server 'chmod 600 /opt/myapp/.env'

For anything genuinely sensitive (database passwords, API tokens), Docker secrets or a secrets manager is a better fit than a plain .env. But for most small deployments, a 600 env file owned by the deploy user is a reasonable line to hold.

Method 2: deploy with a Docker context (no file copy)

A Docker context lets your local CLI talk to a remote daemon over SSH. You run docker compose on your laptop, but everything executes on the server. No scp, no second copy of the file drifting out of sync.

Create the context once:

docker context create remote --docker "host=ssh://deploy@your-server"

Then run your normal commands against it:

docker --context remote compose up -d

Or switch to it so you don't repeat the flag:

docker context use remote
docker compose up -d      # now targets the server
docker context use default # switch back when you're done

One thing to know: when you build an image with a remote context, the build runs on the server, so the build context (your source) is sent over the SSH connection. For image-heavy builds that can be slow over a thin link. If that's your situation, build locally, push to a registry, and have the server pull. Which brings up the next question.

Build on the server, or push to a registry?

If your compose file has a build: section, you have two options.

Build on the server. Fine for small images and beefy servers. The server needs your source and a compiler toolchain in the build image. Just run docker compose up -d --build.

Build once, pull everywhere. Better for anything you deploy more than once, or to more than one host. Build and push locally:

docker compose build
docker compose push

Then on the server, pull and start:

ssh deploy@your-server 'cd /opt/myapp && docker compose pull && docker compose up -d'

This keeps the server doing one job (running containers) and makes rollbacks trivial. You just re-pull the previous tag. Pin your images to real version tags, not latest. latest makes it impossible to know what's actually running.

Verify your Docker Compose remote server deploy worked

up -d returning cleanly means Compose started the containers. It does not mean your app is healthy. A container can start, crash on a missing env var, and restart in a loop while docker ps briefly shows it as up.

Check the real state:

ssh deploy@your-server 'cd /opt/myapp && docker compose ps'

Look at the STATUS column. Up 2 minutes is good. Restarting or Up (unhealthy) is not. Tail the logs for anything that looks wrong:

ssh deploy@your-server 'cd /opt/myapp && docker compose logs --tail=50 -f'

Add a healthcheck to your services so Compose and Docker know when a container is genuinely ready to serve traffic, rather than just up. A web service checking its own /health endpoint catches the crash-loop case before it takes down traffic.

Staring at logs -f over SSH is fine when you're already at a keyboard. It's less fine when the deploy was an hour ago and the service falls over during dinner. This is roughly why we built Docker HQ so you can pull up compose ps, read logs, and open a shell on any host from your phone, and get a push alert the moment a container starts flapping instead of finding out from a customer. If you want alerting on remote hosts without wiring up your own stack, the plans page lays out what's included.

A minimal deploy script

Once you've done this a few times, wrap it so you stop mistyping paths at 2am:

#!/usr/bin/env bash
set -euo pipefail

HOST="deploy@your-server"
DIR="/opt/myapp"

scp compose.yaml "$HOST:$DIR/compose.yaml"
ssh "$HOST" "cd $DIR && docker compose pull && docker compose up -d --remove-orphans"
ssh "$HOST" "cd $DIR && docker compose ps"

--remove-orphans cleans up containers for services you deleted from the file. set -euo pipefail stops the script the moment any step fails, so a broken scp doesn't lead to a confusing half-deploy.

Don't forget the data

Named volumes hold your databases, uploads, and anything else that has to survive a container being recreated. docker compose down leaves volumes alone by default, which is what you want. But docker compose down -v deletes them, and people run that by reflex when debugging. Know the difference before you're tired.

Before any deploy that touches a database image or a volume mount, take a backup. We wrote up the mechanics in how to back up Docker volumes and restore them, and if you're running anything you care about, set the backups on a timer instead of remembering to do them, covered in scheduling automatic Docker volume backups. For keeping an eye on resource use and container health over time, the complete guide to Docker container monitoring goes deeper than the compose ps check above.

Frequently asked questions

What's the difference between docker-compose and docker compose?

docker-compose (with a hyphen) is the old standalone Python tool, now v1 and end-of-life. docker compose (a space) is the v2 plugin written in Go and shipped with modern Docker. Use the plugin. The commands are almost identical, so most old guides still apply, but new features land only in the plugin.

How do I update a single service without restarting the whole stack?

Name the service: docker compose up -d --no-deps web. The --no-deps flag stops Compose from also restarting the services web depends on. Compose recreates only that container if its image or config changed, and leaves everything else running.

Can I run docker compose on a remote server without copying my files?

Yes. Create a Docker context pointing at the host over SSH (docker context create remote --docker "host=ssh://user@server"), then run docker --context remote compose up -d. The CLI runs locally, the containers run on the server, and your compose file never leaves your machine.

Why does my container show as up but the app doesn't respond?

The process inside started but isn't serving yet, or crashed after start and is restarting. Run docker compose ps to check for a Restarting status and docker compose logs <service> to see why. Add a healthcheck so Docker reports the container as unhealthy instead of falsely up.

Next step

Pick one server and run Method 1 end to end, including the compose ps and logs check at the finish. Once that's muscle memory, add a healthcheck to each service. That single addition catches more bad deploys than anything else on this page.