Docker Community Forums

Share and learn in the Docker community.

How to gracefully exit and remove docker-compose services after issuing CTRL-C?

Hi,

I have a docker-compose service that runs django using gunicorn in an entrypoint shell script.

When I issue CTRL-C after the docker-compose stack has been started, the web and nginx services do not gracefully exit and are not deleted. How do I configure the docker environment so that the services are removed when a CTRL-C is issued?

I have tried using stop_signal: SIGINT but the result is the same. Any ideas?

docker-compose log after CTRL-C issued

^CGracefully stopping... (press Ctrl+C again to force)
Killing nginx  ... done
Killing web    ... done

docker containers after CTRL-C is issued

CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS                       PORTS     NAMES
4b2f7db95c90   nginx:alpine   "/docker-entrypoint.…"   5 minutes ago   Exited (137) 5 minutes ago             nginx
cdf3084a8382   myimage        "./docker-entrypoint…"   5 minutes ago   Exited (137) 5 minutes ago             web

Dockerfile

#
# Use poetry to build wheel and install dependencies into a virtual environment.
# This will store the dependencies during compile docker stage.
# In run stage copy the virtual environment to the final image. This will reduce the 
# image size.
#
# Install poetry using pip, to allow version pinning. Use --ignore-installed to avoid
# dependency conflicts with poetry.
#

# ---------------------------------------------------------------------------------------

##
# base: Configure python environment and set workdir
##
FROM python:3.8-slim as base

ENV PYTHONDONTWRITEBYTECODE=1 \
  PYTHONFAULTHANDLER=1 \
  PYTHONHASHSEED=random \
  PYTHONUNBUFFERED=1
  
WORKDIR /app

# configure user pyuser:
RUN useradd --user-group --create-home --no-log-init --shell /bin/bash pyuser && \
  chown pyuser /app


# ---------------------------------------------------------------------------------------

##
# compile:  Install dependencies from poetry exported requirements
#           Use poetry to build the wheel for the python package.
#           Install the wheel using pip.
##

FROM base as compile

ARG DEPLOY_ENV=development \
  POETRY_VERSION=1.1.7

# pip:
ENV PIP_DEFAULT_TIMEOUT=100 \
  PIP_DISABLE_PIP_VERSION_CHECK=1 \
  PIP_NO_CACHE_DIR=1

# system dependencies:
RUN apt-get update && \
  apt-get install -y --no-install-recommends \
  build-essential gcc && \
  apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false && \
  apt-get clean -y && \
  rm -rf /var/lib/apt/lists/*

# install poetry, ignoring installed dependencies
RUN pip install --ignore-installed "poetry==$POETRY_VERSION" 

# virtual environment:
RUN python -m venv /opt/venv
ENV VIRTUAL_ENV=/opt/venv 
ENV PATH="$VIRTUAL_ENV/bin:$PATH"

# install dependencies:
COPY pyproject.toml poetry.lock ./
RUN /opt/venv/bin/pip install --upgrade pip \
  && poetry install $(if [ "$DEPLOY_ENV" = 'production' ]; then echo '--no-dev'; fi) \
    --no-ansi \
    --no-interaction

# copy source:
COPY . .

# build and install wheel:
RUN poetry build && /opt/venv/bin/pip install dist/*.whl

# -------------------------------------------------------------------------------------------

##
# run:        Copy virtualenv from compile stage, to reduce final image size
#               Run the docker-entrypoint.sh script as pyuser
# 
#               This performs the following actions when the container starts:
#               - Make and run database migrations
#               - Collect static files
#               - Create the superuser
#               - Run wsgi app using gunicorn
#               
# port:         5000
#
# build args:   
#
#   GIT_HASH                    Git hash the docker image is derived from
#
# environment:
#
#   DJANGO_DEBUG                True if django debugging is enabled
#   DJANGO_SECRET_KEY           The secret key used for django server, defaults to secret
#   DJANGO_SUPERUSER_EMAIL      Django superuser email, default=myname@example.com
#   DJANGO_SUPERUSER_PASSWORD   Django superuser passwd, default=Pa55w0rd
#   DJANGO_SUPERUSER_USERNAME   Django superuser username, default=admin
##

FROM base as run

ARG GIT_HASH

ENV DJANGO_DEBUG=${DJANGO_DEBUG:-False}
ENV DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY:-secret}
ENV DJANGO_SETTINGS_MODULE=default_project.main.settings
ENV DJANGO_SUPERUSER_EMAIL=${DJANGO_SUPERUSER_EMAIL:-"myname@example.com"}
ENV DJANGO_SUPERUSER_PASSWORD=${DJANGO_SUPERUSER_PASSWORD:-"Pa55w0rd"}
ENV DJANGO_SUPERUSER_USERNAME=${DJANGO_SUPERUSER_USERNAME:-"admin"}

ENV GIT_HASH=${GIT_HASH:-dev}

# install virtualenv from compiled image
COPY --chown=pyuser:pyuser --from=compile /opt/venv /opt/venv

# set path for virtualenv and VIRTUAL_ENV toactivate virtualenv
ENV VIRTUAL_ENV="/opt/venv"
ENV PATH="$VIRTUAL_ENV/bin:$PATH"

COPY --chown=pyuser:pyuser ./docker/docker-entrypoint.sh ./

USER pyuser

RUN mkdir /opt/venv/lib/python3.8/site-packages/default_project/staticfiles

EXPOSE 5000

ENTRYPOINT ["./docker-entrypoint.sh"]

Entrypoint

#!/bin/sh

set -e

echo "Making migrations..."
django-admin makemigrations

echo "Running migrations..."
django-admin migrate

echo "Making staticfiles..."
mkdir -p /opt/venv/lib/python3.8/site-packages/default_project/staticfiles

echo "Collecting static files..."
django-admin collectstatic --noinput

# requires gnu text tools
# echo "Compiling translation messages..."
# django-admin compilemessages

# echo "Making translation messages..."
# django-admin makemessages

if [ "$DJANGO_SUPERUSER_USERNAME" ]
then
    echo "Creating django superuser"
    django-admin createsuperuser \
        --noinput \
        --username $DJANGO_SUPERUSER_USERNAME \
        --email $DJANGO_SUPERUSER_EMAIL
fi

exec gunicorn \
  --bind 0.0.0.0:5000 \
  --forwarded-allow-ips='*' \
  --worker-tmp-dir /dev/shm \
  --workers=4 \
  --threads=1 \
  --worker-class=gthread \
  default_project.main.wsgi:application

exec "$@"

docker-compose

version: '3.8'
services:
  web:
    container_name: web 
    image: myimage
    init: true
    build:
      context: .
      dockerfile: docker/Dockerfile
    environment:
      - DJANGO_DEBUG=${DJANGO_DEBUG}
      - DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
      - DJANGO_SUPERUSER_EMAIL=${DJANGO_SUPERUSER_EMAIL}
      - DJANGO_SUPERUSER_PASSWORD=${DJANGO_SUPERUSER_PASSWORD}
      - DJANGO_SEUPRUSER_USERNAME=${DJANGO_SUPERUSER_USERNAME}
    # stop_signal: SIGINT
    volumes:
      - static-files:/opt/venv/lib/python3.8/site-packages/{{ cookiecutter.project_name }}/staticfiles:rw
    ports:
      - 127.0.0.1:${DJANGO_PORT}:5000

  nginx:
    container_name: nginx
    image: nginx:alpine
    volumes:
      - ./docker/nginx:/etc/nginx/conf.d
      - static-files:/static
    depends_on:
      - web
    ports:
      - 127.0.0.1:8000:80

volumes:
  static-files:

Hi, just type “docker-compose down” in another terminal or after CTRL-C and all your Exited containers will be removed

Many thanks. Yes, currently, that is what I have to do to terminate the containers. Is there anyway of making the containers exit gracefully and cleanly upon receipt of a CTRL-C signal, without requiring this additional manual step?