Introduce unified Docker / Docker Compose infrastructure

This commit is contained in:
Nikita P. Shupeyko 2018-06-27 21:12:45 +03:00
parent 3ac9902670
commit 36535770d1
42 changed files with 225 additions and 267 deletions

View File

@ -46,7 +46,7 @@ You will probably also need to setup the Mail backend, for example by adding a `
Optional: Use AWS IAM Role for EC2 instance
-------------------------------------------
If you are deploying to AWS, you can use the IAM role to substitute AWS credentials, after which it's safe to remove the ``AWS_ACCESS_KEY_ID`` AND ``AWS_SECRET_ACCESS_KEY`` from ``.envs/.production/.django``. To do it, create an `IAM role`_ and `attach`_ it to the existing EC2 instance or create a new EC2 instance with that role. The role should assume, at minimum, the ``AmazonS3FullAccess`` permission.
If you are deploying to AWS, you can use the IAM role to substitute AWS credentials, after which it's safe to remove the ``AWS_ACCESS_KEY_ID`` AND ``AWS_SECRET_ACCESS_KEY`` from ``.envs/.django``. To do it, create an `IAM role`_ and `attach`_ it to the existing EC2 instance or create a new EC2 instance with that role. The role should assume, at minimum, the ``AmazonS3FullAccess`` permission.
.. _IAM role: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
.. _attach: https://aws.amazon.com/blogs/security/easily-replace-or-attach-an-iam-role-to-an-existing-ec2-instance-by-using-the-ec2-console/

View File

@ -87,31 +87,24 @@ This is the excerpt from your project's ``local.yml``: ::
# ...
postgres:
build:
context: .
dockerfile: ./compose/production/postgres/Dockerfile
volumes:
- local_postgres_data:/var/lib/postgresql/data
- local_postgres_data_backups:/backups
# ...
env_file:
- ./.envs/.local/.postgres
- ./.envs/.postgres.local
# ...
The most important thing for us here now is ``env_file`` section enlisting ``./.envs/.local/.postgres``. Generally, the stack's behavior is governed by a number of environment variables (`env(s)`, for short) residing in ``envs/``, for instance, this is what we generate for you: ::
The most important thing for us here now is ``env_file`` section enlisting ``./.envs/.postgres.local``. Generally, the stack's behavior is governed by a number of environment variables (`env(s)`, for short) residing in ``envs/``, for instance, this is what we generate for you: ::
.envs
├── .local
│   ├── .django
│   └── .postgres
└── .production
├── .caddy
├── .django
└── .postgres
├── .django.local
├── .postgres
└── .postgres.local
By convention, for any service ``sI`` in environment ``e`` (you know ``someenv`` is an environment when there is a ``someenv.yml`` file in the project root), given ``sI`` requires configuration, a ``.envs/.e/.sI`` `service configuration` file exists.
By convention, for any service ``sI`` in environment ``e`` (you know ``someenv`` is an environment when there is a ``someenv.yml`` file in the project root), given ``sI`` requires configuration, a ``.envs/.sI(.e)`` `service configuration` file exists.
Consider the aforementioned ``.envs/.local/.postgres``: ::
Consider the aforementioned ``.envs/.postgres.local``: ::
# PostgreSQL
# ------------------------------------------------------------------------------
@ -122,7 +115,7 @@ Consider the aforementioned ``.envs/.local/.postgres``: ::
The three envs we are presented with here are ``POSTGRES_DB``, ``POSTGRES_USER``, and ``POSTGRES_PASSWORD`` (by the way, their values have also been generated for you). You might have figured out already where these definitions will end up; it's all the same with ``django`` and ``caddy`` service container envs.
One final touch: should you ever need to merge ``.envs/production/*`` in a single ``.env`` run the ``merge_production_dotenvs_in_dotenv.py``: ::
One final touch: should you ever need to merge ``.envs/*`` (except ``.envs/*.local``) in a single ``.env`` run the ``merge_production_dotenvs_in_dotenv.py``: ::
$ python merge_production_dotenvs_in_dotenv.py
@ -184,6 +177,6 @@ Prerequisites:
* ``use_docker`` was set to ``y`` on project initialization;
* ``use_celery`` was set to ``y`` on project initialization.
By default, it's enabled both in local and production environments (``local.yml`` and ``production.yml`` Docker Compose configs, respectively) through a ``flower`` service. For added security, ``flower`` requires its clients to provide authentication credentials specified as the corresponding environments' ``.envs/.local/.django`` and ``.envs/.production/.django`` ``CELERY_FLOWER_USER`` and ``CELERY_FLOWER_PASSWORD`` environment variables. Check out ``localhost:5555`` and see for yourself.
By default, it's enabled both in local and production environments (``local.yml`` and ``production.yml`` Docker Compose configs, respectively) through a ``flower`` service. For added security, ``flower`` requires its clients to provide authentication credentials specified as the corresponding environments' ``.envs/.django.local`` and ``.envs/.production/.django`` ``CELERY_FLOWER_USER`` and ``CELERY_FLOWER_PASSWORD`` environment variables. Check out ``localhost:5555`` and see for yourself.
.. _`Flower`: https://github.com/mher/flower

View File

@ -91,7 +91,7 @@ use_travisci:
Indicates whether the project should be configured to use `Travis CI`_.
keep_local_envs_in_vcs:
Indicates whether the project's ``.envs/.local/`` should be kept in VCS
Indicates whether the project's ``.envs/*.local`` should be kept in VCS
(comes in handy when working in teams where local environment reproducibility
is strongly encouraged).

View File

@ -54,7 +54,7 @@ def remove_pycharm_files():
def remove_docker_files():
shutil.rmtree("compose")
shutil.rmtree("docker")
file_names = ["local.yml", "production.yml", ".dockerignore"]
for file_name in file_names:
@ -223,10 +223,10 @@ def set_flags_in_envs(
celery_flower_user,
debug=False,
):
local_django_envs_path = os.path.join(".envs", ".local", ".django")
production_django_envs_path = os.path.join(".envs", ".production", ".django")
local_postgres_envs_path = os.path.join(".envs", ".local", ".postgres")
production_postgres_envs_path = os.path.join(".envs", ".production", ".postgres")
local_django_envs_path = os.path.join(".envs", ".django.local")
production_django_envs_path = os.path.join(".envs", ".django")
local_postgres_envs_path = os.path.join(".envs", ".postgres.local")
production_postgres_envs_path = os.path.join(".envs", ".postgres")
set_django_secret_key(production_django_envs_path)
set_django_admin_url(production_django_envs_path)
@ -252,9 +252,14 @@ def remove_envs_and_associated_files():
os.remove("merge_production_dotenvs_in_dotenv.py")
def remove_celery_compose_dirs():
shutil.rmtree(os.path.join("compose", "local", "django", "celery"))
shutil.rmtree(os.path.join("compose", "production", "django", "celery"))
def remove_django_celery_docker_scripts():
file_names = [
"celerybeat",
"celeryworker",
"flower"
]
for file_name in file_names:
os.remove(os.path.join("docker", "django", "scripts", file_name))
def main():
@ -296,7 +301,7 @@ def main():
append_to_gitignore_file(".env")
append_to_gitignore_file(".envs/*")
if "{{ cookiecutter.keep_local_envs_in_vcs }}".lower() == "y":
append_to_gitignore_file("!.envs/.local/")
append_to_gitignore_file("!.envs/*.local")
if "{{ cookiecutter.js_task_runner}}".lower() == "none":
remove_gulp_files()
@ -320,7 +325,7 @@ def main():
if "{{ cookiecutter.use_celery }}".lower() == "n":
remove_celery_app()
if "{{ cookiecutter.use_docker }}".lower() == "y":
remove_celery_compose_dirs()
remove_django_celery_docker_scripts()
if "{{ cookiecutter.use_travisci }}".lower() == "n":
remove_dottravisyml_file()

View File

@ -1,44 +0,0 @@
FROM python:3.6-alpine
ENV PYTHONUNBUFFERED 1
RUN apk update \
# psycopg2 dependencies
&& apk add --virtual build-deps gcc python3-dev musl-dev \
&& apk add postgresql-dev \
# Pillow dependencies
&& apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev \
# CFFI dependencies
&& apk add libffi-dev py-cffi \
# Translations dependencies
&& apk add gettext \
# https://docs.djangoproject.com/en/dev/ref/django-admin/#dbshell
&& apk add postgresql-client
# Requirements are installed here to ensure they will be cached.
COPY ./requirements /requirements
RUN pip install -r /requirements/local.txt
COPY ./compose/production/django/entrypoint /entrypoint
RUN sed -i 's/\r//' /entrypoint
RUN chmod +x /entrypoint
COPY ./compose/local/django/start /start
RUN sed -i 's/\r//' /start
RUN chmod +x /start
{% if cookiecutter.use_celery == "y" %}
COPY ./compose/local/django/celery/worker/start /start-celeryworker
RUN sed -i 's/\r//' /start-celeryworker
RUN chmod +x /start-celeryworker
COPY ./compose/local/django/celery/beat/start /start-celerybeat
RUN sed -i 's/\r//' /start-celerybeat
RUN chmod +x /start-celerybeat
COPY ./compose/local/django/celery/flower/start /start-flower
RUN sed -i 's/\r//' /start-flower
RUN chmod +x /start-flower
{% endif %}
WORKDIR /app
ENTRYPOINT ["/entrypoint"]

View File

@ -1,8 +0,0 @@
#!/bin/sh
set -o errexit
set -o nounset
rm -f './celerybeat.pid'
celery -A {{cookiecutter.project_slug}}.taskapp beat -l INFO

View File

@ -1,10 +0,0 @@
#!/bin/sh
set -o errexit
set -o nounset
celery flower \
--app={{cookiecutter.project_slug}}.taskapp \
--broker="${CELERY_BROKER_URL}" \
--basic_auth="${CELERY_FLOWER_USER}:${CELERY_FLOWER_PASSWORD}"

View File

@ -1,7 +0,0 @@
#!/bin/sh
set -o errexit
set -o nounset
celery -A {{cookiecutter.project_slug}}.taskapp worker -l INFO

View File

@ -1,3 +0,0 @@
FROM abiosoft/caddy:0.11.0
COPY ./compose/production/caddy/Caddyfile /etc/Caddyfile

View File

@ -1,54 +0,0 @@
FROM python:3.6-alpine
ENV PYTHONUNBUFFERED 1
RUN apk update \
# psycopg2 dependencies
&& apk add --virtual build-deps gcc python3-dev musl-dev \
&& apk add postgresql-dev \
# Pillow dependencies
&& apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev \
# CFFI dependencies
&& apk add libffi-dev py-cffi
RUN addgroup -S django \
&& adduser -S -G django django
# Requirements are installed here to ensure they will be cached.
COPY ./requirements /requirements
RUN pip install --no-cache-dir -r /requirements/production.txt \
&& rm -rf /requirements
COPY ./compose/production/django/entrypoint /entrypoint
RUN sed -i 's/\r//' /entrypoint
RUN chmod +x /entrypoint
RUN chown django /entrypoint
COPY ./compose/production/django/start /start
RUN sed -i 's/\r//' /start
RUN chmod +x /start
RUN chown django /start
{% if cookiecutter.use_celery == "y" %}
COPY ./compose/production/django/celery/worker/start /start-celeryworker
RUN sed -i 's/\r//' /start-celeryworker
RUN chmod +x /start-celeryworker
RUN chown django /start-celeryworker
COPY ./compose/production/django/celery/beat/start /start-celerybeat
RUN sed -i 's/\r//' /start-celerybeat
RUN chmod +x /start-celerybeat
RUN chown django /start-celerybeat
COPY ./compose/production/django/celery/flower/start /start-flower
RUN sed -i 's/\r//' /start-flower
RUN chmod +x /start-flower
{% endif %}
COPY . /app
RUN chown -R django /app
USER django
WORKDIR /app
ENTRYPOINT ["/entrypoint"]

View File

@ -1,8 +0,0 @@
#!/bin/sh
set -o errexit
set -o pipefail
set -o nounset
celery -A {{cookiecutter.project_slug}}.taskapp beat -l INFO

View File

@ -1,10 +0,0 @@
#!/bin/sh
set -o errexit
set -o nounset
celery flower \
--app={{cookiecutter.project_slug}}.taskapp \
--broker="${CELERY_BROKER_URL}" \
--basic_auth="${CELERY_FLOWER_USER}:${CELERY_FLOWER_PASSWORD}"

View File

@ -1,8 +0,0 @@
#!/bin/sh
set -o errexit
set -o pipefail
set -o nounset
celery -A {{cookiecutter.project_slug}}.taskapp worker -l INFO

View File

@ -1,9 +0,0 @@
#!/bin/sh
set -o errexit
set -o pipefail
set -o nounset
python /app/manage.py collectstatic --noinput
/usr/local/bin/gunicorn config.wsgi --bind 0.0.0.0:5000 --chdir=/app

View File

@ -1,6 +0,0 @@
FROM postgres:{{ cookiecutter.postgresql_version }}
COPY ./compose/production/postgres/maintenance /usr/local/bin/maintenance
RUN chmod +x /usr/local/bin/maintenance/*
RUN mv /usr/local/bin/maintenance/* /usr/local/bin \
&& rmdir /usr/local/bin/maintenance

View File

@ -0,0 +1,3 @@
FROM abiosoft/caddy:0.11.0
COPY ./Caddyfile /etc/Caddyfile

View File

@ -0,0 +1,46 @@
FROM python:3.6-alpine
ENV PYTHONUNBUFFERED 1
ARG environment
RUN apk update \
# psycopg2 dependencies
&& apk add --virtual build-deps gcc python3-dev musl-dev \
&& apk add postgresql-dev \
# Pillow dependencies
&& apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev \
# CFFI dependencies
&& apk add libffi-dev py-cffi \
&& [ "${environment}" != 'production' ] && apk add \
# Translations dependencies
gettext \
# and https://docs.djangoproject.com/en/dev/ref/django-admin/#dbshell
postgresql-client
COPY ./requirements/ /app/requirements/
WORKDIR /app/
RUN [ "${environment}" = 'production' ] && pip --no-cache-dir install -r "./requirements/${environment}.txt" || pip install -r "./requirements/${environment}.txt"
RUN addgroup -S django \
&& adduser -S -g django django
COPY ./docker/django/scripts/ /scripts/
RUN [ "${environment}" = 'production' ] && find /scripts/ -type f -name '*.local' -exec rm -f {} \; || true
RUN chown --recursive django:django /scripts/
RUN find /scripts/ -type f -exec sed -i 's/\r//' {} \;
RUN chmod +x /scripts/*
RUN mv /scripts/* / \
&& rmdir /scripts/
COPY . /app/
RUN chown --recursive django:django /app/
USER django
ENTRYPOINT ["/entrypoint"]
ARG cmd
# See why we need ENV as well: https://stackoverflow.com/a/35562189
ENV cmd ${cmd}
CMD ${cmd}

View File

@ -0,0 +1,9 @@
#!/bin/sh
set -o errexit
set -o nounset
celery beat \
--app={{ cookiecutter.project_slug }}.taskapp \
--loglevel=INFO

View File

@ -0,0 +1,9 @@
#!/bin/sh
set -o errexit
set -o nounset
celery worker \
--app={{ cookiecutter.project_slug }}.taskapp \
--loglevel=INFO

View File

@ -0,0 +1,10 @@
#!/bin/sh
set -o errexit
set -o nounset
python manage.py migrate
# TODO: runs so long that web dyno times out after 60 secs on Heroku
#python manage.py collectstatic --noinput
/usr/local/bin/gunicorn config.wsgi --bind 0.0.0.0:5000 --chdir=/app

View File

@ -1,7 +1,6 @@
#!/bin/sh
set -o errexit
set -o pipefail
set -o nounset

View File

@ -0,0 +1,12 @@
#!/bin/sh
set -o errexit
set -o nounset
# TODO: ? merge with -local somehow
# N.B. If only .env files supported variable expansion...
export CELERY_BROKER_URL="${REDIS_URL}"
exec "$@"

View File

@ -1,7 +1,6 @@
#!/bin/sh
set -o errexit
set -o pipefail
set -o nounset
@ -17,9 +16,7 @@ export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES
postgres_ready() {
python << END
import sys
import psycopg2
try:
psycopg2.connect(
dbname="${POSTGRES_DB}",
@ -31,7 +28,6 @@ try:
except psycopg2.OperationalError:
sys.exit(-1)
sys.exit(0)
END
}
until postgres_ready; do

View File

@ -0,0 +1,10 @@
#!/bin/sh
set -o errexit
set -o nounset
celery flower \
--app={{ cookiecutter.project_slug }}.taskapp \
--broker="${CELERY_BROKER_URL}" \
--loglevel=INFO

View File

@ -0,0 +1 @@
FROM mailhog/mailhog:v1.0.0

View File

@ -0,0 +1,7 @@
FROM postgres:{{ cookiecutter.postgresql_version }}
COPY ./scripts/ /usr/local/bin/scripts/
RUN find /usr/local/bin/scripts/ -type f -exec sed -i 's/\r//' {} \;
RUN chmod +x /usr/local/bin/scripts/*
RUN mv /usr/local/bin/scripts/* /usr/local/bin/ \
&& rmdir /usr/local/bin/scripts/

View File

@ -0,0 +1 @@
FROM redis:3.2

View File

@ -3,12 +3,16 @@ version: '3'
volumes:
local_postgres_data: {}
local_postgres_data_backups: {}
local_redis_data: {}
services:
django:{% if cookiecutter.use_celery == 'y' %} &django{% endif %}
build:
context: .
dockerfile: ./compose/local/django/Dockerfile
dockerfile: ./docker/django/Dockerfile
args:
environment: local
cmd: /django.local
image: {{ cookiecutter.project_slug }}_local_django
depends_on:
- postgres
@ -18,35 +22,14 @@ services:
volumes:
- .:/app
env_file:
- ./.envs/.local/.django
- ./.envs/.local/.postgres
- ./.envs/.django.local
- ./.envs/.postgres.local
ports:
- "8000:8000"
command: /start
postgres:
build:
context: .
dockerfile: ./compose/production/postgres/Dockerfile
image: {{ cookiecutter.project_slug }}_production_postgres
volumes:
- local_postgres_data:/var/lib/postgresql/data
- local_postgres_data_backups:/backups
env_file:
- ./.envs/.local/.postgres
{%- if cookiecutter.use_mailhog == 'y' %}
mailhog:
image: mailhog/mailhog:v1.0.0
ports:
- "8025:8025"
{%- endif %}
entrypoint: /entrypoint.local
command: /django.local
{%- if cookiecutter.use_celery == 'y' %}
redis:
image: redis:3.2
celeryworker:
<<: *django
image: {{ cookiecutter.project_slug }}_local_celeryworker
@ -57,7 +40,7 @@ services:
- mailhog
{%- endif %}
ports: []
command: /start-celeryworker
command: /celeryworker
celerybeat:
<<: *django
@ -69,13 +52,34 @@ services:
- mailhog
{%- endif %}
ports: []
command: /start-celerybeat
command: /celerybeat
flower:
<<: *django
image: {{ cookiecutter.project_slug }}_local_flower
ports:
- "5555:5555"
command: /start-flower
command: /flower
redis:
build: ./docker/redis/
volumes:
- local_redis_data:/data
{%- endif %}
postgres:
build: ./docker/postgres/
image: {{ cookiecutter.project_slug }}_production_postgres
volumes:
- local_postgres_data:/var/lib/postgresql/data
- local_postgres_data_backups:/backups
env_file:
- ./.envs/.postgres.local
{%- if cookiecutter.use_mailhog == 'y' %}
mailhog:
build: ./docker/mailhog/
ports:
- "8025:8025"
{%- endif %}

View File

@ -3,67 +3,87 @@ version: '3'
volumes:
production_postgres_data: {}
production_postgres_data_backups: {}
production_redis_data: {}
production_caddy: {}
services:
django:{% if cookiecutter.use_celery == 'y' %} &django{% endif %}
build:
context: .
dockerfile: ./compose/production/django/Dockerfile
dockerfile: ./docker/django/Dockerfile
args:
environment: production
cmd: /django
image: {{ cookiecutter.project_slug }}_production_django
depends_on:
- postgres
- redis
env_file:
- ./.envs/.production/.django
- ./.envs/.production/.postgres
command: /start
- ./.envs/.django
- ./.envs/.postgres
command: /django
{%- if cookiecutter.use_celery == 'y' %}
postgres:
celeryworker:
<<: *django
build:
context: .
dockerfile: ./compose/production/postgres/Dockerfile
dockerfile: ./docker/django/Dockerfile
args:
environment: production
cmd: /celeryworker
image: {{ cookiecutter.project_slug }}_production_celeryworker
ports: []
command: /celeryworker
celerybeat:
<<: *django
build:
context: .
dockerfile: ./docker/django/Dockerfile
args:
environment: production
cmd: /celerybeat
image: {{ cookiecutter.project_slug }}_production_celerybeat
command: /celerybeat
flower:
<<: *django
build:
context: .
dockerfile: ./docker/django/Dockerfile
args:
environment: production
cmd: /flower
image: {{ cookiecutter.project_slug }}_production_flower
ports:
- "5555:5555"
command: /flower
{%- endif %}
postgres:
build: ./docker/postgres/
image: {{ cookiecutter.project_slug }}_production_postgres
volumes:
- production_postgres_data:/var/lib/postgresql/data
- production_postgres_data_backups:/backups
env_file:
- ./.envs/.production/.postgres
- ./.envs/.postgres
redis:
build: ./docker/redis/
volumes:
- production_redis_data:/data
caddy:
build:
context: .
dockerfile: ./compose/production/caddy/Dockerfile
build: ./docker/caddy/
image: {{ cookiecutter.project_slug }}_production_caddy
depends_on:
- django
volumes:
- production_caddy:/root/.caddy
env_file:
- ./.envs/.production/.caddy
- ./.envs/.caddy
ports:
- "0.0.0.0:80:80"
- "0.0.0.0:443:443"
redis:
image: redis:3.2
{%- if cookiecutter.use_celery == 'y' %}
celeryworker:
<<: *django
image: {{ cookiecutter.project_slug }}_production_celeryworker
command: /start-celeryworker
celerybeat:
<<: *django
image: {{ cookiecutter.project_slug }}_production_celerybeat
command: /start-celerybeat
flower:
<<: *django
image: {{ cookiecutter.project_slug }}_production_flower
ports:
- "5555:5555"
command: /start-flower
{%- endif %}