Category: Docker Compose

  • Docker Compose Project Name

    Docker Compose Project Name: A Complete Guide to Naming and Scoping Your Stacks

    Disclosure: This post contains one or more links to providers we have a real, registered affiliate/referral relationship with. We may earn a commission at no extra cost to you if you sign up through them.

    Every Docker Compose stack has an identity that goes beyond the services defined in its YAML file: the docker compose project name. This name controls how containers, networks, and volumes are labeled and grouped, and getting it wrong is one of the most common sources of confusing, duplicate, or orphaned resources on a development machine or a small VPS. This guide explains exactly how the docker compose project name is determined, how to override it, and how to use it deliberately when running multiple environments side by side.

    What Is a Docker Compose Project Name?

    When you run docker compose up, Compose doesn’t just create containers — it creates a project, a logical namespace that groups every resource belonging to that invocation. The docker compose project name is the string used to prefix container names, form the default network name, and tag every object Compose creates with a com.docker.compose.project label.

    If you never set it explicitly, Compose derives the docker compose project name from the name of the directory containing your compose.yaml (or docker-compose.yml) file, lowercased and stripped of characters that aren’t valid in Docker resource names. So a project living in /home/user/my-app/ becomes project name my-app, and a service named web in that file ends up running as a container called my-app-web-1.

    This matters more than it looks. Two checkouts of the same repository in differently named folders will get two different project names and therefore two entirely separate sets of containers, networks, and volumes — even though the compose.yaml file is byte-for-byte identical. Conversely, two unrelated projects that happen to share a folder name will collide.

    Why the Project Name Matters in Practice

    The docker compose project name isn’t cosmetic. It determines:

  • Container naming: <project>-<service>-<replica-number>
  • The default network name: <project>_default
  • Volume naming for any volumes not given an explicit external name
  • Which containers docker compose down, docker compose stop, and docker compose ps will target, since these commands operate per-project by default
  • If you’ve ever run docker compose up from two different directories that both contain services named db, and been surprised that they don’t conflict on the container name, the docker compose project name is the reason why — they were silently placed in separate namespaces.

    How Compose Determines the Docker Compose Project Name

    Compose resolves the project name using a defined precedence order, checked top to bottom until one is found:

    1. The -p / --project-name CLI Flag

    The most explicit method. Passing -p on the command line overrides everything else:

    docker compose -p staging-app up -d

    Every container, network, and volume created by this invocation will be scoped under staging-app, regardless of what the working directory or compose file is named.

    2. The COMPOSE_PROJECT_NAME Environment Variable

    If no -p flag is given, Compose checks for COMPOSE_PROJECT_NAME in the environment or in a .env file next to the compose file:

    export COMPOSE_PROJECT_NAME=my-service-prod
    docker compose up -d

    This is the preferred approach for CI/CD pipelines and deploy scripts, since it avoids repeating the flag on every command and keeps naming consistent across up, down, logs, and exec invocations.

    3. The name: Top-Level Key in compose.yaml

    Recent versions of the Compose Specification support declaring the project name directly inside the file:

    name: inventory-service
    
    services:
      api:
        image: inventory-api:latest
        ports:
          - "8080:8080"
      redis:
        image: redis:7-alpine

    This is arguably the most maintainable option for a project that’s meant to always run under one identity: it travels with the repository, so anyone who clones it and runs docker compose up gets the same project name without needing to remember an environment variable or a flag.

    4. The Directory Name (Default Fallback)

    If none of the above are set, Compose falls back to the basename of the directory holding the compose file, normalized to lowercase alphanumerics, hyphens, and underscores.

    Overriding the Docker Compose Project Name for Multiple Environments

    A common real-world need is running the same compose.yaml multiple times with different data and different container sets — for example, a staging copy and a production copy on the same host, or several client instances of the same application. The docker compose project name is exactly the mechanism designed for this.

    # Client A
    docker compose -p client-a --env-file .env.client-a up -d
    
    # Client B
    docker compose -p client-b --env-file .env.client-b up -d

    Because each invocation uses a distinct docker compose project name, both stacks can run concurrently on the same host without port, network, or container-name collisions (assuming the compose file itself doesn’t hardcode a fixed host port for every environment).

    Running Isolated Copies for Testing

    This same pattern is useful for spinning up a throwaway copy of a stack for integration testing without touching your regular development containers:

    docker compose -p test-run-$(date +%s) up -d --build
    # run your test suite against the ephemeral stack
    docker compose -p test-run-$(date +%s) down -v

    Because the project name is unique per run, you avoid any risk of a test accidentally reusing a volume or network from your main development stack.

    Checking and Auditing Project Names on a Running Host

    Once a host accumulates several stacks over time, it’s easy to lose track of which containers belong to which project. Compose and the Docker CLI both expose this information through labels.

    docker ps --format 'table {{.Names}}t{{.Label "com.docker.compose.project"}}'

    You can also list every distinct project currently known to Compose:

    docker compose ls

    This prints each project name Compose is aware of, along with its status (running, exited) and the config files associated with it — useful before running a docker compose down you don’t want to accidentally scope too broadly.

    Cleaning Up by Project Name

    Because down is scoped to the resolved project name, cleanup is precise as long as you’re consistent about which name you pass:

    docker compose -p client-a down -v

    This removes only client-a‘s containers, its default network, and (with -v) its anonymous volumes — leaving client-b and anything else on the host untouched. For a deeper walkthrough of down behavior, including what it does and doesn’t remove by default, see this guide to stopping stacks with Docker Compose.

    Common Mistakes and Pitfalls

    A few docker compose project name mistakes come up repeatedly enough to call out directly:

  • Renaming a project directory after deployment. Since the default project name is derived from the directory name, renaming /opt/my-app to /opt/myapp-v2 on a production host silently changes the docker compose project name on the next up, and Compose will create an entirely new set of containers and networks rather than recognizing the existing ones.
  • Relying on the default name in CI. If your pipeline checks out code into a temporary directory with a randomized name, your docker compose project name will change on every run unless you pin it explicitly with -p or COMPOSE_PROJECT_NAME.
  • Assuming docker compose down without -p targets “everything.” It only targets the resolved project for the current working directory. Run it from the wrong directory (or after a directory rename) and it may report nothing to remove, leaving old containers running.
  • Mixing project name conventions across teammates. If one developer sets COMPOSE_PROJECT_NAME in their shell profile and another doesn’t, the same repository can produce two different project names on two machines, breaking assumptions in scripts that reference container names directly.
  • Explicitly declaring the docker compose project name — either in the compose file’s name: key or in a checked-in .env file — removes this entire class of bug, since the name no longer depends on where the file happens to be checked out.

    Docker Compose Project Name and Networking

    Every project gets its own default bridge network, named <project>_default, unless the compose file defines custom networks. Containers within the same project can reach each other by service name over this network; containers in different projects cannot, by default, since they sit on separate networks entirely.

    name: shop-backend
    
    services:
      api:
        image: shop-api:latest
        depends_on:
          - db
      db:
        image: postgres:16
        environment:
          POSTGRES_PASSWORD: example
        volumes:
          - db-data:/var/lib/postgresql/data
    
    volumes:
      db-data:

    With this file, api can reach the database at hostname db because both containers share the shop-backend_default network. If you need cross-project communication — for instance, a reverse proxy stack that must reach containers in several application stacks — you typically declare a shared external network rather than relying on the default per-project one. If you’re running Postgres this way, the Postgres Docker Compose setup guide covers volume and environment configuration in more depth, and the Docker Compose volumes guide is useful background on how named volumes like db-data above are scoped per project.

    Interaction With Environment Variables and Secrets

    The docker compose project name doesn’t just affect naming — it can also be referenced inside your compose file and .env file for building dynamic values, such as prefixing a database name or a bucket path per environment. Managing this cleanly usually means keeping environment-specific values out of the compose file itself. The Docker Compose env variables guide and the related environment variables reference both go into detail on .env file precedence, which is worth understanding alongside project naming since both are resolved from the same working directory context. If you’re also handling passwords or API keys, pair project-name discipline with proper secret handling as described in the Docker Compose secrets guide rather than placing them directly in environment variables.

    Choosing a Naming Convention

    For teams running several stacks on shared infrastructure, a consistent docker compose project name convention avoids ambiguity months later when nobody remembers which container belongs to which client or environment. A reasonable pattern:

    name: <service>-<environment>

    For example: inventory-staging, inventory-prod, billing-staging, billing-prod. This keeps docker compose ls output readable and makes docker ps output self-explanatory even on a host running a dozen stacks.

    If you’re deploying several such stacks to a single VPS, make sure the host has enough headroom in memory and disk before you start stacking projects — an unmanaged VPS hosting guide is a good reference for what to check before committing to a given instance size. For hosts specifically, DigitalOcean, Hetzner, and Vultr are commonly used providers for running small-to-medium Compose deployments.


    Recommended: Ready to put this into practice? DigitalOcean is a tool we use for exactly this, and we have a real, disclosed affiliate relationship with them.

    Controlling the Project Name to Affect All Container Names at Once

    Rather than setting container_name on every service, you can control the prefix applied to all of them by setting the Compose project name. This affects containers, networks, volumes, and default hostnames together, which is often what you actually want.

    docker compose -p myapp up -d

    Or persist it via an environment variable so you don’t have to remember the flag every time:

    export COMPOSE_PROJECT_NAME=myapp
    docker compose up -d

    You can also set name: at the top level of the compose file itself (supported in the Compose Specification):

    name: myapp
    
    services:
      api:
        image: node:20-alpine
      db:
        image: postgres:16

    This is a good middle ground between the default and the fully-manual container_name approach — it keeps the automatic <project>-<service>-<n> numbering (so scaling still works) while making sure the project prefix is stable no matter where the checkout lives on disk.

    Environment Files and .env Interaction

    Compose automatically reads a .env file in the project directory, and COMPOSE_PROJECT_NAME can be set there too. This is useful in CI pipelines where the checkout path is unpredictable but you still want deterministic container and network names between runs. If you’re managing secrets and environment variables alongside naming, it’s worth reading through a guide on managing Compose environment variables the right way to keep the two concerns properly separated.

    Using Hostnames Alongside Container Names

    Container names and network hostnames are related but not identical concepts. By default, Compose creates a network for the project and registers each service’s container name (or the service name itself) as a resolvable hostname on that network. This means other containers on the same Compose network can reach db by that name regardless of what the actual container_name is set to, because Compose’s embedded DNS resolves service names automatically.

    services:
      api:
        image: node:20-alpine
        container_name: myapp-api
        environment:
          DATABASE_URL: postgres://user:pass@db:5432/appdb
      db:
        image: postgres:16
        container_name: myapp-db

    Notice that DATABASE_URL references db, the service name, not myapp-db, the container name. This distinction trips people up constantly: service names are what other containers use for internal DNS resolution, while container_name is what you see in docker ps, docker logs, and host-level tooling. You can also override the internal hostname explicitly with hostname: if you need it to differ from the service name for some reason.

    Debugging Name Conflicts

    If you try to start a stack and get an error like “container name already in use,” it’s almost always because a previous container with that fixed container_name is still around — stopped but not removed. Docker won’t let two containers share a name even if one is stopped. Cleaning this up is usually a matter of running docker compose down before docker compose up again, but if you’re troubleshooting a stuck container manually:

    docker rm -f myapp-api
    docker compose up -d api

    For a systematic breakdown of what docker compose down actually removes (and what it leaves behind, like named volumes), see this full guide to stopping stacks. If containers seem to be starting but immediately failing, checking logs is the next step — this debugging guide to Compose logs covers the common patterns for tracing failures back to their cause.

    Renaming Existing Containers Without Recreating Them

    Sometimes you inherit a stack where containers were never given explicit names, and you want to fix that without tearing everything down. Docker does support renaming a running container directly with docker rename, independent of Compose:

    docker rename myapp-api-1 myapp-api

    This works, but it’s a trap if you don’t also update your compose file: the next time you run docker compose up, Compose compares the current container against what the compose file describes, notices the container name doesn’t match what it expects (myapp-api-1), and will typically create a brand-new container rather than adopting the renamed one. If you want a name to stick permanently, always add container_name to the compose file rather than relying on a one-off docker rename call.

    Recreating Containers Safely After a Naming Change

    If you do update the compose file to add or change a container_name, the cleanest way to apply it is:

    docker compose up -d --force-recreate

    This tells Compose to recreate containers even if it thinks nothing else changed, which forces the new name to take effect. For stateful services like databases, make sure your data lives in a named volume (not an anonymous one) before doing this, so the recreate doesn’t lose data — the Postgres Compose setup guide and the Redis Compose setup guide both cover volume configuration in detail if you’re naming containers for stateful services specifically.

    Naming Considerations in Multi-Environment Setups

    Teams that run the same compose file across development, staging, and production often hit naming collisions when multiple environments run on the same Docker host — for instance, a shared CI runner. A few approaches handle this cleanly:

  • Use COMPOSE_PROJECT_NAME (or the -p flag) set per environment, e.g. myapp-staging vs. myapp-prod, and avoid hardcoded container_name values entirely so the project prefix does the differentiating work.
  • If you do need fixed container_name values (for external monitoring integration, say), interpolate the environment into the name using a variable: container_name: myapp-api-${ENVIRONMENT:-dev}.
  • Keep environment-specific overrides in a separate compose.override.yaml file rather than duplicating the whole base file, which also makes diffs between environments easier to review.
  • If your naming strategy also needs to account for image rebuilds after a Dockerfile change, it’s worth pairing this with a look at how docker compose rebuild and docker compose up --build interact with existing containers, covered in the Compose rebuild guide. And if you’re still deciding between a single Dockerfile and a full Compose setup for a small project, the Dockerfile vs Docker Compose comparison is a useful starting point before naming even becomes a concern.

    Hosting Considerations for Multi-Container Stacks

    Naming discipline matters more as a stack grows, and stack size is often driven by where you’re hosting it. A small VPS with limited memory encourages fewer, well-named services; a larger box lets you run full staging and production stacks side by side, which is exactly the scenario where consistent project and container naming saves the most debugging time. If you’re evaluating VPS providers for running a multi-container Compose stack, DigitalOcean and Vultr both offer straightforward Docker-ready images that make it easy to standardize your naming conventions from the first deploy.

    Container Naming in CI/CD and Automated Deployments

    In automated pipelines, a stable docker compose container name is often more useful than in manual development, because scripts and health checks need something predictable to target. A typical pattern in a deploy script:

    #!/usr/bin/env bash
    set -euo pipefail
    
    docker compose -f docker-compose.prod.yml up -d
    docker exec app-backend curl -f http://localhost:3000/health || exit 1

    Here, app-backend is a fixed name set in the compose file, and the deploy script references it directly rather than parsing docker compose ps output to discover the generated name. This is a small thing, but it removes a class of fragile string-parsing from deploy scripts. If you’re running this kind of pipeline on a self-managed server rather than a managed platform, a guide on self-hosting n8n with Docker shows a comparable pattern of naming containers explicitly for automation reliability in a real production stack.

    Combining Fixed Names with Health Checks

    Compose’s healthcheck directive works independently of container_name, but the two are often used together in production, since a fixed name makes it trivial to query a specific container’s health status externally:

    docker inspect --format='{{.State.Health.Status}}' app-backend

    This pattern is common in monitoring scripts and works cleanly as long as the container name doesn’t change between deployments — another reason to standardize on explicit names for critical, single-instance services early rather than retrofitting them later.

    FAQ

    Does changing the docker compose project name destroy my existing containers?
    No, but it does orphan them. Compose won’t automatically stop or remove containers from the old project name — they’ll keep running under their original name while a new up under the new project name creates a fresh, separate set of containers. You’d need to manually stop and remove the old ones, or reference them with the old -p value to bring them down cleanly.

    Can two Compose projects share the same volume?
    Yes, but only if you declare the volume as external: true and reference its real name rather than letting Compose scope it under the project. Otherwise each project gets its own volume, even if the volume key in the YAML is spelled identically.

    Is the docker compose project name case-sensitive?
    Compose normalizes project names to lowercase, along with converting other unsupported characters. If you set COMPOSE_PROJECT_NAME=MyApp, Compose will still resolve resource names using a lowercased form internally, so it’s best to just write project names in lowercase from the start to avoid confusion.

    What happens if I don’t set a docker compose project name and run the same compose file from two different clones of the same repo?
    Each clone’s directory name becomes the project name by default. If both clones share the same directory name (for example, both cloned as myapp), you’ll get a naming collision the moment both try to run concurrently on the same host. If the directory names differ, you’ll instead get two silently separate stacks, which is usually not what you want either — this is precisely the scenario where declaring name: explicitly in the compose file avoids surprises.

    Conclusion

    The docker compose project name is a small piece of configuration with outsized consequences: it decides how your containers are named, how they’re networked, and which ones a given docker compose down or docker compose stop will actually touch. Relying on the directory-name default works fine for a single, casually-run project, but anything running in CI, on a shared host, or in multiple environments benefits from setting the docker compose project name explicitly — via the name: key in the compose file, COMPOSE_PROJECT_NAME, or the -p flag. Doing so removes an entire category of “why are there two databases running” debugging sessions, and makes cleanup and auditing predictable as the number of stacks on a host grows. For the full command reference on project scoping and other flags, the official Docker Compose CLI reference and the Compose Specification documentation are the authoritative sources to check against your installed version.

  • Keycloak Docker Compose

    Keycloak Docker Compose: A Complete Setup and Configuration Guide

    Disclosure: This post contains one or more links to providers we have a real, registered affiliate/referral relationship with. We may earn a commission at no extra cost to you if you sign up through them.

    Running Keycloak in a container is the fastest way to get a production-grade identity and access management server working on a VPS or local machine. This guide walks through a complete keycloak docker compose setup, from the minimal file to a hardened, database-backed configuration you can actually run in production.

    Keycloak is an open-source identity provider that handles authentication, authorization, single sign-on, and user federation for your applications. Instead of installing Java, downloading a distribution archive, and wiring up a database by hand, a keycloak docker compose file lets you define the entire stack — Keycloak plus its database — in one declarative document and bring it up with a single command. This article covers the base configuration, database persistence, environment variables, reverse proxy setup, networking, and common troubleshooting steps.

    Why Use Docker Compose for Keycloak

    Keycloak ships an official container image, and running it directly with docker run works fine for a five-minute test. But real deployments need a database, persistent volumes, environment configuration, and usually a reverse proxy in front for TLS termination. A keycloak docker compose file captures all of that in one place, checked into version control, reproducible on any host.

    Compared to a Kubernetes-based deployment, Docker Compose is simpler to reason about for a single-node setup. You don’t need a control plane, ingress controller, or Helm chart — just a docker-compose.yml and a .env file. If you eventually outgrow a single node, migrating from Compose to Kubernetes is a well-understood path; see Kubernetes vs Docker Compose: Which Should You Use? for a breakdown of when that migration is worth it.

    Who Should Use This Approach

    This setup is a good fit for:

  • Small to mid-sized teams running their own SSO for internal apps or a handful of customer-facing services.
  • Developers who want a local Keycloak instance that mirrors production configuration.
  • Anyone replacing a SaaS identity provider with a self-hosted one to control cost or data residency.
  • If you’re already running other services with Compose — a database, an automation tool like n8n, or a monitoring stack — adding Keycloak to the same host follows the same pattern you’re used to.

    Building the Base Keycloak Docker Compose File

    The minimal keycloak docker compose configuration needs two services: Keycloak itself and a Postgres database. Keycloak ships with an embedded H2 database, but that’s explicitly documented as unsuitable for production, so a real database is not optional for anything beyond a quick local test.

    services:
      postgres:
        image: postgres:16
        restart: unless-stopped
        environment:
          POSTGRES_DB: keycloak
          POSTGRES_USER: keycloak
          POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
        volumes:
          - keycloak_pg_data:/var/lib/postgresql/data
        networks:
          - keycloak_net
    
      keycloak:
        image: quay.io/keycloak/keycloak:25.0
        restart: unless-stopped
        command: start
        environment:
          KC_DB: postgres
          KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
          KC_DB_USERNAME: keycloak
          KC_DB_PASSWORD: ${POSTGRES_PASSWORD}
          KC_HOSTNAME: ${KEYCLOAK_HOSTNAME}
          KC_HOSTNAME_STRICT: "false"
          KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN_USER}
          KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
          KC_PROXY: edge
        ports:
          - "127.0.0.1:8080:8080"
        depends_on:
          - postgres
        networks:
          - keycloak_net
    
    volumes:
      keycloak_pg_data:
    
    networks:
      keycloak_net:

    This is the shape almost every keycloak docker compose setup converges on: one database service, one Keycloak service, a named volume for durability, and a private network so the two containers can talk to each other without exposing Postgres to the host.

    Choosing the Right Keycloak Image Tag

    Always pin the Keycloak image to a specific version (quay.io/keycloak/keycloak:25.0 above, not latest). Keycloak’s release notes regularly include breaking changes to configuration keys and startup behavior between major versions, and an unpinned tag means your stack can change behavior on a routine docker compose pull without warning. Check the Keycloak documentation for the migration notes before jumping multiple versions.

    Running in Production Mode vs Development Mode

    The start command in the example above runs Keycloak in production mode, which enforces hostname configuration and expects TLS to be handled somewhere (either by Keycloak itself or, more commonly, by a reverse proxy in front of it). Keycloak also has a start-dev command intended purely for local testing — it disables several production safeguards and should never appear in a keycloak docker compose file meant for anything beyond a laptop experiment.

    Configuring Environment Variables and Secrets

    Hardcoding admin passwords and database credentials directly in docker-compose.yml is a common mistake. Instead, use a .env file alongside the compose file and reference variables with ${VARIABLE_NAME} syntax, as shown above.

    # .env
    POSTGRES_PASSWORD=change_me_to_a_real_secret
    KEYCLOAK_ADMIN_USER=admin
    KEYCLOAK_ADMIN_PASSWORD=change_me_to_a_real_secret
    KEYCLOAK_HOSTNAME=auth.example.com

    Docker Compose automatically loads a .env file in the same directory, so no extra flags are needed. Add .env to .gitignore so secrets never end up in version control. For a deeper look at variable precedence, override files, and multi-environment setups, see Docker Compose Env: Manage Variables the Right Way.

    For anything beyond a small personal deployment, consider moving secrets out of plain .env files entirely and into Docker Compose’s native secrets support, which mounts values as files inside the container rather than as environment variables visible in docker inspect output. The pattern is covered in detail in Docker Compose Secrets: Secure Config Management Guide.

    Setting Up the Keycloak Admin User

    The KEYCLOAK_ADMIN and KEYCLOAK_ADMIN_PASSWORD environment variables only take effect on the very first startup, when Keycloak initializes its database schema and creates the bootstrap admin account. If you change these variables after the first run, Keycloak won’t retroactively update the admin account — you’ll need to create additional admin users through the admin console or the kc.sh CLI inside the container instead.

    Persisting Data and Managing the Database

    Every keycloak docker compose setup needs a persistence strategy for two things: the Postgres data directory and, if you use file-based realm exports, the realm configuration itself.

    The named volume keycloak_pg_data in the example above ensures the database survives container restarts and docker compose down (without the -v flag). If you’re new to how volumes interact with the container lifecycle, Docker Compose Down: Full Guide to Stopping Stacks explains exactly what gets removed and what persists under each variant of the down command.

    For a closer look at tuning Postgres itself as a Compose service — connection limits, resource constraints, backup volumes — see Postgres Docker Compose: Full Setup Guide for 2026 or the community-maintained image details covered in PostgreSQL Docker Compose: Full Setup Guide 2026.

    Exporting and Importing Realm Configuration

    Realms, clients, roles, and identity provider mappings can be exported to JSON and mounted into the container at startup, which is useful for reproducible environments (dev, staging, production all starting from the same baseline realm).

    docker compose exec keycloak /opt/keycloak/bin/kc.sh export 
      --dir /opt/keycloak/data/export --realm myrealm

    To import on a fresh container, mount the exported directory as a volume and add --import-realm to the startup command, pointing Keycloak at the mounted path.

    Backing Up the Database Independently

    Even with a named volume, you should take regular logical backups of the Postgres database rather than relying solely on the volume surviving. A simple cron-driven pg_dump against the postgres service, piped to a file outside the container, is enough for most small deployments:

    docker compose exec -T postgres pg_dump -U keycloak keycloak > keycloak_backup.sql

    Setting Up a Reverse Proxy in Front of Keycloak

    Running Keycloak behind a reverse proxy handles TLS termination and lets you serve it on the standard HTTPS port without giving the container access to port 443 directly. Nginx, Caddy, and Traefik are all common choices; Cloudflare in front of the proxy adds another layer of caching and DDoS protection, which is covered from a different angle in Cloudflare Page Rules: Complete Setup & Optimization Guide.

    A minimal Nginx server block proxying to the Keycloak container looks like this:

    server {
        listen 443 ssl;
        server_name auth.example.com;
    
        location / {
            proxy_pass http://127.0.0.1:8080;
            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;
        }
    }

    The KC_PROXY: edge setting in the compose file’s environment block tells Keycloak to trust the X-Forwarded-* headers from the proxy rather than expecting TLS to terminate inside the container itself. Without this setting, Keycloak generates incorrect redirect URLs (http:// instead of https://), which is one of the most common issues reported after a first keycloak docker compose deployment goes live behind a proxy.

    Health Checks and Startup Ordering

    depends_on in Compose only waits for the dependent container to start, not for Postgres to be ready to accept connections. Keycloak retries its database connection internally, so a bare depends_on is usually tolerable, but adding an explicit health check makes startup ordering deterministic and failures easier to diagnose:

      postgres:
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U keycloak"]
          interval: 5s
          timeout: 5s
          retries: 5

    Then reference it with depends_on: { postgres: { condition: service_healthy } } on the Keycloak service so it genuinely waits.

    Networking, Ports, and Scaling Considerations

    In the base example, Keycloak binds to 127.0.0.1:8080, meaning the port is only reachable from the host itself — the reverse proxy handles all external traffic. This is deliberate: never expose Keycloak’s admin console directly to the public internet without a proxy and TLS in front of it.

    If you’re deploying multiple containers on the same host — Keycloak, its database, and unrelated services like an automation engine — keep each stack on its own Compose network to avoid unintended cross-talk. The general pattern for managing multiple services this way is the same one used in n8n Self Hosted: Full Docker Installation Guide 2026, where a similar database-plus-app pairing runs in isolation on its own bridge network.

    For debugging container startup issues or watching Keycloak’s Java stack traces during a failed boot, docker compose logs -f keycloak is the first command to reach for — a full walkthrough of filtering, following, and exporting logs from a Compose stack is in Docker Compose Logs: The Complete Debugging Guide.

    Where to Host Your Keycloak Compose Stack

    Keycloak with Postgres is not a heavy workload for small-to-medium user bases, but the JVM has a meaningful baseline memory footprint — plan for at least 2GB of RAM dedicated to the stack, more as realm and session counts grow. A small managed VPS is enough for most self-hosted identity setups; providers like DigitalOcean and Hetzner both offer VPS tiers suitable for a keycloak docker compose deployment serving a handful of applications, and either lets you resize the instance later if session volume grows.

    Upgrading Keycloak Versions Safely

    When bumping the image tag, always check the release notes for database migration steps first — Keycloak automatically migrates its schema on startup when the version changes, but rolling back afterward is not supported. Take a database backup before any version bump, apply the change to a staging copy of the stack first if you have one, and only then update production. If you need to rebuild the stack after changing the compose file itself (not just the image tag), Docker Compose Rebuild: Complete Guide & Best Tips covers the difference between up, up --build, and a full recreate.


    Recommended: Ready to put this into practice? DigitalOcean is a tool we use for exactly this, and we have a real, disclosed affiliate relationship with them.

    FAQ

    Does a keycloak docker compose setup need a separate database container?
    Technically no — Keycloak includes an embedded H2 database for quick testing — but Keycloak’s own documentation is explicit that H2 is unsupported for production. Any real deployment should run Postgres, MySQL, or another supported database as a separate service in the same compose file.

    Why does Keycloak redirect to http:// instead of https:// behind my reverse proxy?
    This almost always means the KC_PROXY environment variable isn’t set, or your proxy isn’t forwarding the X-Forwarded-Proto header. Set KC_PROXY: edge in the Keycloak service’s environment and confirm your proxy config passes that header through, as shown in the reverse proxy section above.

    Can I run Keycloak and my application in the same docker-compose.yml?
    Yes, and it’s common for smaller deployments — just put both services in the same file on the same network so the application can reach Keycloak by its service name (e.g., http://keycloak:8080) for token validation, while external users still go through the reverse proxy.

    How do I reset the Keycloak admin password after the first startup?
    The KEYCLOAK_ADMIN_PASSWORD variable only applies on initial database creation. After that, reset it via docker compose exec keycloak /opt/keycloak/bin/kc.sh bootstrap-admin commands, or through the admin console itself if you still have console access.

    Conclusion

    A keycloak docker compose file gives you a reproducible, version-controlled way to run a full identity provider — Keycloak plus its database — with a single docker compose up. The pieces that matter most are using a real database instead of the embedded H2 store, keeping secrets out of the compose file itself, configuring KC_PROXY correctly when running behind a reverse proxy, and treating the Postgres volume and regular pg_dump backups as equally important. Start with the minimal two-service file above, then layer in secrets management, health checks, and a reverse proxy as your deployment moves from a local test toward production. For official configuration reference beyond what’s covered here, the Keycloak Server Administration Guide and Docker’s Compose file reference are the two primary sources worth bookmarking.

  • Deploying Docker Compose

    Deploying Docker Compose: A Practical Guide for Production Teams

    Disclosure: This post contains one or more links to providers we have a real, registered affiliate/referral relationship with. We may earn a commission at no extra cost to you if you sign up through them.

    Deploying Docker Compose to a real server involves more than running docker compose up and hoping for the best. This guide walks through the practical steps of deploying Docker Compose applications reliably, covering configuration, secrets, networking, and the day-to-day operational habits that separate a fragile setup from a stable one.

    Docker Compose was originally built for local development, but it has become a genuinely useful tool for deploying small-to-medium production workloads on a single host or a small cluster. Whether you’re standing up a self-hosted automation stack, a database-backed web app, or an internal tool, deploying Docker Compose correctly means thinking about restart policies, environment separation, logging, and rollback strategy from the start rather than retrofitting them later.

    Why Deploying Docker Compose Still Makes Sense in 2026

    Kubernetes gets most of the attention in infrastructure conversations, but not every workload needs an orchestrator with dozens of moving parts. For a single VPS, a small team, or a project that doesn’t need horizontal autoscaling, deploying Docker Compose is often the more maintainable choice. It keeps the entire application topology in one readable file, requires no control-plane overhead, and lets a single engineer reason about the whole system without needing a dedicated platform team.

    That said, Compose isn’t a toy. Production use requires the same discipline you’d apply to any deployment: version-controlled configuration, secrets kept out of the repository, health checks, and a repeatable rollout process. If your workload later outgrows a single host, it’s worth reading a comparison like Kubernetes vs Docker Compose to understand when the tradeoff actually flips in Kubernetes’ favor.

    When a Single Host Is Enough

    A single well-specced VPS can comfortably run a Compose stack for most internal tools, small SaaS products, and automation platforms — things like a self-hosted n8n instance, a WordPress site, or a small API backend. The deciding factor isn’t request volume alone; it’s whether you need automatic failover across multiple machines. If one host going down for a few minutes during a restart is acceptable, Compose is usually sufficient.

    When to Reconsider

    If you need zero-downtime rolling deployments across multiple nodes, built-in service discovery across a cluster, or automated horizontal scaling based on load, you’re better served by an orchestrator. Deploying Docker Compose past that point usually means bolting on scripts to replicate what Kubernetes or Nomad already do natively — at which point the “simplicity” argument for Compose no longer holds.

    Preparing Your Compose File for Production

    Before deploying Docker Compose to any server, the compose.yaml file itself needs to look different from a local dev version. Development compose files often bind-mount source code, expose every port to 0.0.0.0, and skip restart policies entirely. None of that belongs in production.

    A production-ready Compose file typically includes:

  • Explicit image tags (never latest) so redeploys are reproducible
  • A restart: unless-stopped or restart: always policy on every long-running service
  • Named volumes instead of bind mounts for persistent data
  • Health checks so Docker (and your monitoring) can tell when a container is actually ready
  • Resource limits (deploy.resources.limits or the legacy mem_limit/cpus fields) to stop one container from starving the host
  • Here’s a minimal example of a production-oriented Compose file for a small web app with a database:

    services:
      web:
        image: registry.example.com/myapp:1.4.2
        restart: unless-stopped
        depends_on:
          db:
            condition: service_healthy
        environment:
          - DATABASE_URL=postgres://app:${DB_PASSWORD}@db:5432/app
        ports:
          - "127.0.0.1:8080:8080"
        healthcheck:
          test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
          interval: 30s
          timeout: 5s
          retries: 3
    
      db:
        image: postgres:16
        restart: unless-stopped
        environment:
          - POSTGRES_USER=app
          - POSTGRES_PASSWORD=${DB_PASSWORD}
          - POSTGRES_DB=app
        volumes:
          - db_data:/var/lib/postgresql/data
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U app"]
          interval: 10s
          timeout: 5s
          retries: 5
    
    volumes:
      db_data:

    Note that the web port binds only to 127.0.0.1, with a reverse proxy (Nginx, Caddy, or Traefik) handling public traffic and TLS termination. Exposing application containers directly on 0.0.0.0 is a common mistake when deploying Docker Compose to a public-facing VPS. For a Postgres-specific walkthrough, the Postgres Docker Compose setup guide covers backup and tuning details this example skips.

    Pinning Versions and Managing Image Tags

    Using latest for any image in a deployment is a reliable way to introduce surprise breakage. When a base image updates, your next docker compose pull can silently change the application behavior, dependencies, or even the entrypoint. Pin exact versions (postgres:16.4, not postgres:latest) and treat a version bump as a deliberate, reviewed change, not something that happens automatically on a routine deploy.

    Handling Secrets and Environment Variables Correctly

    Deploying Docker Compose safely means never committing real credentials to version control. The most common approach is a .env file, read automatically by Compose and referenced with ${VARIABLE} syntax in the compose file, combined with .gitignore to keep it out of the repository.

    For anything more sensitive — API keys, database passwords, TLS private keys — Compose’s native secrets: block (backed by files rather than environment variables) reduces the risk of a secret leaking into docker inspect output or process listings. Environment variables are visible to any process that can read /proc/<pid>/environ on the host, while file-based secrets mounted at /run/secrets/ are scoped more tightly.

    Two site resources go into more depth here:

  • Docker Compose Env: Manage Variables the Right Way covers .env file precedence rules and common variable-substitution pitfalls.
  • Docker Compose Secrets: Secure Config Management Guide covers the secrets: block itself, including Docker Swarm’s encrypted secret store versus the plain-file approach used in standalone Compose.
  • Separating Environments Cleanly

    A common pattern when deploying Docker Compose across dev, staging, and production is to use Compose’s file-layering feature: a base compose.yaml with shared service definitions, plus compose.prod.yaml and compose.dev.yaml overrides that supply environment-specific values.

    docker compose -f compose.yaml -f compose.prod.yaml up -d

    This avoids maintaining three near-duplicate files and keeps the differences between environments explicit and reviewable in a diff.

    Choosing and Provisioning a Server

    Deploying Docker Compose to production starts with picking infrastructure that matches the workload. A small automation stack or single-app deployment usually needs 2-4 vCPUs, 4-8 GB of RAM, and enough disk for logs, images, and any database volumes — specifics depend heavily on your actual services, so size based on your own container resource usage rather than a generic recommendation. Providers like DigitalOcean, Hetzner, and Vultr all offer VPS tiers suited to running a Compose stack, with straightforward Docker-ready images available at provisioning time.

    Once the server exists, install Docker Engine and the Compose plugin per the official Docker installation instructions, and confirm the daemon is running before deploying anything.

    Firewalling and Network Exposure

    Deploying Docker Compose without firewall rules is one of the more common production mistakes. Docker manipulates iptables directly to publish container ports, which means a UFW rule blocking a port at the OS level can still be bypassed by a container’s own port mapping. Bind container ports to 127.0.0.1 where a reverse proxy is used, and confirm with iptables -L -n (not just ufw status) that only the ports you intend are actually reachable from outside.

    The Deployment Workflow Itself

    A repeatable deployment workflow for Compose generally follows the same shape regardless of what the application does:

    1. Pull or build the latest tagged image
    2. Copy the updated compose.yaml/override files and .env to the server (or check out from git)
    3. Run docker compose pull to fetch new images
    4. Run docker compose up -d to recreate only the containers whose configuration changed
    5. Verify health checks pass and application logs look clean
    6. Roll back to the previous image tag if anything looks wrong

    # on the target server, inside the project directory
    git pull origin main
    docker compose pull
    docker compose up -d
    docker compose ps

    docker compose up -d is idempotent — it only recreates containers whose image or configuration actually changed, leaving untouched services running. This is one of the underrated strengths of deploying Docker Compose over a naive “stop everything, start everything” script, since it minimizes downtime for services that didn’t change.

    Zero-Downtime Considerations

    Standalone Compose doesn’t have native rolling-update support the way Swarm or Kubernetes does — recreating a container means a brief gap between the old container stopping and the new one becoming healthy. For workloads where even a few seconds of downtime matters, options include running two instances behind a reverse proxy and swapping traffic manually, or migrating to Docker Swarm mode (which understands Compose files natively) for its update_config rolling-deploy behavior. For most internal tools and low-traffic sites, a few seconds of downtime during a deploy is an acceptable tradeoff for the simplicity Compose provides.

    Rebuilding After Code or Dockerfile Changes

    If you’re deploying Docker Compose for an application you build from source rather than pulling a prebuilt image, remember that docker compose up -d alone won’t pick up Dockerfile changes — you need an explicit rebuild step. The Docker Compose Rebuild guide covers exactly when --build is required versus when Compose can detect the change on its own, and the difference between Dockerfile and Docker Compose responsibilities is worth understanding if you’re new to the build-vs-orchestration split.

    Monitoring, Logging, and Troubleshooting After Deployment

    A Compose deployment isn’t done once docker compose up -d exits successfully — you need visibility into whether it keeps running correctly. docker compose logs -f is the first troubleshooting step for almost any issue, and docker compose ps quickly shows which containers are unhealthy or restarting in a loop.

    By default, Docker’s json-file log driver has no size cap, which means a noisy service can quietly fill a disk over weeks of uptime. Set log rotation explicitly:

    services:
      web:
        image: registry.example.com/myapp:1.4.2
        logging:
          driver: json-file
          options:
            max-size: "10m"
            max-file: "3"

    For a deeper look at reading and filtering logs across multiple services, see the Docker Compose Logs debugging guide. If you’re running a cache or queue alongside your main application, the Redis Docker Compose setup guide is a useful companion for that specific service.

    Backups Before Every Deploy

    Any deployment that includes a database should have an automated backup step that runs before a deploy touches that service — a pg_dump or equivalent, stored off-host. This is separate from application deployment but should be part of the same runbook: a bad migration during a Compose deploy is far less stressful to recover from when a known-good dump exists from minutes earlier.


    Recommended: Ready to put this into practice? DigitalOcean is a tool we use for exactly this, and we have a real, disclosed affiliate relationship with them.

    FAQ

    Is Docker Compose suitable for production, or only for local development?
    Docker Compose is used in production for many single-host or small-scale deployments. It lacks built-in multi-node orchestration, automatic failover, and rolling updates across a cluster, but for workloads that fit on one server, deploying Docker Compose in production is a common and reasonable choice as long as you apply the same operational discipline (restart policies, health checks, secrets management) you would with any other deployment method.

    What’s the difference between docker compose up and docker compose up -d?
    docker compose up runs in the foreground, streaming logs to your terminal and stopping the stack when you press Ctrl+C. docker compose up -d runs the containers detached in the background, which is what you want for any real deployment, since the process needs to survive after your SSH session ends.

    How do I roll back a Compose deployment if something breaks?
    Keep the previous image tag referenced somewhere (in your deploy script’s history, a git tag, or your CI/CD pipeline’s build log), then update the image: line back to that tag and run docker compose up -d again. This is why pinning explicit version tags instead of latest matters — without a known-good tag to revert to, rollback becomes guesswork.

    Do I need Docker Swarm mode to deploy Docker Compose files in production?
    No. Standard Compose runs perfectly well against a single Docker Engine daemon without Swarm being initialized. Swarm mode becomes relevant only if you need multi-node scheduling or built-in rolling updates — Compose files are largely compatible with Swarm’s docker stack deploy if you outgrow single-host limits later, which is one reason Compose remains a reasonable starting point.

    Conclusion

    Deploying Docker Compose to production is straightforward in principle but requires the same operational rigor as any other deployment method: pinned image versions, externalized secrets, health checks, log rotation, and a documented rollback path. For workloads that fit comfortably on a single host, Compose offers a genuinely simpler operational model than a full orchestrator, without sacrificing reliability, as long as the production-specific details covered here — network exposure, restart policies, and backup timing — are handled deliberately rather than left to defaults. Consult the official Docker Compose documentation for the full specification as your setup grows more complex.

  • Docker Run To Compose

    Migrating Docker Run To Compose

    Disclosure: This post contains one or more links to providers we have a real, registered affiliate/referral relationship with. We may earn a commission at no extra cost to you if you sign up through them.

    Managing a growing list of docker run flags is a common signal that it’s time to move docker run to compose. Converting a long imperative command into a declarative docker-compose.yml file makes your setup reproducible, versionable, and far easier to hand off to another engineer or CI pipeline.

    Why Move Docker Run To Compose

    A single docker run command works fine for a quick test, but once you add volumes, environment variables, port mappings, restart policies, and networks, the command becomes unreadable and easy to get wrong. Every time you run it, you risk a typo that silently changes behavior — a missing -v, a reordered -p, or a forgotten --restart flag.

    Moving docker run to compose solves this by capturing the entire configuration in one YAML file that lives in your repository, next to your application code. Instead of remembering a dozen flags, you run:

    docker compose up -d

    and the exact same environment comes up every time, on every machine.

    Key Benefits Of Compose Over Raw Run Commands

  • Version control — the file can be committed to git and reviewed like any other code change.
  • Repeatability — no risk of forgetting a flag between deployments.
  • Multi-container orchestration — a compose file can define an app, database, and cache together, with dependency ordering.
  • Readable diffs — changing a port or environment variable is a one-line diff instead of hunting through shell history.
  • Easier onboarding — a new team member runs one command instead of copy-pasting a long shell one-liner.
  • Understanding The Anatomy Of A Docker Run Command

    Before you convert docker run to compose, it helps to break a typical command into its parts. Consider this example:

    docker run -d \
      --name web_app \
      -p 8080:80 \
      -e NODE_ENV=production \
      -v app_data:/data \
      --restart unless-stopped \
      --network app_net \
      myorg/web-app:1.4

    Each flag maps to a specific compose key:

  • -d (detached mode) has no direct compose key — docker compose up -d handles it at invocation time.
  • --name maps to container_name.
  • -p maps to ports.
  • -e maps to environment.
  • -v maps to volumes.
  • --restart maps to restart.
  • --network maps to networks.
  • the image tag maps directly to image.
  • Once you can identify each of these pairs, the actual translation from docker run to compose becomes mostly mechanical.

    Mapping Common Flags To Compose Keys

    Here’s a more complete reference table you’ll use repeatedly:

    | docker run flag | Compose key |
    |—|—|
    | --name | container_name |
    | -p host:container | ports: |
    | -e KEY=VALUE | environment: |
    | -v host:container | volumes: |
    | --network | networks: |
    | --restart | restart: |
    | --link | depends_on: (with a real network, not legacy links) |
    | --memory / --cpus | deploy.resources.limits (or mem_limit/cpus in Compose v2) |

    Step-By-Step: Converting Docker Run To Compose

    The cleanest way to move docker run to compose is to work through the command flag by flag and build the YAML incrementally rather than trying to write the whole file from memory.

    Step 1: Create The Base Service Definition

    Start with the image and a service name. The service name doesn’t have to match --name, though keeping them aligned reduces confusion:

    services:
      web_app:
        image: myorg/web-app:1.4
        container_name: web_app

    Step 2: Add Ports, Environment, And Volumes

    Translate each remaining flag into its corresponding block. For anything beyond a couple of environment variables, prefer an env_file over inline environment entries — it keeps secrets out of the compose file itself, a practice covered in more depth in this guide to managing Compose environment variables the right way.

    services:
      web_app:
        image: myorg/web-app:1.4
        container_name: web_app
        restart: unless-stopped
        ports:
          - "8080:80"
        environment:
          - NODE_ENV=production
        volumes:
          - app_data:/data
        networks:
          - app_net
    
    volumes:
      app_data:
    
    networks:
      app_net:

    That’s the full docker run to compose translation for the example above — every flag from the original command now has a home in the YAML file, and the stack starts with docker compose up -d.

    Step 3: Validate And Run

    Before trusting a converted file in production, validate the syntax and confirm the config resolves as expected:

    docker compose config
    docker compose up -d
    docker compose ps

    docker compose config prints the fully resolved configuration (with any .env substitutions applied), which is the fastest way to catch a typo before containers actually start.

    Handling Multi-Container Setups When You Move Docker Run To Compose

    Real applications rarely run as a single container. If you’ve been starting an app container and a database with two separate docker run commands (and manually creating a shared network), compose replaces both commands — and the manual networking step — with one file.

    services:
      app:
        image: myorg/web-app:1.4
        ports:
          - "8080:80"
        depends_on:
          - db
        environment:
          - DATABASE_URL=postgres://user:pass@db:5432/appdb
    
      db:
        image: postgres:16
        environment:
          - POSTGRES_USER=user
          - POSTGRES_PASSWORD=pass
          - POSTGRES_DB=appdb
        volumes:
          - db_data:/var/lib/postgresql/data
    
    volumes:
      db_data:

    Services in the same compose file share a default network automatically, so app can reach db by service name — no manual docker network create step required, which is one of the more tedious things you had to do by hand with separate docker run invocations. For a deeper walkthrough of getting a database container production-ready this way, see this Postgres Docker Compose setup guide.

    depends_on controls startup order but not readiness — the database container will be running before it’s necessarily accepting connections. For anything that matters, add a healthcheck rather than assuming the dependency is ready.

    Common Pitfalls When You Convert Docker Run To Compose

    A few mistakes come up repeatedly when teams first move docker run to compose:

  • Forgetting named volumes need declaring — a volumes: entry under a service references a name, but that name must also be listed under the top-level volumes: key or Compose will create an anonymous one instead.
  • Dropping -it silently — interactive/TTY flags don’t have a direct compose equivalent for up; use docker compose run --rm -it <service> <command> for one-off interactive sessions instead.
  • Assuming --link still matters — legacy --link is superseded by Compose’s built-in service-name DNS resolution; don’t try to replicate it explicitly.
  • Copy-pasting host paths verbatim — a bind mount that worked on your laptop (-v /Users/you/app:/app) won’t work on a server; use a relative path or a named volume instead.
  • Not rebuilding after a Dockerfile changedocker compose up -d won’t pick up a changed Dockerfile unless you also run docker compose build or add --build. This is covered in detail in the guide on rebuilding a Compose stack correctly.
  • If you’re unsure whether a given project even needs a Dockerfile versus a plain image reference in your compose file, this comparison of Dockerfile vs Docker Compose is a useful primer before you go further.

    Debugging A Failed Conversion

    When a converted stack doesn’t behave the same as the original docker run command did, the fastest diagnostic loop is:

    docker compose logs -f app
    docker compose exec app env
    docker compose config

    Comparing the output of docker compose exec app env against the environment variables you passed to the original docker run -e flags will usually surface a missing or misspelled key immediately. For a fuller debugging workflow, see this Compose logs debugging guide.

    Managing Secrets After You Move Docker Run To Compose

    One thing a raw docker run -e command can’t do cleanly is separate secrets from configuration. Once you’ve committed to compose, it’s worth also adopting Compose’s secrets handling (or at minimum a gitignored .env file) rather than leaving passwords inline in the YAML you now version-control. This Docker Compose secrets management guide walks through the options in more detail.

    If your stack includes a cache layer alongside the app and database, the same conversion principles apply — flags become keys, and the container joins the same default network automatically, as shown in this Redis Docker Compose guide.

    Where To Run Your Converted Compose Stack

    A compose file is portable by design, but it still needs a host. If you’re moving off a local machine and onto a small production server, a straightforward VPS is usually sufficient for a single compose stack — you don’t need a full orchestration platform unless you’re running many services across multiple machines. Providers like DigitalOcean and Hetzner offer VPS instances that run Docker and Compose out of the box with a standard Linux image.

    If your workload grows beyond what a single compose file can reasonably manage — multiple hosts, automated failover, rolling updates — that’s the point to evaluate a step up, which is covered in this comparison of Kubernetes vs Docker Compose.


    Recommended: Ready to put this into practice? DigitalOcean is a tool we use for exactly this, and we have a real, disclosed affiliate relationship with them.

    FAQ

    Does docker compose up replace docker run entirely?
    For anything with more than a couple of flags, yes. docker run still has a place for quick, disposable one-off containers, but once you need repeatability across environments, moving docker run to compose is worth the small upfront effort.

    Can I convert a docker run command to compose automatically?
    There’s no universally reliable built-in converter, since some flags (like -it) don’t map cleanly to up. Manual, flag-by-flag translation as shown above is more reliable than trusting an automated tool blindly, especially for anything going into production.

    What happens to --rm when I move docker run to compose?
    --rm removes the container after it exits, which is mainly useful for one-off commands. Use docker compose run --rm <service> for the equivalent behavior; a long-running services: entry under up isn’t meant to be ephemeral in the same way.

    Do I need a Dockerfile to use Compose?
    No. If you’re just running a pre-built image with different flags, image: in the compose file is enough. A build: key (pointing at a Dockerfile) is only needed if Compose should build the image itself rather than pull it from a registry.

    Conclusion

    Converting docker run to compose is mostly a mechanical exercise once you understand how each CLI flag maps to a YAML key — ports, environment variables, volumes, networks, and restart policies all have direct equivalents. The real payoff isn’t the conversion itself, but what it enables afterward: a config file you can commit, review, and reuse across environments instead of re-typing (or mis-typing) a long shell command. For the official reference on every available compose key, see the Docker Compose file reference and the broader Docker documentation for anything not covered here.

  • Docker Compose Update

    Docker Compose Update: A Complete Guide to Updating Services Safely

    Disclosure: This post contains one or more links to providers we have a real, registered affiliate/referral relationship with. We may earn a commission at no extra cost to you if you sign up through them.

    Keeping containers current is one of those tasks that seems trivial until it breaks a production stack at the worst possible moment. This guide walks through how to perform a docker compose update correctly, covering image pulls, dependency ordering, rollback strategy, and the common pitfalls that turn a routine update into an incident.

    Whether you’re managing a single VPS running a handful of services or a small fleet of hosts, understanding exactly what happens during a docker compose update — and what doesn’t happen automatically — is essential to running a reliable stack.

    Why a Docker Compose Update Is Different From a Simple Restart

    A common misconception is that restarting a service refreshes its image. It doesn’t. Docker Compose caches image references locally, and unless you explicitly pull new layers, docker compose restart will simply relaunch the exact same container from the exact same image digest. A real docker compose update requires three distinct steps: pulling new image data, recreating containers that reference changed images, and — where applicable — running any migration or initialization logic those new images expect.

    This distinction matters because teams often assume their stack is “up to date” simply because it’s been restarted periodically. In reality, an image tagged latest in your docker-compose.yml can sit unchanged on disk for months unless a pull is triggered on purpose.

    The Three Phases of an Update

    Every meaningful docker compose update goes through the same three phases, regardless of stack size:

    1. Pull — fetch new image layers from the registry.
    2. Recreate — stop old containers and start new ones from the updated images, preserving volumes and networks.
    3. Verify — confirm the new containers are healthy and the application behaves correctly.

    Skipping verification is the single most common cause of “silent” outages — a container starts, looks running, but the application inside is crash-looping or serving errors.

    The Standard Docker Compose Update Workflow

    The most common and safest pattern for a docker compose update looks like this:

    cd /opt/myapp
    docker compose pull
    docker compose up -d
    docker compose ps

    docker compose pull fetches the latest images for every service defined with a tag (not a locally built build: context). docker compose up -d then compares the new image IDs against the running containers and recreates only the ones that changed — services with unchanged images are left untouched, which avoids unnecessary downtime for parts of the stack that didn’t need it.

    Updating a Single Service Instead of the Whole Stack

    You rarely want to update everything at once, especially in a multi-service stack where a database and an application share a compose file. Targeting a single service keeps the blast radius small:

    docker compose pull redis
    docker compose up -d redis

    This pulls and recreates only the redis service, leaving every other container — and its network connections — untouched. This pattern is especially useful when you’re testing a docker compose update against a single component before rolling it out stack-wide.

    Forcing Recreation Without a New Image

    Occasionally you need to force a container recreation even when the image hasn’t changed — for example, after editing environment variables or volume mounts in docker-compose.yml. The --force-recreate flag handles this:

    docker compose up -d --force-recreate app

    This is a distinct scenario from a true docker compose update, but it’s worth knowing the difference: pull changes the image, --force-recreate changes the container regardless of the image.

    Handling Stateful Services During an Update

    Updating stateless application containers is usually low-risk. Updating stateful services — databases, message queues, search indexes — carries real risk, because a new major version may expect a different on-disk data format.

  • Always check the release notes for the target image tag before updating a database container.
  • Take a fresh backup or snapshot of the associated volume before pulling a new major version.
  • Pin database images to a specific minor version (e.g., postgres:16.4 rather than postgres:16) so an unrelated docker compose pull doesn’t silently jump a version.
  • Test the update against a copy of the data on a separate host or a staging compose project first.
  • If you’re running Postgres in this kind of setup, the Postgres Docker Compose setup guide covers volume and backup conventions that make this kind of update far less risky. The same logic applies to Redis and other stateful services — know what’s persisted, and where, before you touch the image tag.

    Version Pinning: The Most Important Habit for Safe Updates

    Nothing causes more unpredictable docker compose update behavior than floating tags. If your docker-compose.yml references nginx:latest or node:20, a docker compose pull can bring in a genuinely different image than the one you tested last week, with no changelog forcing you to notice.

    services:
      web:
        image: nginx:1.27.0
        restart: unless-stopped
        ports:
          - "80:80"
        volumes:
          - ./html:/usr/share/nginx/html:ro

    Pinning to a specific tag turns every docker compose update into a deliberate, reviewable action: you change the tag in the YAML file, commit that change, and only then run the pull/recreate cycle. This also makes rollback trivial — revert the tag in version control and re-run the same two commands.

    Diffing Before You Pull

    If you’re not sure whether an update will pull anything new, docker compose pull --dry-run (available in recent Compose versions) or simply comparing digests avoids surprises:

    docker compose images
    docker compose pull
    docker compose images

    Comparing the two images outputs tells you exactly which services actually received new image IDs, which is far more reliable than assuming a pull did nothing just because the command returned quickly.

    Rebuilding Locally Built Images as Part of an Update

    If a service is built from a local Dockerfile rather than pulled from a registry, docker compose pull does nothing for it — you need to rebuild:

    docker compose build --no-cache app
    docker compose up -d app

    For projects that mix built and pulled services, a combined update command covers both cases:

    docker compose build
    docker compose pull
    docker compose up -d

    If you’re unclear on when to reach for a Dockerfile versus relying purely on Compose-managed images, the Dockerfile vs Docker Compose comparison and the more detailed Docker Compose rebuild guide are useful references for getting this workflow right before automating it.

    Zero-Downtime Considerations

    Standard docker compose up -d briefly stops the old container before starting the new one. For most internal tools and low-traffic services this brief gap is inconsequential, but for anything user-facing it’s worth understanding the limitation: Compose alone does not provide rolling updates or health-check-gated traffic cutover the way an orchestrator like Kubernetes does.

    If your stack genuinely needs zero-downtime deploys, options include:

  • Running two instances of the service behind a reverse proxy and updating them one at a time.
  • Using Docker Swarm mode (docker service update), which does support rolling updates natively.
  • Migrating to Kubernetes if the operational complexity is justified by the traffic and uptime requirements.
  • For most small-to-medium VPS deployments, a brief restart window during a scheduled docker compose update is an acceptable tradeoff against the added complexity of a full orchestrator.

    Health Checks as an Update Safety Net

    Defining a healthcheck in your compose file means Docker itself will report a container as unhealthy if the application fails to start correctly after an update, rather than just showing Up:

    services:
      app:
        image: myapp:2.3.0
        healthcheck:
          test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
          interval: 30s
          timeout: 5s
          retries: 3

    Checking docker compose ps after an update and confirming every service shows healthy rather than just running is a simple habit that catches a large share of failed updates immediately, instead of hours later.

    Rolling Back a Failed Update

    Because Compose doesn’t retain previous container state after up -d recreates a service, rollback means reverting the image reference and re-running the same commands:

    git checkout HEAD~1 -- docker-compose.yml
    docker compose up -d

    This is exactly why pinned, version-controlled tags matter — without them, “the previous version” isn’t a well-defined thing you can roll back to. If your compose file references secrets or environment-specific values, keeping those separated from the image tag itself (see the Docker Compose secrets guide and the Docker Compose env guide) keeps a rollback limited to just the image change, rather than dragging configuration changes along with it.

    Automating Docker Compose Updates Safely

    Fully automated updates (cron-triggered pull + up -d) are tempting but risky for anything stateful or user-facing, since there’s no human checkpoint before a bad image reaches production. A safer middle ground:

  • Automate the pull step and alert on new image availability, but leave up -d as a manual or approval-gated step.
  • Run automated updates against a staging compose project first, and only promote the same tag to production after a manual check.
  • Log every docker compose update — service name, old digest, new digest, timestamp — so a bad update is traceable after the fact. Reviewing docker compose logs (see the Docker Compose logs debugging guide) immediately after an automated update is a reasonable minimum safeguard.
  • If you’re running this kind of workflow on a lightweight VPS rather than a managed platform, providers like DigitalOcean or Hetzner are common, low-overhead choices for hosting a single-host Compose stack that you update on a schedule.


    Recommended: Ready to put this into practice? DigitalOcean is a tool we use for exactly this, and we have a real, disclosed affiliate relationship with them.

    FAQ

    Does docker compose up -d automatically pull new images?
    No. up -d only recreates containers if it detects a locally cached image ID differs from what’s currently running. If you never run docker compose pull (or use a build step), up -d will keep using whatever image is already on disk, even for a floating tag like latest.

    How do I update just one service without touching the rest of the stack?
    Pass the service name to both commands: docker compose pull <service> followed by docker compose up -d <service>. Compose will leave every other running container untouched, including shared networks and volumes.

    Is docker compose restart the same as an update?
    No. restart stops and starts the existing container from its existing image — it never pulls or rebuilds anything. A real docker compose update always requires either pull or build before up -d for the change to take effect.

    What’s the safest way to update a database container?
    Pin the image to an exact version, back up the volume first, review the target version’s release notes for breaking changes, and test the update against a copy of the data before touching production. Never rely on a floating major-version tag for a stateful service.

    Conclusion

    A reliable docker compose update comes down to a small set of disciplined habits: pin your image tags instead of floating on latest, pull deliberately rather than assuming a restart refreshes anything, treat stateful services with extra caution and a backup step, and verify health after every update rather than trusting that “running” means “working.” None of this requires exotic tooling — the built-in pull, build, and up -d commands are sufficient for the vast majority of single-host and small-fleet deployments. For teams eventually needing rolling updates with zero downtime, that’s the point to evaluate Swarm or Kubernetes rather than trying to force that behavior out of Compose alone. For the full command reference and flags used throughout this guide, the official Docker Compose CLI reference is the authoritative source to keep bookmarked.

  • Docker Compose Expose Port

    Docker Compose Expose Port: A Complete Guide to Container Networking

    Disclosure: This post contains one or more links to providers we have a real, registered affiliate/referral relationship with. We may earn a commission at no extra cost to you if you sign up through them.

    Getting container networking right is one of the first hurdles anyone hits when moving from a single docker run command to a full stack. If you’ve ever wondered why a service is reachable from another container but not from your browser, or vice versa, you’re dealing with the difference between exposing and publishing a port. This guide covers everything you need to know about docker compose expose port configuration, from the basic syntax to common troubleshooting scenarios, so you can confidently control exactly which services are reachable and from where.

    Understanding how port exposure works in Compose isn’t just an academic exercise. It directly affects your application’s security posture, how your services communicate internally, and whether your production deployment accidentally leaks an admin panel to the public internet. We’ll walk through the mechanics, the syntax, and the practical patterns that experienced teams use to keep their stacks both functional and secure.

    What Does It Mean to Expose a Port in Docker Compose?

    Before diving into syntax, it’s worth clarifying a distinction that trips up a lot of people: “expose” and “publish” are not the same thing in the Docker world, even though they’re often used interchangeably in casual conversation.

    When you use the expose key in a Compose file, you’re documenting that a container listens on a given port, and you’re making that port reachable to other containers on the same Docker network. You are not making it reachable from the host machine or the outside world. This is purely inter-container communication.

    When you use the ports key (sometimes called “publishing”), you’re mapping a container port to a port on the Docker host itself, which makes the service reachable from outside the container network — including from your local machine or the internet, depending on your firewall and cloud security group rules.

    The Core Syntax Difference

    Here’s a minimal example showing both keys in a single Compose file:

    services:
      api:
        image: my-api:latest
        expose:
          - "3000"
      web:
        image: nginx:latest
        ports:
          - "80:80"
        depends_on:
          - api

    In this setup, the api service is only reachable by other containers on the same network (like web), using the hostname api and port 3000. The web service, on the other hand, is published to the host’s port 80, meaning anyone who can reach the host’s IP address can hit it.

    Why This Distinction Matters

    If you’re building a typical three-tier application — frontend, backend API, database — you generally only want to publish the frontend’s port. The API and the database should remain internal, reachable only by the services that need them. This is a basic but effective security boundary: an attacker scanning your host’s open ports won’t even see that your database exists, because Compose never mapped it to the host.

    This is also why understanding docker compose expose port behavior matters for cost and complexity control. Every port you publish to the host is a port you now need to secure, monitor, and potentially expose through your cloud firewall or load balancer. Keeping internal services on expose rather than ports reduces your attack surface without any extra tooling.

    How the ports Key Actually Works

    The ports directive is what most people reach for first, and for external-facing services it’s the correct choice. It supports several formats, and picking the right one matters.

    Short Syntax

    The short syntax is a string in the form HOST:CONTAINER, optionally with a host IP and protocol:

    services:
      db:
        image: postgres:16
        ports:
          - "5432:5432"
          - "127.0.0.1:5433:5432"
          - "8080:80/tcp"

    The second line here is important for security-conscious setups: binding to 127.0.0.1 instead of leaving it unbound (which defaults to 0.0.0.0) means the port is only reachable from the host machine itself, not from other machines on the network. This is a pattern worth adopting for any database port you publish for local debugging — you don’t want a Postgres or Redis instance reachable from the wider internet just because you forgot to scope the binding. If you’re running Postgres in Compose, our Postgres Docker Compose setup guide covers this binding pattern in more depth.

    Long Syntax

    Compose also supports a long-form, object-based syntax, which is more explicit and easier to read in complex files:

    services:
      db:
        image: postgres:16
        ports:
          - target: 5432
            published: 5432
            protocol: tcp
            mode: host

    The long syntax is particularly useful when you’re generating Compose files programmatically or want self-documenting configuration that doesn’t require memorizing the short-syntax ordering.

    Random Host Port Assignment

    If you only specify the container port, Docker will assign a random available port on the host:

    services:
      worker:
        image: my-worker:latest
        ports:
          - "3000"

    This is less common in production but genuinely useful in local development when you’re running multiple copies of a service and don’t want manual port bookkeeping. You can find the assigned port with docker compose port worker 3000.

    Common docker compose expose port Scenarios and Patterns

    Let’s walk through a few realistic scenarios where the choice between expose and ports actually matters in practice.

    Scenario: Internal Microservices Behind a Reverse Proxy

    A very common architecture is to have a reverse proxy (like Nginx or Traefik) as the only publicly reachable service, with everything else — APIs, background workers, caches — kept internal.

    services:
      proxy:
        image: nginx:latest
        ports:
          - "443:443"
          - "80:80"
        depends_on:
          - api
          - auth
    
      api:
        build: ./api
        expose:
          - "3000"
    
      auth:
        build: ./auth
        expose:
          - "4000"
    
      redis:
        image: redis:7
        expose:
          - "6379"

    Here, only proxy is published. The api, auth, and redis services are reachable by name (api:3000, auth:4000, redis:6379) from within the Docker network, but completely invisible from outside the host. This is the pattern you should default to for anything that doesn’t need to be directly internet-facing. If you’re setting up a cache alongside this kind of stack, our Redis Docker Compose guide walks through a similar internal-only configuration.

    Scenario: Local Development With Direct Access

    During local development, you often want direct access to services for debugging with tools like psql, redis-cli, or Postman, without going through a proxy layer. In that case, publishing ports temporarily is reasonable:

    services:
      api:
        build: ./api
        ports:
          - "3000:3000"
        environment:
          - NODE_ENV=development

    A good practice is to keep this kind of direct-access configuration in a separate docker-compose.override.yml file that only applies locally, while your base docker-compose.yml uses expose for the same service. Compose automatically merges an override file with the base file, so your production config never accidentally inherits a development-only published port.

    Scenario: Debugging “Connection Refused” Errors

    If you’ve ever hit connection refused when trying to reach a service from your host machine, the most common cause is that the service was only exposed, not ports-published. The container is running and listening fine — from inside the Docker network — but the host simply has no route to it.

    A quick way to confirm this:

  • Run docker compose ps and check whether the service shows a host port mapping in the PORTS column.
  • If it’s blank or shows only the container port with no -> host mapping, the port was exposed but not published.
  • Add a ports entry if you genuinely need host access, or use docker compose exec to jump into another container on the same network if you don’t.
  • Networking Fundamentals Behind Compose Port Behavior

    To really understand why expose and ports behave the way they do, it helps to know what’s happening under the hood with Docker’s networking model.

    Default Bridge Networks

    When you run docker compose up, Compose automatically creates a dedicated bridge network for your project (named after the project directory by default). Every service in the Compose file is attached to this network unless you specify otherwise, and each service gets a DNS entry matching its service name. This is why api can reach redis simply by resolving the hostname redis — no IP addresses or manual /etc/hosts entries required.

    The expose key doesn’t create any new networking behavior beyond what the bridge network already provides — containers on the same network can already reach each other’s listening ports regardless of whether you declare expose. Its main value is documentation and, in Swarm mode, port advertisement between services. Many teams still declare it explicitly because it makes the Compose file self-documenting: anyone reading it immediately knows which ports the service listens on internally.

    The docker-proxy Process and iptables

    When you publish a port with ports, Docker on Linux sets up iptables NAT rules (or uses docker-proxy as a fallback) to forward traffic from the host’s network interface to the container’s internal IP address on the Docker bridge network. This is genuinely a form of port forwarding, not just a label — it changes real packet routing on your host. That’s why binding to 127.0.0.1 versus 0.0.0.0 has real security implications: it controls which network interface the forwarding rule listens on.

    For a deeper look at how this plays out with custom bridge networks and multiple services, see the official Docker networking documentation.

    Best Practices for Managing Ports in Compose

    A few practical guidelines will keep your Compose-based stacks secure and maintainable as they grow.

    Default to expose, Opt Into ports

    Treat ports as something you add deliberately for each service that genuinely needs external reachability, rather than something you add by default. This “deny by default” posture means a misconfiguration is far less likely to accidentally expose an internal service.

    Use Environment Variables for Port Numbers

    Hardcoding port numbers throughout a large Compose file makes it harder to change later, especially across environments. Use variable substitution instead:

    services:
      api:
        build: ./api
        ports:
          - "${API_PORT:-3000}:3000"

    This lets you override API_PORT per environment via a .env file without editing the Compose file itself. If you’re managing several environment-specific values like this, our Docker Compose env variables guide and the more detailed Docker Compose environment variables reference both go deeper into patterns for keeping configuration clean across dev, staging, and production.

    Avoid Publishing Database Ports in Production

    It’s tempting to publish a database port “just for now” while debugging, then forget to remove it before deploying. Treat any published database port in a production Compose file as a red flag during code review. If you need occasional external access to a production database, use an SSH tunnel or a bastion host instead of a permanently open port.

    Audit Your Compose Files Periodically

    As stacks grow, it’s easy to lose track of which services are publishing ports and why. Running docker compose config will print the fully resolved configuration, including all port mappings, which is a useful way to audit a file that uses multiple .env files, override files, or variable substitution.

    Rebuilding and Restarting After Port Changes

    Changing a ports or expose value in your Compose file requires recreating the container — a simple restart won’t apply new networking configuration, since port bindings are set up when the container is created, not when it starts.

    docker compose up -d --force-recreate api

    If you’ve also changed the underlying image (for example, after modifying a Dockerfile), you’ll want a rebuild as well. Our Docker Compose rebuild guide covers the difference between --force-recreate and a full --build, which matters when you’re troubleshooting why a port change doesn’t seem to take effect. And if you’re not sure whether your networking or build issue is actually a Compose problem versus a base Dockerfile problem, Dockerfile vs Docker Compose is a useful comparison to work through.

    When something still isn’t behaving as expected after a port change, checking the container logs is usually the fastest way to confirm the service actually started listening on the port you think it did — our Docker Compose logs guide covers the flags and filtering options that make this faster.

    Hosting Considerations for Published Ports

    If you’re deploying a Compose stack to a VPS rather than a managed platform, the ports you publish interact directly with your provider’s firewall rules, not just Docker’s own iptables configuration. A published port in your Compose file does nothing if your cloud provider’s security group or firewall blocks that port at the network edge — and conversely, a port left open at the firewall level but never published in Compose is simply unreachable regardless. When choosing where to run a stack like this, providers such as DigitalOcean offer straightforward firewall configuration alongside your droplet, which pairs well with a deliberate, minimal docker compose expose port strategy — publish only what needs to be public, and lock the firewall down to match.


    Recommended: Ready to put this into practice? DigitalOcean is a tool we use for exactly this, and we have a real, disclosed affiliate relationship with them.

    FAQ

    What’s the difference between expose and ports in Docker Compose?
    expose makes a port reachable only to other containers on the same Docker network — it has no effect on host or external access. ports maps a container port to a port on the host machine, making it reachable from outside the container network, subject to your firewall rules.

    Do I need to use expose at all if my services are already on the same network?
    Not strictly — containers on the same Compose-managed network can reach each other’s listening ports regardless of whether expose is declared. Many teams still include it for documentation purposes, since it makes clear at a glance which ports a service actually listens on.

    Why is my published port not reachable from another machine?
    Check three layers: first, confirm the port shows a host mapping in docker compose ps; second, confirm you didn’t bind it to 127.0.0.1 (which restricts it to the host itself); third, check your host’s firewall or cloud security group, since Docker’s port publishing doesn’t override external firewall rules.

    Can I expose the same container port to multiple host ports?
    Yes — list multiple entries under ports for the same container port, each with a different host port, for example "8080:80" and "8443:80" under the same service. Docker will forward traffic from either host port to the same container port.

    Conclusion

    The distinction between exposing and publishing a port is small in terms of syntax but significant in terms of security and architecture. Getting docker compose expose port configuration right means thinking deliberately about which services genuinely need to be reachable from outside your Docker network, and keeping everything else internal by default. Combine that discipline with sensible host-binding choices, environment-variable-driven port numbers, and periodic audits of your Compose files, and you’ll avoid the most common networking mistakes teams make when scaling from a single container to a full multi-service stack. For the full range of configuration options available, the official Docker Compose specification remains the authoritative reference to check against as your stack evolves.

  • Docker Run To Docker Compose

    Migrating from Docker Run to Docker Compose

    Disclosure: This post contains one or more links to providers we have a real, registered affiliate/referral relationship with. We may earn a commission at no extra cost to you if you sign up through them.

    If you have been managing containers with a growing pile of docker run commands, you already know the pain: long flags, forgotten port mappings, and no easy way to restart everything the same way twice. Moving from docker run to docker compose gives you a single, version-controlled file that describes your entire container setup, and it is one of the most useful upgrades a small infrastructure can make. This guide walks through why the migration matters and exactly how to do it, step by step.

    Why Move From Docker Run to Docker Compose

    A single docker run command is fine for a quick test. The trouble starts when you have more than one container, or when a container needs specific volumes, networks, environment variables, and restart policies. Every time you need to recreate that container, you have to remember (or dig up in your shell history) the exact flags you used.

    Docker Compose solves this by moving the entire configuration into a docker-compose.yml (or compose.yaml) file. Instead of typing out a long command, you run docker compose up -d and Compose reads the file, builds or pulls the images, creates the network, and starts every service in the correct order.

    Key Benefits of Docker Compose Over Raw Docker Run

  • Reproducibility — the same file produces the same environment every time, on any machine.
  • Version control — you can commit the compose file to git and track every change to your infrastructure.
  • Multi-container orchestration — services can reference each other by name instead of by IP address.
  • Simpler commandsdocker compose up, docker compose down, and docker compose logs replace multiple long docker run invocations.
  • Built-in networking — Compose automatically creates a dedicated network so containers can talk to each other without manual --network flags.
  • Mapping Docker Run Flags to Docker Compose Syntax

    The core work of migrating from docker run to docker compose is translating command-line flags into YAML keys. Most flags have a direct, one-to-one equivalent, which makes the docker run to docker compose conversion mostly mechanical once you know the mapping.

    Common Flag-to-YAML Translations

    | docker run flag | Compose key |
    |—|—|
    | -p 8080:80 | ports: ["8080:80"] |
    | -v /data:/app/data | volumes: ["/data:/app/data"] |
    | -e KEY=value | environment: [KEY=value] |
    | --name mycontainer | container_name: mycontainer |
    | --restart unless-stopped | restart: unless-stopped |
    | --network mynet | networks: [mynet] |
    | -d (detached) | implied by docker compose up -d |

    Worked Example: From Command to File

    Suppose you have been starting a simple web app with a command like this:

    docker run -d 
      --name myapp 
      -p 8080:80 
      -e NODE_ENV=production 
      -v myapp_data:/app/data 
      --restart unless-stopped 
      myorg/myapp:latest

    Translating this line by line into Compose syntax gives you a clean, readable file:

    services:
      myapp:
        image: myorg/myapp:latest
        container_name: myapp
        ports:
          - "8080:80"
        environment:
          - NODE_ENV=production
        volumes:
          - myapp_data:/app/data
        restart: unless-stopped
    
    volumes:
      myapp_data:

    Once this file exists, the container is started with docker compose up -d, and every future engineer on the project can read exactly what configuration is running without reverse-engineering a shell script.

    Step-by-Step Docker Run to Docker Compose Migration

    Moving an existing set of docker run commands to Compose is a repeatable process. Following these steps for each container keeps the migration low-risk and easy to verify.

    Step 1: Inventory Your Existing Containers

    Before writing any YAML, list every container you currently run and the exact flags used to start it. You can pull this information from docker inspect if you no longer have the original commands:

    docker inspect myapp --format '{{json .Config}}' | python3 -m json.tool

    This shows the image, environment variables, exposed ports, and mounted volumes for a running container, which is useful when the original docker run command has been lost.

    Step 2: Write One Service Per Container

    Create a docker-compose.yml file and add one entry under services: for each container in your inventory, following the flag mapping table above. If two containers need to talk to each other (for example, an app and a database), Compose’s default network lets them reach each other using the service name as a hostname — no more hardcoded IP addresses or --link flags.

    Step 3: Test, Then Cut Over

    Bring the new stack up alongside the old containers first, using different host ports if needed, and confirm the application behaves the same way. Once verified, stop the old containers and start the Compose-managed versions on the real ports:

    docker compose up -d
    docker compose ps

    If something goes wrong, docker compose down cleanly removes the containers and network Compose created, which is a much safer rollback path than manually tracking down every container you started by hand.

    Handling Multi-Container Applications

    The biggest practical reason teams move from docker run to docker compose is multi-container coordination. A typical web application stack might include an app server, a database, and a cache — three separate docker run commands that all need to agree on network names, startup order, and shared volumes.

    Defining Service Dependencies

    Compose’s depends_on key lets you express startup order directly in the file:

    services:
      web:
        image: myorg/myapp:latest
        depends_on:
          - db
        ports:
          - "8080:80"
      db:
        image: postgres:16
        environment:
          - POSTGRES_PASSWORD=changeme
        volumes:
          - db_data:/var/lib/postgresql/data
    
    volumes:
      db_data:

    depends_on controls the order containers are started in, not whether the dependent service is actually ready to accept connections — for that, add a healthcheck or handle retries in your application code. If you are running Postgres specifically, our Postgres Docker Compose setup guide covers healthchecks and data persistence in more detail.

    Managing Secrets and Environment Files

    Hardcoding passwords directly in docker run -e commands is a common bad habit that Compose makes easy to fix. Instead of inline environment variables, reference an .env file or Compose’s secrets: block. For a deeper walkthrough of both approaches, see our guides on managing Compose environment variables and Docker Compose secrets management.

    Common Pitfalls When Migrating to Docker Compose

    Most migration problems come from small mismatches between the old command and the new file, not from Compose itself.

  • Forgetting named volumes — a bind mount in docker run -v /host/path:/container/path is straightforward, but a named volume (-v myvolume:/path) needs a matching volumes: top-level declaration in the compose file, or Compose will create an anonymous volume instead.
  • Port order confusion — Compose uses the same host:container order as docker run -p, but it is easy to transpose the two when hand-editing YAML, so double-check each mapping.
  • Missing restart policies — a docker run command with no --restart flag and a Compose service with no restart: key behave the same way (no automatic restart), but many teams assume Compose restarts by default. It does not; set restart: unless-stopped explicitly if that is the behavior you want.
  • Network name collisions — Compose automatically names its network after the project directory. If you rename the folder, the network name changes too, which can break external references. Set name: under a top-level networks: block if you need a stable name.
  • Once you’re comfortable with these basics, tools like Docker Compose Rebuild and Docker Compose Logs become part of the regular workflow for keeping a migrated stack healthy.

    Docker Run to Docker Compose: When Compose Isn’t Enough

    Docker Compose is built for running containers on a single host. If your workload grows to the point where you need multiple hosts, automated failover, or rolling updates across a cluster, Compose is no longer the right tool — that’s the point where teams typically look at Kubernetes instead. Our comparison of Kubernetes vs Docker Compose covers where that line usually falls. For most small-to-medium projects — a handful of services on a single VPS — Compose remains the simpler, easier-to-maintain choice, and there is no need to reach for a full orchestrator just because it exists.

    If you are hosting this stack on a virtual server, providers like DigitalOcean offer straightforward VPS plans that work well for a Compose-managed application, without the operational overhead of a full cluster.


    Recommended: Ready to put this into practice? DigitalOcean is a tool we use for exactly this, and we have a real, disclosed affiliate relationship with them.

    FAQ

    Does migrating from docker run to docker compose change how my containers behave?
    No. Compose is a orchestration layer on top of the same Docker Engine APIs that docker run uses. As long as the flags are translated correctly into the compose file, the resulting containers run identically — same image, same network behavior, same resource usage.

    Can I run docker run and docker compose containers on the same host at the same time?
    Yes. They share the same Docker daemon, so nothing stops you from having some containers started manually and others managed by Compose. This is actually a useful way to migrate gradually, testing one service in Compose while leaving the rest as-is.

    What happens to my data volumes during the migration?
    If you reference the same named volume in your compose file that your original docker run -v myvolume:/path command used, Compose will attach to the existing volume rather than creating a new one — no data is lost. Bind mounts to host directories work the same way, since the data lives on the host filesystem independent of the container.

    Do I need to remove the old containers before switching to Compose?
    It’s a good idea to stop and remove the old docker run containers once you’ve verified the Compose version works, mainly to avoid port conflicts. You can do this with docker stop and docker rm, or simply docker compose down if you first ran the old container under the same name Compose expects to manage.

    Conclusion

    Moving from docker run to docker compose is one of the highest-value, lowest-risk changes you can make to a small container deployment. It replaces fragile, easy-to-forget shell commands with a single declarative file that documents your infrastructure, works the same way across machines, and scales naturally as you add more services. Start by inventorying your current docker run commands, translate each one using the flag mapping above, and test the new file alongside your existing setup before cutting over. For further reading on the underlying tool, the official Docker Compose documentation and Docker CLI reference are the most reliable sources for flags and behavior that change between releases.

  • Docker Compose Up Command

    Docker Compose Up Command

    Disclosure: This post contains one or more links to providers we have a real, registered affiliate/referral relationship with. We may earn a commission at no extra cost to you if you sign up through them.

    The docker compose up command is the single most-used entry point for running multi-container applications defined in a compose.yaml file. Whether you are starting a local development environment or bringing a production stack online, understanding exactly what the docker compose up command does — and the flags that change its behavior — will save you from a lot of confusing debugging sessions later.

    This guide walks through how the docker compose up command works, the flags you’ll use most often, common failure modes, and how it fits into a realistic DevOps workflow alongside related commands like build, down, and logs.

    What the Docker Compose Up Command Actually Does

    When you run docker compose up inside a directory containing a compose.yaml (or docker-compose.yml) file, Compose performs a sequence of steps:

    1. Parses the compose file and resolves any .env variables referenced in it.
    2. Creates a dedicated network for the project (unless one is already defined).
    3. Creates named volumes that don’t already exist.
    4. Builds images for any service with a build: directive, if they aren’t already built or --build is passed.
    5. Creates containers for each service, respecting depends_on ordering.
    6. Starts the containers and attaches your terminal to their combined log output.

    This is different from docker compose start, which only starts containers that already exist — it does not create anything new. The docker compose up command is the one you reach for the first time you bring a stack online, and generally the one you keep using afterward too, since it’s idempotent: running it again with no changes simply leaves already-running containers alone.

    # compose.yaml
    services:
      web:
        image: nginx:latest
        ports:
          - "8080:80"
      api:
        build: ./api
        environment:
          - NODE_ENV=production
        depends_on:
          - db
      db:
        image: postgres:16
        volumes:
          - db_data:/var/lib/postgresql/data
    
    volumes:
      db_data:

    Running docker compose up against this file will build the api image, pull nginx and postgres if they aren’t cached locally, create the db_data volume, and start all three containers in dependency order.

    Common Flags for the Docker Compose Up Command

    Most day-to-day usage of the docker compose up command revolves around a small set of flags. Learning these well covers the vast majority of real workflows.

    Detached Mode with -d

    By default, the docker compose up command runs in the foreground and streams logs from every container to your terminal. Press Ctrl+C and Compose sends a stop signal to all containers. For anything long-running — a staging server, a background worker, a database — you almost always want detached mode instead:

    docker compose up -d

    This starts the containers and returns control of the terminal immediately. You can then check status with docker compose ps or tail logs on demand — see our complete guide to docker compose logs for filtering and following log output after the fact.

    Rebuilding Images with --build

    If you’ve changed a Dockerfile or the contents of a build context, Compose won’t automatically know to rebuild unless you tell it to:

    docker compose up -d --build

    This forces a rebuild of any service with a build: key before starting containers. It’s a common source of confusion — “I changed my code but the container still runs the old version” almost always means a missing --build flag. If you want more control over incremental vs. full rebuilds, our Docker Compose Rebuild guide covers the tradeoffs in depth.

    Removing Orphan Containers

    If you’ve renamed or removed a service from your compose file, old containers from a previous run can be left behind:

    docker compose up -d --remove-orphans

    This is good hygiene to include in CI/deploy scripts so stale containers never silently accumulate.

    Docker Compose Up vs Related Commands

    It helps to be explicit about what the docker compose up command does not do, since a few adjacent commands are frequently confused with it.

    Up vs Start

    docker compose start only starts containers that Compose already created. It does not read new configuration, build images, or create networks/volumes. docker compose up does all of that. If you’ve never run up for a given compose file, start will simply fail — there’s nothing to start yet.

    Up vs Run

    docker compose run is for one-off commands against a service’s image — for example, running a database migration or opening a shell — without affecting the rest of your stack’s running state. up is for bringing the full defined stack online. They solve different problems and shouldn’t be used interchangeably.

    Up vs Down

    docker compose down is effectively the inverse operation: it stops containers and removes them, along with the default network. It does not remove volumes unless you pass -v. If you’re troubleshooting a stack lifecycle issue, our Docker Compose Down guide is the natural companion reading to this one.

    Environment Variables and the Docker Compose Up Command

    A frequent source of “it works on my machine” bugs is environment variable resolution. The docker compose up command reads variables from three main places: a .env file in the project directory, variables exported in your shell, and environment:/env_file: entries inside the compose file itself.

  • Shell-exported variables generally take precedence over .env file values.
  • .env file values are used to interpolate ${VARIABLE} syntax inside compose.yaml itself, not just inside containers.
  • env_file: entries under a service inject variables into that specific container’s runtime environment, separate from interpolation.
  • Getting these three layers mixed up is one of the most common Compose mistakes. If you’re regularly hitting “variable not set” warnings when running the docker compose up command, our dedicated Docker Compose Env guide walks through the precedence rules and debugging techniques in more detail, and the Docker Compose Environment Variables guide covers the environment: key specifically.

    Overriding Variables at Invocation Time

    You can also pass variables inline for a single run of the docker compose up command:

    API_KEY=test123 docker compose up -d

    This is useful for CI pipelines or local overrides without editing a .env file, though for anything sensitive you should prefer Compose’s secrets support rather than inline environment variables — see the Docker Compose Secrets guide for that pattern.

    Troubleshooting a Failed Docker Compose Up

    When the docker compose up command fails, the error usually falls into one of a few categories:

  • Port conflicts — another process (or another Compose project) already binds the host port you specified.
  • Build failures — a Dockerfile step fails, often due to a missing file in the build context or a network issue during a package install.
  • Dependency ordering — a service starts before a dependency (like a database) is actually ready to accept connections, even though depends_on only waits for the container to start, not for the application inside it to be ready.
  • Volume permission issues — a mounted volume has ownership that doesn’t match the user the container process runs as.
  • Diagnosing with Logs

    The first troubleshooting step after any failed docker compose up command should almost always be checking logs for the specific service that failed:

    docker compose logs api --tail=100

    Fixing Startup Ordering with Healthchecks

    depends_on alone only guarantees container start order, not application readiness. To make a service genuinely wait for a database to be ready, pair depends_on with a condition: service_healthy and a healthcheck: block on the dependency:

    services:
      db:
        image: postgres:16
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U postgres"]
          interval: 5s
          timeout: 5s
          retries: 5
      api:
        build: ./api
        depends_on:
          db:
            condition: service_healthy

    With this in place, the docker compose up command will hold off starting api until Postgres actually reports healthy, not just running.

    Where to Run Docker Compose Up in Production

    For local development, running the docker compose up command directly on your workstation is fine. For anything customer-facing, you’ll want a dedicated server rather than your laptop — a small unmanaged VPS is usually the simplest option for a single-host Compose deployment. Providers like DigitalOcean offer straightforward VPS instances that work well for this. If you’re new to running your own server rather than a managed platform, our Unmanaged VPS Hosting guide explains what you’re responsible for versus what the provider handles.

    Once your compose file is running on a real host, keep in mind that docker compose up -d alone doesn’t restart automatically after a server reboot unless your services have a restart: policy set (e.g. restart: unless-stopped), and it doesn’t handle zero-downtime deploys — it will briefly stop and recreate containers whose configuration changed.


    Recommended: Ready to put this into practice? DigitalOcean is a tool we use for exactly this, and we have a real, disclosed affiliate relationship with them.

    FAQ

    Does docker compose up rebuild images automatically?
    No. It only builds an image if that image doesn’t exist yet for a service with a build: directive. If the image already exists — even if the underlying Dockerfile or source code has changed — Compose will reuse it unless you explicitly pass --build.

    What’s the difference between docker compose up and docker compose up -d?
    Without -d, Compose runs in the foreground and streams all container logs to your terminal until you stop it with Ctrl+C. With -d (detached), containers start in the background and control returns to your shell immediately.

    Why does docker compose up say a port is already allocated?
    Another container, another Compose project, or a non-Docker process on the host is already using that port. Stop the conflicting process, or change the host-side port mapping in your compose file (e.g. "8081:80" instead of "8080:80").

    Can I run docker compose up for just one service?
    Yes — pass the service name as an argument, e.g. docker compose up -d api. Compose will still start any services listed in depends_on for that service, but it won’t start unrelated services defined elsewhere in the file.

    Conclusion

    The docker compose up command is deceptively simple on the surface but has real nuance once you factor in build caching, environment variable precedence, and dependency readiness. Getting comfortable with its core flags — -d, --build, and --remove-orphans — along with understanding how it differs from start, run, and down, covers most of what you’ll need for day-to-day Compose work. For deeper dives into specific pieces of this workflow, see the official Docker Compose CLI reference and the broader Docker documentation.

  • Latest Docker Compose Version

    Latest Docker Compose Version

    Disclosure: This post contains one or more links to providers we have a real, registered affiliate/referral relationship with. We may earn a commission at no extra cost to you if you sign up through them.

    Keeping track of the latest Docker Compose version matters more than it might seem at first glance. Compose has moved from a standalone Python tool into a native Docker CLI plugin written in Go, and the differences between major versions affect everything from your docker-compose.yml syntax to how commands behave in CI pipelines. This guide explains how Compose versioning actually works today, how to check and upgrade what you have, and what to watch for when moving between versions.

    Why the Latest Docker Compose Version Matters

    Docker Compose has gone through a substantial architectural shift. The original docker-compose (V1) was a separate Python-based binary you installed independently of the Docker Engine. Docker Compose V2 rewrote the tool in Go and integrated it directly into the Docker CLI as a plugin, invoked as docker compose (no hyphen) instead of docker-compose. If you’re still running V1, you’re using an unmaintained, deprecated tool — Docker has officially retired further development on it.

    Knowing the latest Docker Compose version isn’t just trivia. New releases regularly:

  • Fix bugs in dependency ordering, health-check handling, and volume mounting
  • Add support for newer Compose Specification fields
  • Improve compatibility with Buildx and multi-platform builds
  • Patch security issues in the CLI plugin itself
  • Improve output formatting, --wait behavior, and watch-mode reliability
  • Because Compose V2 ships as part of the Docker CLI plugin ecosystem, staying current is usually as simple as keeping Docker Desktop or the docker-compose-plugin package up to date — but it’s still worth knowing how to check, pin, and verify your version explicitly, especially in automated environments like CI runners or provisioning scripts.

    A Quick Note on Versioning Confusion

    One of the most common points of confusion is that “Docker Compose version” can refer to two different things: the tool version (e.g., v2.29.0) and the Compose file format version (the old version: "3.8" key at the top of a docker-compose.yml). These are unrelated numbering schemes. The Compose Specification (the modern, unversioned schema that replaced the old numbered file format) no longer requires a version key at all — Compose V2 ignores it if present and will not error if it’s missing.

    How to Check Your Current Docker Compose Version

    Before deciding whether you need to upgrade, check what you’re running.

    docker compose version

    This is the V2 command and should return something like:

    Docker Compose version v2.29.2

    If instead you get a “command not found” error, try the legacy standalone binary:

    docker-compose --version

    If that succeeds, you’re running V1, and it’s worth planning a migration — V1 no longer receives updates. Also check your Docker Engine version, since Compose V2 relies on features exposed by a reasonably current Engine:

    docker version

    Where the Latest Docker Compose Version Is Published

    Docker publishes release notes and version tags for Compose on its official GitHub repository and documentation. Rather than guessing or relying on outdated blog posts, check the source directly. The Docker Compose documentation is the authoritative reference for current behavior, supported flags, and file-format guidance, and it’s updated alongside each release.

    If you manage Docker across a fleet of servers — for example, provisioning fresh VPS instances for staging environments — it helps to script the version check as part of your setup so you’re not manually SSH-ing into each box to confirm. Teams running self-hosted automation stacks, like those documented in our n8n self-hosted installation guide, often bake this check into their provisioning scripts to avoid drift between environments.

    Upgrading to the Latest Docker Compose Version

    How you upgrade depends on your platform and how Docker was originally installed.

    Docker Desktop Users (macOS, Windows)

    If you’re running Docker Desktop, Compose V2 ships bundled with it. Upgrading Docker Desktop itself brings the latest Docker Compose version along automatically. Check for updates through the Docker Desktop UI, or download the newest installer from Docker’s official site.

    Linux Server Installations

    On Linux servers — which is where most production Docker Compose workloads actually run — Compose V2 is installed as a CLI plugin, typically via your distribution’s package manager alongside the Docker Engine packages. A typical upgrade on a Debian/Ubuntu-based VPS looks like:

    sudo apt-get update
    sudo apt-get install --only-upgrade docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

    After running this, re-check the version with docker compose version to confirm the upgrade landed correctly. If you manage several unmanaged VPS instances directly (rather than through a managed panel), this manual upgrade path is one of the recurring maintenance tasks worth documenting in your own runbooks — something we cover in more depth in our unmanaged VPS hosting guide.

    Manual Binary Installation

    In some minimal or containerized build environments, you may install the Compose plugin binary directly rather than through a package manager:

    DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
    mkdir -p $DOCKER_CONFIG/cli-plugins
    curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 
      -o $DOCKER_CONFIG/cli-plugins/docker-compose
    chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose
    docker compose version

    This pulls whatever release GitHub currently marks as latest, so it’s a reasonable approach when you always want the newest build without hardcoding a version number — though for reproducible CI builds, pinning to a specific tag is usually safer than always tracking latest.

    Docker Compose V1 vs V2: What Actually Changed

    Understanding the practical differences helps explain why staying on the latest Docker Compose version matters beyond just bug fixes.

    Command Syntax

    V1 used a standalone binary invoked with a hyphen: docker-compose up. V2 integrates into the Docker CLI itself: docker compose up. Functionally they’re similar for basic commands, but V2 added several new subcommands and flags not present in V1, including improvements around docker compose watch for live development reloads and better --wait support for health-check-gated startup.

    Underlying Language and Performance

    V1 was written in Python, which meant it depended on a Python runtime and associated packaging quirks. V2 is written in Go, matching the rest of the Docker CLI toolchain, which generally means faster startup and fewer dependency conflicts on the host.

    File Format Handling

    V1 was stricter about requiring the version: key and validating it against a numbered schema (2.x, 3.x). V2 is built around the unversioned Compose Specification, which is more permissive and continues to evolve independently of Docker Engine releases. If you’re comparing these two tools directly for a project decision, our Docker Compose vs Dockerfile article and Kubernetes vs Docker Compose comparison cover related tooling decisions worth reading alongside this one.

    Pinning and Verifying Compose Versions in CI/CD

    Automated pipelines are where version drift causes the most subtle bugs — a workflow that passes locally on the latest Docker Compose version might behave differently on an older CI runner image. A minimal example of pinning a specific Compose plugin version inside a CI job:

    name: build-and-test
    on: [push]
    jobs:
      compose-check:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - name: Verify Compose version
            run: |
              docker compose version
              docker compose config --quiet
          - name: Build stack
            run: docker compose up --build --wait

    The docker compose config --quiet step is a useful habit: it validates your docker-compose.yml against whatever schema the installed Compose version understands, catching syntax issues before you actually try to bring services up. This is particularly useful if your stack involves multiple interdependent services — for example, a database plus an automation engine, as described in our n8n automation self-hosting guide — where a subtle version mismatch in health-check syntax can silently break startup ordering.

    Common Issues When Upgrading

    docker-compose: command not found After Upgrade

    If a package manager upgrade removed the standalone V1 binary but your scripts still call docker-compose with a hyphen, you’ll need to either update those scripts to use docker compose, or install a compatibility shim. Long-term, updating the scripts is the better fix, since V1 won’t receive further updates.

    Environment Variable Interpolation Differences

    Compose V2 handles some edge cases in .env file interpolation slightly differently than V1 did, particularly around default value syntax like ${VAR:-default}. If you rely heavily on environment-driven configuration, review our Docker Compose environment variables guide after upgrading to confirm your interpolation patterns still resolve as expected.

    Health Checks and depends_on Ordering

    Newer Compose versions have refined how depends_on with condition: service_healthy interacts with startup ordering. If your stack — say, a Postgres-backed application — relies on strict startup sequencing, it’s worth re-testing this after any Compose upgrade rather than assuming behavior is unchanged; see our Postgres Docker Compose setup guide for a working health-check pattern.

    Choosing Where to Run Your Docker Compose Stack

    Once you’ve confirmed you’re on the latest Docker Compose version, the next practical question is often where to actually run it. A small VPS is usually sufficient for development or low-traffic production stacks. Providers like DigitalOcean and Hetzner offer straightforward Linux VPS instances where installing Docker Engine and the Compose plugin takes only a few commands, giving you full control over which Compose version you run rather than being locked into whatever a managed platform provides.


    Recommended: Ready to put this into practice? DigitalOcean is a tool we use for exactly this, and we have a real, disclosed affiliate relationship with them.

    FAQ

    Is Docker Compose V1 still supported?
    No. Docker has ended active development on the standalone Python-based Compose V1 tool. All new features, bug fixes, and security patches go into Compose V2, which is the version bundled as a Docker CLI plugin (docker compose, no hyphen).

    How do I know if I’m running the latest Docker Compose version?
    Run docker compose version and compare the output against the release tags listed on Docker’s official documentation or the Compose GitHub releases page. There’s no built-in auto-check command, so this comparison needs to be done manually or scripted into your provisioning process.

    Do I need to change my docker-compose.yml file when upgrading to the latest Docker Compose version?
    Usually not for minor version upgrades. Major format changes (like the shift away from a required version: key) are backward compatible — V2 will still read older files that include a version key, it just ignores it. It’s still good practice to run docker compose config --quiet after any upgrade to catch unexpected parsing differences.

    Can I run both docker-compose (V1) and docker compose (V2) on the same machine?
    Yes, technically, if the V1 binary is still present alongside the V2 plugin. This is not recommended for anything beyond a temporary migration period, since it invites confusion about which tool actually executed a given command, especially in scripts that mix the hyphenated and non-hyphenated forms.

    Conclusion

    The latest Docker Compose version is almost always the V2 CLI plugin, not the deprecated standalone V1 binary. Checking your version with docker compose version, keeping the Docker Engine and Compose plugin updated through your platform’s normal package-management path, and validating your compose files after upgrades are simple habits that prevent most version-related surprises. For teams running multiple services — databases, automation engines, or custom applications — staying current on Compose also means staying current on fixes to dependency ordering, health checks, and file-format handling that directly affect how reliably your stack starts up. For deeper platform-specific reference, Docker’s own Compose documentation and the general Docker Engine documentation remain the most reliable sources as the tool continues to evolve.

  • Docker Compose Postgres

    Docker Compose Postgres: A Complete Setup and Operations Guide

    Disclosure: This post contains one or more links to providers we have a real, registered affiliate/referral relationship with. We may earn a commission at no extra cost to you if you sign up through them.

    Running a PostgreSQL instance with Docker Compose is one of the fastest ways to get a reliable, reproducible database environment for local development, staging, or a small production workload. This guide walks through a real docker compose postgres configuration, covering persistence, environment variables, networking, backups, and common troubleshooting steps so you can run Postgres in containers with confidence.

    Postgres is one of the most widely deployed relational databases, and Docker Compose makes it trivial to spin up a consistent instance across machines without touching a host-level package manager. Whether you’re prototyping an application, running CI tests, or managing a small self-hosted stack, a docker compose postgres setup gives you version-pinned, disposable infrastructure that behaves the same way everywhere.

    Why Use Docker Compose for Postgres

    Before diving into configuration, it helps to understand why so many teams choose a docker compose postgres approach over installing Postgres directly on the host.

    A containerized database gives you:

  • Version isolation — run Postgres 14, 15, or 16 side by side without conflicting installations
  • Easy teardown and rebuild — destroy and recreate the container without touching host state
  • Portable configuration — the same docker-compose.yml works on a laptop, a CI runner, or a VPS
  • Simplified onboarding — new team members run one command instead of following a multi-step install guide
  • This isn’t a claim that containers are inherently faster or “the best” way to run a database — it’s a practical tradeoff. For small-to-medium workloads and development environments, the operational simplicity is usually worth it. For very large, latency-sensitive production databases, a managed service or dedicated host may be more appropriate.

    Basic Docker Compose Postgres Configuration

    The minimum viable docker compose postgres setup only needs a handful of fields: an image, a set of environment variables, and a persistent volume.

    version: "3.9"
    
    services:
      postgres:
        image: postgres:16
        container_name: postgres_db
        restart: unless-stopped
        environment:
          POSTGRES_USER: appuser
          POSTGRES_PASSWORD: change_this_password
          POSTGRES_DB: appdb
        ports:
          - "5432:5432"
        volumes:
          - postgres_data:/var/lib/postgresql/data
    
    volumes:
      postgres_data:

    Run it with:

    docker compose up -d

    This starts a single Postgres container, exposes port 5432 to the host, and stores data in a named Docker volume so it survives container restarts and recreation.

    Choosing the Right Postgres Image Tag

    Always pin your image to a specific major version (postgres:16, not postgres:latest). An unpinned tag means a future docker compose pull could silently move you to a new major version with a different on-disk data format, which Postgres does not support upgrading in place without a dump-and-restore or a dedicated upgrade tool. Pinning avoids that surprise entirely.

    Environment Variables That Matter

    The official Postgres image reads several environment variables at first-run time only — they are applied when the data directory is initialized, not on every subsequent start:

  • POSTGRES_USER — the superuser role created on first boot
  • POSTGRES_PASSWORD — required unless you explicitly opt into trust authentication
  • POSTGRES_DB — an initial database created alongside the user
  • PGDATA — override the internal data directory path if needed
  • If you need to manage many environment values across services, see this guide on how to manage Docker Compose environment variables cleanly using .env files instead of hardcoding secrets into the compose file.

    Persisting Data With Volumes

    The single most important part of any docker compose postgres deployment is making sure data survives a container restart, rebuild, or host reboot. Without a volume, deleting the container deletes your database.

    There are two common approaches:

    Named Volumes

    Named volumes (as shown in the example above) are managed by Docker itself and stored under /var/lib/docker/volumes/. They’re the recommended default because Docker handles permissions and lifecycle for you, and they work identically across Linux, macOS, and Windows hosts.

    Bind Mounts

    A bind mount maps a specific host directory into the container:

    volumes:
      - ./pgdata:/var/lib/postgresql/data

    Bind mounts are useful when you want direct filesystem access to the data files from the host — for example, to inspect them with host-level backup tooling — but they’re more sensitive to permission mismatches between the host user and the container’s postgres user. For most setups, a named volume is the simpler and more portable choice.

    Networking and Connecting to Postgres From Other Containers

    A docker compose postgres service running alongside your application doesn’t need its port published to the host at all if only other containers need to reach it. Compose automatically creates a shared network where services can resolve each other by service name.

    services:
      postgres:
        image: postgres:16
        environment:
          POSTGRES_USER: appuser
          POSTGRES_PASSWORD: change_this_password
          POSTGRES_DB: appdb
        volumes:
          - postgres_data:/var/lib/postgresql/data
    
      app:
        build: .
        environment:
          DATABASE_URL: postgresql://appuser:change_this_password@postgres:5432/appdb
        depends_on:
          - postgres
    
    volumes:
      postgres_data:

    Here, the app service connects to postgres:5432 using the service name as the hostname — no port needs to be exposed externally unless you want to connect from a local client like psql or a GUI tool on the host machine.

    Using depends_on and Healthchecks Correctly

    depends_on only controls container start order, not database readiness. Postgres can still be initializing when your application container starts, causing early connection failures. A healthcheck fixes this:

    services:
      postgres:
        image: postgres:16
        environment:
          POSTGRES_USER: appuser
          POSTGRES_PASSWORD: change_this_password
          POSTGRES_DB: appdb
        volumes:
          - postgres_data:/var/lib/postgresql/data
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
          interval: 5s
          timeout: 5s
          retries: 5
    
      app:
        build: .
        depends_on:
          postgres:
            condition: service_healthy
    
    volumes:
      postgres_data:

    Now app waits until Postgres actually reports itself ready via pg_isready, not just until the container process has started.

    Backups and Data Safety

    Running Postgres in Docker doesn’t change the fundamentals of database backup strategy — you still need a repeatable, tested backup process, container or not.

    Manual Dumps With pg_dump

    The simplest backup approach is running pg_dump inside the running container:

    docker compose exec postgres pg_dump -U appuser appdb > backup.sql

    Restoring is the reverse:

    cat backup.sql | docker compose exec -T postgres psql -U appuser -d appdb

    Automating Backups

    For anything beyond occasional manual dumps, schedule this command via cron or a dedicated backup sidecar container, and store the resulting archive somewhere outside the Docker host — object storage or a separate backup volume. A backup that only lives on the same disk as the database it protects doesn’t protect against disk failure.

    If you’re running Postgres as part of a larger automation stack, it’s also worth reviewing how Docker Compose volumes behave under different backup and snapshot strategies, since volume management directly affects how safely you can restore state.

    Troubleshooting Common Docker Compose Postgres Issues

    Even a straightforward docker compose postgres setup runs into a handful of recurring problems. A few of the most common:

    Container Restarts in a Loop

    If the Postgres container keeps restarting, check the logs first:

    docker compose logs postgres

    For a deeper look at filtering and following logs across a multi-service stack, this Docker Compose logs debugging guide covers flags and patterns that make diagnosing startup failures much faster.

    A common cause is a data directory that was initialized with a different Postgres major version than the image now specifies — Postgres refuses to start against an incompatible on-disk format. The fix is either reverting the image tag or performing a proper version upgrade (dump on the old version, restore on the new one).

    “Connection Refused” From the Application Container

    This almost always means one of three things: the healthcheck/depends_on gating isn’t in place yet, the application is trying to connect to localhost instead of the service name (postgres in the examples above), or the password/user doesn’t match what was set on first initialization (remember, POSTGRES_PASSWORD is only applied on the very first container start — changing it later in the compose file has no effect on an existing volume).

    Environment Variables Not Taking Effect

    Because Postgres only reads POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB during first-time initialization, changing them after the volume already contains data does nothing. To apply new values, you either need to run the appropriate ALTER ROLE/CREATE DATABASE SQL manually, or remove the volume and reinitialize (losing existing data unless backed up first).

    Comparing Docker Compose Postgres to a Managed Database

    It’s worth being honest about tradeoffs. A docker compose postgres setup gives you full control and no vendor lock-in, but you’re also responsible for patching, backups, high availability, and monitoring yourself. A managed database service handles most of that operational burden for you, at the cost of less control and ongoing hosting fees.

    For development, CI, and small self-hosted production workloads — the kind commonly deployed on a single VPS — running Postgres via Docker Compose is a solid, well-understood approach. If you’re deploying this alongside other self-hosted services on a VPS, providers like DigitalOcean or Hetzner offer straightforward virtual machines that work well for this kind of Docker-based stack.

    If you’re also comparing container orchestration options for running Postgres at larger scale, this Kubernetes vs Docker Compose comparison walks through when it makes sense to graduate beyond a single-host Compose setup.


    Recommended: Ready to put this into practice? DigitalOcean is a tool we use for exactly this, and we have a real, disclosed affiliate relationship with them.

    FAQ

    Does Docker Compose Postgres data persist after docker compose down?
    Yes, as long as you’re using a named volume or bind mount (not an anonymous volume). docker compose down removes containers and the default network but leaves named volumes intact. Data is only lost if you run docker compose down -v, which explicitly removes volumes too.

    How do I change the Postgres password after the container has already initialized?
    Environment variables like POSTGRES_PASSWORD only apply on first initialization. To change the password afterward, connect with psql and run ALTER ROLE appuser WITH PASSWORD 'new_password'; directly against the running database.

    Can I run multiple Postgres containers in the same Docker Compose Postgres project?
    Yes. Give each service a distinct name, a distinct volume, and either distinct host ports (if you need external access to both) or none at all if they’re only reached by other containers over the internal network.

    Why does my docker compose postgres container fail to start after a Postgres version upgrade in the image tag?
    Postgres data files are not forward-compatible in place across major versions. You need to either keep the image tag matching the version the volume was initialized with, or perform a proper dump-and-restore migration to the new version.

    Conclusion

    A docker compose postgres setup gives you a reproducible, version-pinned database environment with minimal configuration overhead. The core requirements are simple — persist data with a named volume, pin your image tag, gate application startup on a real healthcheck rather than just container start order, and maintain a tested backup process outside the container itself. Get those fundamentals right, and Docker Compose is a dependable way to run Postgres for development, CI, and many production workloads. For further reading on the underlying tooling, the official Docker documentation and the PostgreSQL documentation are the most reliable references for configuration options not covered here.