DEV Community 👩‍💻👨‍💻

Cover image for Deploy your docker containers with zero-downtime
Wassim Ben Jdida
Wassim Ben Jdida

Posted on • Updated on

Deploy your docker containers with zero-downtime

I like using docker, everywhere, and especially docker-compose and its magic.

currently I'm working on an application, I will be deploying the app so soon, I searched the internet how to deploy an app using only my docker-compose file, but I only found docker swarm, nginx-porxy and other stuff that are not free.

in this article I will explain how you can deploy your app with zero-downtime, using just your docker-compose file.

Docker-compose setup

This is the docker-compose file i will explain what's actully needs to be added to make the zero-downtime works.


version: "3.6"

    image: postgres
    restart: always
      - 5432:5432
    container_name: pqdb
      - db_nw

      dockerfile: Dockerfile.server
      - "8989"
      - db_nw
      - web_nw
    restart: always
      - ../:/apps/go-app
      - app_db  

    image: nginx:latest
    container_name: nginx
    restart: always
      - ./cfg/nginx.conf:/etc/nginx/nginx.conf
      - 8888:80
      - web_nw
      - app_server

    driver: bridge
    driver: bridge
Enter fullscreen mode Exit fullscreen mode

i have 3 services, app_db, app_server and app_nginx. nothing fancy here. the most important piece here is not setting a container name nor port mapping ("1234:0987") on the app_server service i will tell you why later.

but you are maybe wondering is the custom networks necessary for making this works ? short answer is No.

let me explain docker networks first cause it has big role in making this works.

Docker networks

When you run your docker-compose file, docker connects your services to the same network which is the default network called bridge, and i think the name is self-explanatory, it creates a bridge between the services so they can communicate with each other, and the fun part is you don't need to know each container IP address, just use the service name and docker will handle the rest.

but Im creating custom networks so I can make sure that each service is talking with the service that it suppose to talk with.

if you want to learn more about docker networks and you should do, read this article


nginx is also an important service here, i will show you the config file and then explain.


http {
   server {
      listen 80;
      location / {
         proxy_pass "http://app_server:8989";

events {}
Enter fullscreen mode Exit fullscreen mode

this is the most basic nginx config on earth, we are creating a web server that listens on port 80 and pass requests to our app server.
as you can see im using the service name here, docker will translate that to the proper container IP address.

Now if you run your docker compose file you should be able to make requests to the web server which we mapped it to port 8888

curl http://localhost:8888

But wait...

yes, you want to know why i didn't put the container name and not mapping a port to the app_server service. right ?
well, simply not setting them will let docker make the decision, it will create a name that is identical to the service name but suffixed with the container number/index.
example, if you run your docker compose file and type

docker ps

you will see that the app_server container name is actually app_server_1.
ok what about the port ? also not mapping a port to the service will let docker choose a dynamic port and map it to the port that we set on the service, each time you run the docker compose file the port will change.

The fun part

now we will make the zero-downtime deployment works. we will write a simple and small bash script, that will do the following:

1) create a new app_server instance (with the new code changes)
2) check if its up and running
3) reload nginx (so it can recognize the new instance)
4) delete the old instance (with the old code)

let me show you the script and then I will explain how and why ?


reload_nginx() {  
  docker exec $nginx_container_name /usr/sbin/nginx -s reload  

# server health check
server_status() {
  # $1 = first func arg
  local port=$1
  local status=$(curl -is --connect-timeout 5 --show-error http://localhost:$port | head -n 1 | cut -d " " -f2)

  # if status is not a status code (123), means we got an error not an http header
  # this could be a timeout message, connection refused error msg, and so on...
  if [[ $(echo ${#status}) != 3 ]]; then
    echo "503"

  echo $status

update_server() {

  old_container_id=$(docker ps -f name=$service_name -q | tail -n1)

  # create a new instance of the server
  docker-compose up --build -d --no-deps --scale $service_name=2 --no-recreate $service_name
  new_container_id=$(docker ps -f name=$service_name -q | head -n1)

  if [[ -z $new_container_id ]]; then
    echo "ID NOT FOUND, QUIT !"
  new_container_port=$(docker port $new_container_id | cut -d " " -f3 | cut -d ":" -f2)

  if [[ -z $new_container_port ]]; then
    echo "PORT NOT FOUND, QUIT !"

  # sleep until server is up
  while [[ $(server_status $new_container_port) > "404" ]]; do
    echo "New instance is getting ready..."
    sleep 3

  # ---- server is up ---

  # reload nginx, so it can recognize the new instance

  # remove old instance 
  docker rm $old_container_id -f

  # reload ngnix, so it stops routing requests to the old instance

  echo "DONE !"

# call func
Enter fullscreen mode Exit fullscreen mode

we have 3 functions:
1) reloading nginx
actually reloading nginx doesn't cause any downtime

3) checking the server health

curl -is --connect-timeout 5 --show-error http://localhost:$port
Enter fullscreen mode Exit fullscreen mode

this will return the response headers, as we can see the first line is the http status code

HTTP/1.1 200 OK
Age: ...
Cache-Control: ...
Content-Type: ...
Enter fullscreen mode Exit fullscreen mode

we want the first line

head -n1
Enter fullscreen mode Exit fullscreen mode

split the text based on spaces and get the second field which is 200 in our example

cut -d " " -f2
Enter fullscreen mode Exit fullscreen mode

3) update server
in this function we get the current or the old server container id, then we create a new instance of the app_server service without touching other linked services,

docker-compose up -d --no-deps --scale $service_name=2 --no-recreate $service_name
Enter fullscreen mode Exit fullscreen mode

then we get this new container id and port so we can check its status if its up so we can then stop the old container and start sending requests to the new container.


so how this will work ? how will i deploy my server and update it ?
if you run the script on your machine, it should work on your server machine cause thats how docker works, right ?

you can use scp (secure copy) to copy your code from your local machine to the server, and you can use the same command to update the code on the server

scp USER@IP:~/apps ./local/app
Enter fullscreen mode Exit fullscreen mode

you should use docker-compose up first of all if its your first time your deploy code. and then run the update_server script to update your changes without any downtime.

you can also use git which i prefer to deploy and update your server and use git hooks so whenever there is a git push to your server you run the update_server script

read more how you can use git to deploy your code, as simple as git push.


Bash cheat sheet
Docker cheat sheet

Top comments (5)

ciprian_42 profile image

When running docker-compose up -d --no-deps --scale $service_name=2 --no-recreate $service_name, my newly created service exits, as the port "X" is already used by the first service, that I'm trying to scale up.

In your case, how do you solve the port duplication issue? Thanks for a great tutorial!

wassimbj profile image
Wassim Ben Jdida Author

Hello ciprian, what you should do is remove the manual port mapping you have on the docker-compose file, that will let docker generate a random port for you, just read the first part that talks about the docker compose configuration
should become like this

if it didn't work just rebuild the docker compose file

kweigold profile image

Am trying to do this but nginix stops processing requests once docker rm happens and doesnt restart until after the nginx reload occurs. sometimes this can take a bit of time for the docker rm to stop and remove and any call in progress seems to hang until its own timeout occurs. Any ideas on why it would stop processing on the docker rm when their is already a 2nd docker image that should be handing requests running?

adityabraj profile image
Aditya Bhuvanraj

Hi Wassim. This solution will work with a single container deployment, but what if we are already using the docker-compose scale feature to deploy multiple containers, then how will the update_server() and the nginx conf turn out?

wassimbj profile image
Wassim Ben Jdida Author • Edited on

I didnt try this, but what i think you can do is get the old containers as an array instead of just a variable, loop throught the array and re-do whats in the bash script, get the port foreach container duplicate it and shutdown the old one

DEV has this feature:


Go to your customization settings to nudge your home feed to show content more relevant to your developer experience level. 🛠