每次启动 Docker 容器时,如何防止我的脚本 运行?

How do I prevent my script from being run every time a Docker container is brought up?

我会 运行 一个脚本(填充我的 MySql Docker 容器)只有当我的 docker 容器被构建时。我正在 运行 创建以下 docker-compose.yml 文件,其中包含一个 Django 容器。

version: '3'

services:
  mysql:
    restart: always
    image: mysql:5.7
    environment:
      MYSQL_DATABASE: 'maps_data'
      # So you don't have to use root, but you can if you like
      MYSQL_USER: 'chicommons'
      # You can use whatever password you like
      MYSQL_PASSWORD: 'password'
      # Password for root access
      MYSQL_ROOT_PASSWORD: 'password'
    ports:
      - "3406:3406"
    volumes:
      - my-db:/var/lib/mysql

  web:
    restart: always
    build: ./web
    ports:           # to access the container from outside
      - "8000:8000"
    env_file: .env
    environment:
      DEBUG: 'true'
    command: /usr/local/bin/gunicorn maps.wsgi:application -w 2 -b :8000
    depends_on:
      - mysql

  apache:
    restart: always
    build: ./apache/
    ports:
      - "80:80"
    #volumes:
    #  - web-static:/www/static
    links:
      - web:web

volumes:
  my-db:

我有这个web/Dockerfile

FROM python:3.7-slim

RUN apt-get update && apt-get install

RUN apt-get install -y libmariadb-dev-compat libmariadb-dev
RUN apt-get update \
    && apt-get install -y --no-install-recommends gcc \
    && rm -rf /var/lib/apt/lists/*

RUN python -m pip install --upgrade pip
RUN mkdir -p /app/

WORKDIR /app/

COPY requirements.txt requirements.txt
RUN python -m pip install -r requirements.txt

COPY entrypoint.sh /app/
COPY . /app/
RUN ["chmod", "+x", "/app/entrypoint.sh"]

ENTRYPOINT ["/app/entrypoint.sh"]

这些是我的 entrypoint.sh 文件的内容

#!/bin/bash
set -e

python manage.py migrate maps
python manage.py loaddata maps/fixtures/country_data.yaml
python manage.py loaddata maps/fixtures/seed_data.yaml

exec "$@"

问题是,当我重复 运行 "docker-compose up," 时,entrypoint.sh 脚本正在使用其命令获取 运行。我希望命令仅在首次构建 docker 容器时获得 运行,但在恢复容器时它们似乎总是获得 运行。有什么方法可以调整我必须实现的目标吗?

为什么不使用 entrypoint.sh 文件,而是 运行 web/Dockerfile 中的命令?

RUN python manage.py migrate maps
RUN python manage.py loaddata maps/fixtures/country_data.yaml
RUN python manage.py loaddata maps/fixtures/seed_data.yaml

这样,这些更改将被烘焙到映像中,并且当您启动映像时,这些更改将已经执行。

我之前用过的一种方法是把你的loaddata调用包装在你自己的管理命令中,它首先检查数据库中是否有任何数据,如果有,什么都不做.像这样:

# your_app/management/commands/maybe_init_data.py

from django.core.management import call_command
from django.core.management.base import BaseCommand

from address.models import Country

class Command(BaseCommand):

    def handle(self, *args, **options):
        if not Country.objects.exists():
            self.stdout.write('Seeding initial data')
            call_command('loaddata', 'maps/fixtures/country_data.yaml')
            call_command('loaddata', 'maps/fixtures/seed_data.yaml')

然后将您的入口点脚本更改为:

python manage.py migrate
python manage.py maybe_init_data

(这里假设您有一个 Country 模型 - 替换为您在灯具中实际拥有的模型。)

我最近有一个类似的案例。由于 "ENTRYPOINT" 包含每次容器启动时都会执行的命令,因此解决方案是在 entrypoint.sh 脚本中包含一些逻辑以避免如果这些操作的效果已经存在于数据库中,则应用更新(在您的情况下是数据的迁移和加载)。

例如:

#!/bin/bash
set -e

#Function that goes to verify if effects of migration and load data are present on database
function checkEffects() {
  IS_UPDATED=0
  #Check effects and set to 1 IS_UPDATED if effects are not present
}

checkEffects
if [[ $IS_UPDATED == 0 ]]
then
   echo "Database already initialized. Nothing to do"
else
   echo "Database is clean. Initializing it"
   python manage.py migrate maps
   python manage.py loaddata maps/fixtures/country_data.yaml
   python manage.py loaddata maps/fixtures/seed_data.yaml
fi

exec "$@"

但是情况更复杂因为验证允许决定是否继续更新的效果可能会非常困难,如果这些涉及多个数据和数据. 此外,如果你考虑随着时间的推移容器升级,它会变得非常复杂。

Example: Today you're working with a local Dockerfile for your web service but I think in production you'll start to versioning this service uploading it on a Docker registry. So when you'll upload your first release ( for example the 1.0.0 version ) you'll specify the following on your docker-compose.yml:

web:
   restart: always
   image: <DOCKER_REGISTRY_HOST>:<DOCKER_REGISTRY_PORT>/web:1.0.0 
   ports: # to access the container from outside
     - "8000:8000" 

Then you'll release the "1.2.0" version of the web service container when you'll include other changes on the schema for example loading other data on entrypoint.sh:

#1.0.0 updates  
python manage.py migrate maps 
python manage.py loaddata maps/fixtures/country_data.yaml 
python manage.py loaddata maps/fixtures/seed_data.yaml
#1.2.0 updates   
python manage.py loaddata maps/fixtures/other_seed_data.yaml 

Here you'll have 2 scenarios ( let's ignore for now the need to check for effects on the script ):

1- You deploy for the first time your services with web:1.2.0: As you start from a clean database you should be sure that all updates are executed ( both 1.1.0 and 1.2.0 ).

The solution to this case is easy because you can just execute all updates.
2- You upgrade web container to 1.2.0 on an existing environment where 1.0.0 was running: As your database has been initialized with updates from 1.0.0 you should be sure that only 1.2.0 updates are executed

Here is difficult because you should be able to check what is the version on database applied in order to skip 1.0.0 updates. This will means you should store the web version somewhere on database for example

根据所有这些讨论,我认为最好的解决方案是直接处理用于创建模式和填充数据的脚本,以使这些指令幂等,并注意升级指令。

一些例子:

1- 创建一个 table

创建 table 如下:

CREATE TABLE country

使用 if not exists 来避免 table already present 错误:

CREATE TABLE IF NOT EXISTS country

2-插入默认数据

而不是插入没有指定主键的数据:

INSERT INTO maps.country (name) VALUES ("USA");

包括主键以避免重复:

INSERT INTO maps.country (id,name) VALUES (1,"USA");

在第一个 运行 中为您的数据库播种的想法是一种非常常见的情况。正如其他人所建议的那样,您可以更改 entrypoint.sh 脚本并对其应用一些条件逻辑,使其按照您希望的方式工作。

但我认为,如果您将 seeding the databaserunning services 的逻辑分开并且不要让它们相互纠缠在一起,那将是一个真正更好的做法。这可能会在将来导致一些不需要的行为。

我打算建议使用 docker-compose 的解决方法,并在执行 docker-compose up 时开始搜索一些排除某些服务的语法,但发现这仍然是 open issue. But I found this stack overflow answer 提出了一个非常好的方法。

version: '3'

services:
  all-services:
    image: docker4w/nsenter-dockerd # you want to put there some small image
    command: sh -c "echo start"
    depends_on:
      - mysql
      - web
      - apache

  mysql:
    restart: always
    image: mysql:5.7
    environment:
      MYSQL_DATABASE: 'maps_data'
      # So you don't have to use root, but you can if you like
      MYSQL_USER: 'chicommons'
      # You can use whatever password you like
      MYSQL_PASSWORD: 'password'
      # Password for root access
      MYSQL_ROOT_PASSWORD: 'password'
    ports:
      - "3406:3406"
    volumes:
      - my-db:/var/lib/mysql

  web:
    restart: always
    build: ./web
    ports:           # to access the container from outside
      - "8000:8000"
    env_file: .env
    environment:
      DEBUG: 'true'
    command: /usr/local/bin/gunicorn maps.wsgi:application -w 2 -b :8000
    depends_on:
      - mysql

  apache:
    restart: always
    build: ./apache/
    ports:
      - "80:80"
    #volumes:
    #  - web-static:/www/static
    links:
      - web:web

  seed:
    build: ./web
    env_file: .env
    environment:
      DEBUG: 'true'
    entrypoint: /bin/bash -c "/bin/bash -c \"$${@}\""
    command: |
      /bin/bash -c "
        set -e
        python manage.py loaddata maps/fixtures/country_data.yaml
        python manage.py loaddata maps/fixtures/seed_data.yaml
        /bin/bash || exit 0
      "
    depends_on:
      - mysql

volumes:
  my-db:

如果你使用上面类似的东西,你将能够在运行宁docker-compose up.

之前运行seeding阶段

为您的数据库播种,运行:

docker-compose up seed

要运行整合所有堆栈,请使用:

docker-compose up -d all-services

我认为这是一种干净的方法,可以扩展到许多不同的场景和用例。

更新

如果您真的希望能够 运行 整个堆栈并防止多次 运行ning loaddata 命令引起的意外行为,我建议您定义一个new django management command 检查现有数据。看看这个:

checkseed.py

from django.core.management.base import BaseCommand, CommandError
from project.models import Country  # or whatever model you have seeded

class Command(BaseCommand):
    help = 'Check if seed data already exists'

    def handle(self, *args, **options):
        if Country.objects.all().count() > 0:
            self.stdout.write(self.style.WARNING('Data already exists .. skipping'))
            return False
        # do all the checks for your data integrity
        self.stdout.write(self.style.SUCCESS('Nothing exists'))
        return True

在此之后,您可以更改 docker-composeseed 部分,如下所示:

  seed:
    build: ./web
    env_file: .env
    environment:
      DEBUG: 'true'
    entrypoint: /bin/bash -c "/bin/bash -c \"$${@}\""
    command: |
      /bin/bash -c "
        set -e
        python manage.py checkseed &&
        python manage.py loaddata maps/fixtures/country_data.yaml
        python manage.py loaddata maps/fixtures/seed_data.yaml
        /bin/bash || exit 0
      "
    depends_on:
      - mysql

这样,您可以确定如果任何人 运行s docker-compose up -d 错误,不会导致完整性错误和类似的问题。

通常 builddeploy 步骤是分开的。

您的 ENTRYPOINT部署 的一部分。 如果你想手动配置 deploy 运行 should 运行 migrate commands and just replace containers by a new container (maybe from fresh image), 然后你可以把它分成一个单独的命令

启动数据库(如果不是运行ning)

docker-compose -p production -f docker-compose.yml up mysql -d

迁移

docker run \
        --rm \
        --network production_default \
        --env-file docker.env \
        --entrypoint python \
        my-backend-image-name:prod python manage.py migrate maps

然后部署新映像

docker-compose -p production -f docker-compose.yml up -d

并且每次都手动决定是否运行 迁移步骤