DjangoのDBをSQLite3からMySQL on Dockerに移行したメモ on Raspberry Pi 4B

## 今回の目的

ちょっとごちゃごちゃしてます。

1. Raspberry Pi 3B+で（直接）動いていた自分用DjangoプロジェクトをRaspberry Pi 4Bに移行
2. そのSQLite3 DBが肥大化してきた（vacuumして120MBくらい）のと並行性確保のためMySQLに移行
3. せっかくなのでDjangoプロジェクトをdocker-composeで動かすようにする
4. MySQLもdocker-composeで動かそう
5. DBを自動で更新する定期実行スクリプトもdocker-composeで動かそう


## 1. Raspberry Pi 4Bの初期セットアップ
まずはRaspberry Pi 4Bをセットアップする。

3B+で動いていたStretchは4Bでは動かないというような話を聞いたので、[Download Raspbian for Raspberry Pi](https://www.raspberrypi.org/downloads/raspbian/)から新しいRaspbian Busterを持ってきてMicro SDカードに焼く。メモリ節約（増えたけど）できそうなのでBusterはDesktopではなくLiteにした（どうせサーバ用途）。

~~mini HDMIケーブル/アダプタの持ち合わせがなかった（つらい、Microならあるのに...）ので、~~（20/03/07追記：間違い。Micro HDMIです。Miniはうすっぺらいやつ？）モニタレスセットアップする。100均で売ってるので必要になったら調達。
Micro SDカードを本体に挿し、LANケーブルでルータに接続。5V 3A Type-C電源（つらい、Microから買い替え）を接続して電源を入れる（Micro USBとType-Cの変換も100均にありそうなので電源を買い替えたくなければ... ちょっとこわいけど）。

ルータからDHCPで割り当てられたIPを調べて、LAN内でSSH接続（初期パスワード）。

`Too many authentication failures`と言われたり、`publickey`で弾かれるときは公開鍵で認証しようとして失敗してるので、`-o PreferredAuthentications=password`か`-o PubkeyAuthentication=no`をsshコマンドのオプションに加えるか、`~/.ssh/config`に`PreferredAuthentications password`か`PubkeyAuthentication no`を加える。

- [How to force ssh client to use only password auth? - Unix & Linux Stack Exchange](https://unix.stackexchange.com/questions/15138/how-to-force-ssh-client-to-use-only-password-auth)

### パスワード変更・ユーザ名変更
パスワード変更、それから同時にユーザ名を変更する。piユーザにログインしたままだとユーザ名を変更できないので、新しいsudoerなユーザを作成してログインし直す（今回はLAN内なので一時的にrootパスワードを設定してもいいかもだけど）。

piユーザで以下のコマンドを実行してtmpuserを作成。

```sh
# useraddの場合ホームディレクトリは作られない
sudo useradd tmpuser
sudo passwd tmpuser
```

次にtmpuserをsudoersに追加する。

```sh
sudo adduser tmpuser sudo
```

- [RaspberryPiをRaspbianで使う場合の初期設定メモ（ユーザ追加） - Qiita](https://qiita.com/R-STYLE/items/b481ba2d695ddf8bcee4)
- [Linuxでユーザーをグループに追加する - Qiita](https://qiita.com/orangain/items/056db6ffc16d765a8187)

ちょっと回り道をしたい場合、/etc/sudoersを編集してtmpuserを追加する。なんか/etc/sudoers他がreadonlyになってるので（chmodしてもいいけど）、/etc/sudoers.d/011_tmpuserを作る（sudoグループに追加でもいける）。

```sh
# /etc/sudoers.d/011_tmpuser
tmpuser ALL=(ALL:ALL) ALL
```

一度ログアウト、tmpuserユーザでログインし直して、以下のコマンドでpiユーザの名前を変更、piグループの名前も変更、最後にホームディレクトリを移動する。

```sh
sudo usermod -l NEW_NAME pi
sudo groupmod -n NEW_NAME pi
sudo usermod -m -d /home/NEW_NAME NEW_NAME
```

- [How to Change the Default Account Username and Password – The Pi Hut](https://thepihut.com/blogs/raspberry-pi-tutorials/how-to-change-the-default-account-username-and-password)
- [[Raspbian]ユーザ名変更の個人的に「正しい」と思うやり方 | 純規の暇人趣味ブログ](https://jyn.jp/raspberrypi-username-change/)
- [Raspbianでpiユーザ名とパスワードをうまいこと変更する - Qiita](https://qiita.com/kenchang/items/849c82f16ba5eafe070d)

tmpuserユーザからログアウトして、NEW_NAMEユーザで再ログイン、tmpuserユーザを削除する。回り道をした場合、/etc/sudoers.d/011_tmpuserも削除する。なお、デフォルトでpiユーザがsudoグループに属しているので、NEW_NAMEユーザをあらためてsudoersに加える必要はない（はず）。

```sh
sudo userdel tmpuser
# sudo rm /etc/sudoers.d/011_tmpuser
```

### ホスト名変更

```sh
# /etc/hostname
NEW_HOSTNAME

# /etc/hosts
...
127.0.1.1 NEW_HOSTNAME
```

- [How To Change Your Hostname on Debian - Vultr.com](https://www.vultr.com/docs/how-to-change-your-hostname-on-debian)

### 公開鍵認証
NEW_NAMEユーザに公開鍵を登録、/etc/ssh/sshd_configを編集してSSHの認証を公開鍵のみにする。

pi側

```sh
mkdir ~/.ssh
chmod 700 ~/.ssh
```

ホスト側

```sh
cd ~/.ssh
ssh-keygen -f KEY_NAME
scp KEY_NAME.pub RPI4_HOST:.ssh/
```

pi側

```sh
cd ~/.ssh
cat KEY_NAME.pub >> authorized_keys
chmod 600 authorized_keys
```

- [authorized_keysのパーミッション - アオミドロ草](http://bluingreen.hatenablog.com/entry/2017/03/22/221052)

あとは~/.ssh/configで`IdentityFile`を指定すればよい。やはり`Too many authentication failures`といわれるときは、`IdentitiesOnly yes`を追加する。

- [sshで「Too many authentication failures for ...」が出た場合の対処法 - tkuchikiの日記](https://tkuchiki.hatenablog.com/entry/2013/02/20/104609)
- [[ssh]Too many authentication failures for... のエラー - Qiita](https://qiita.com/mimaun/items/4599d3cff6a0343b6b3f)

必要に応じて`/etc/ssh/sshd_config`を編集して`PasswordAuthentication no`を設定するなどする。

## 2. Raspberry Pi 4BにDocker/docker-composeを導入

```sh
sudo curl -fsSL https://get.docker.com/ | sh
sudo apt install python3-pip
sudo apt install libffi-dev
sudo pip3 install docker-compose
```

- [Raspberry Pi 4でDockerをインストール - Qiita](https://qiita.com/shinmai88/items/71300107d1f0dd68312d)


## 3. DjangoプロジェクトのDocker/docker-compose移行
Djangoプロジェクトはgitで管理してたので、`git clone`で新サーバにプログラムを移行。DB（SQLite3）は`scp`で移行。

旧サーバではvirtualenvで環境管理してたので、ここからrequirements.txtを生成。

```sh
pip3 freeze > requirements.txt
```

せっかくなので新サーバの環境構築はDocker/docker-composeで行う。django:wsgi-gunicorn-nginxの構成だったが、まずは単体で動作テスト。

```docker
# Dockerfile
FROM python:3
ENV PYTHONUNBUFFERED 1
RUN mkdir /code
WORKDIR /code
COPY requirements.txt /code/
RUN pip install -r requirements.txt
COPY . /code/
```

```yaml
# docker-compose.yml
version: '3'

services:
    web:
        build: .
        command: python manage.py runserver 0.0.0.0:8000
        volumes:
            - .:/code
        ports:
            - "127.0.0.1:8000:8000"
        environment:
            - ENVIRONMENT=production
```

```sh
sudo docker-compose up
```

- [Quickstart: Compose and Django | Docker Documentation](https://docs.docker.com/compose/django/)

## 4. MySQLをDocker上で動かす（docker-compose/Raspberry Pi 4）
Raspberry Pi上のdocker-composeでMySQL（MariaDB）を動かす。`jsurf/rpi-mariadb`を使う。

```yaml
...
    db:
        # image: mariadb
        image: jsurf/rpi-mariadb
        command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
        volumes:
            - DATABASE_DIRECTORY:/var/lib/mysql
        environment:
            - MYSQL_ROOT_PASSWORD=ROOT_PASSWORD
            - MYSQL_DATABASE=DATABASE_NAME
            - MYSQL_USER=USER
            - MYSQL_PASSWORD=PASSWORD
    web:
...
```

- [jsurf/rpi-mariadb - Docker Hub](https://hub.docker.com/r/jsurf/rpi-mariadb/)

## 5. DjangoのDB設定をSQLite3からMySQLへ変更

あとで戻すので、簡単に戻せるようにしつつ、Djangoの`settings.py`を適当にいじって環境変数からDBを指定するようにする。

```python
DATABASE_ENGINE = os.environ.get('DATABASE_ENGINE', 'django.db.backends.sqlite3')
DATABASE_OPTIONS = {}
if DATABASE_ENGINE == 'django.db.backends.mysql':
    DATABASE_OPTIONS = {
        'charset': os.environ.get('DATABASE_CHARSET'),
    }

DATABASES = {
    'default': {
        'ENGINE': DATABASE_ENGINE,
        'HOST': os.environ.get('DATABASE_HOST'),
        'PORT': os.environ.get('DATABASE_PORT'),
        'NAME': os.environ.get('DATABASE_NAME', os.path.join(BASE_DIR, 'db.sqlite3')),
        'USER': os.environ.get('DATABASE_USER'),
        'PASSWORD': os.environ.get('DATABASE_PASSWORD'),
        'OPTIONS': DATABASE_OPTIONS,
    },
}
```

`docker-compose.yml`は以下のように編集。environmentのDATABASE部分をすべてコメントアウトするか、`docker-compose.yml`を他に作ることでDBをSQLite3に戻せるようにしておく。

```yaml
    web:
...
        environment:
            - ENVIRONMENT=production
            - DATABASE_ENGINE=django.db.backends.mysql
            - DATABASE_HOST=db
            - DATABASE_PORT=3306
            - DATABASE_NAME=DATABASE_NAME
            - DATABASE_USER=USER
            - DATABASE_PASSWORD=PASSWORD
            - DATABASE_CHARSET=utf8mb4
        depends_on:
            - db
```

`requirements.txt`に`PyMySQL`を追記して、`manage.py`の上部に以下を追記する。

```python
if os.environ.get('DATABASE_ENGINE') == 'django.db.backends.mysql':
    import pymysql
    pymysql.install_as_MySQLdb()
```

- [Django SQLite3からMySQLへの移行 - Qiita](https://qiita.com/ta2mi/items/0547de41d021d67ac3dc)
- [いつもDjangoでMySQL(utf8mb4)を利用するときに行っているDjangoのDATABASE設定 - Qiita](https://qiita.com/shirakiya/items/71861325b2c8988979a2)

なお、MySQL側の初期化が終わる前にDjango側がアクセスするとDjangoがエラー落ちするので、初回実行時は`docker-compose up`を再実行する。2回目移行もDjangoが先に起動してしまう場合は、適当なsleepをはさむか待機スクリプトを作るなどして対処する。9番のセクションでgunicornをはさんだあとだと、Django（gunicorn）が先に起動してもエラーは出なくなった（みたいな）ので、あんまり気にしなくてもいいかもしれない。

```yaml
        command: bash -c "sleep 5 && python manage.py runserver 0.0.0.0:8000"
```

- [Docker: Wait until MySQL is available](https://cweiske.de/tagebuch/docker-mysql-available.htm)
- [yaml - Using Docker-Compose, how to execute multiple commands - Stack Overflow](https://stackoverflow.com/questions/30063907/using-docker-compose-how-to-execute-multiple-commands)

## 6. Migration Errorの解消

DBモデルの定義によっては、`sudo docker-compose up -d`して`sudo docker-compose exec web python3 manage.py migrate`するとエラーが出る。例えばunique制約付きのTextFieldがあって、`max_length`を指定していない場合。今回はURLをURLFieldではなくTextFieldに入れていたのをURLFieldに直し、また短い文字列とわかっているTextFieldに`max_length`（255以下）を指定して解消した（ただし、Raspberry Pi上ではこれだけでは動かなかったので、あとで結局unique制約を外した）。

このあたりは高速化のためメイン機にプロジェクトとDBを移して実験しつつ行った。今回はMySQLのimageを変える必要があるが、（だいたい）同じ環境を自動で整えてくれて、しかもホストの環境を汚さない/影響を受けないのがDockerのいいところ（公式imageがないと互換性に悩まされるのが...？）。

- [つまみがなければ鼻でもつまむ:djangoでmysqlをバックエンドにするとTextFieldにunique制約がつけられない - livedoor Blog（ブログ）](http://blog.livedoor.jp/kaz0215/archives/51119127.html)
- [MySQLでBLOB/TEXT型カラムにインデックスを作成してみる - Qiita](https://qiita.com/mizuki_takahashi/items/52bc426307817123305e)

## 7. DjangoのDBデータをjsonに書き出す

Migration Errorを解消したらDBをSQLite3に戻し、migrationしたあとでデータをjsonにダンプする。

```sh
sudo docker-compose run web bash
```

```sh
python3 manage.py makemigrations
python3 manage.py migrate
# python3 manage.py dumpdata > dump.json
python3 manage.py dumpdata --natural-foreign --natural-primary -e contenttypes -e auth.Permission > dump.json
```

- [Django SQLite3からMySQLへの移行 - Qiita](https://qiita.com/ta2mi/items/0547de41d021d67ac3dc)
- [SQLite3のデータをdumpしてMySQLに移行する - Qiita](https://qiita.com/nnsnodnb/items/9e99e7f0ca3f82bf2171)
- [mysql - Problems with contenttypes when loading a fixture in Django - Stack Overflow](https://stackoverflow.com/a/43805265)

## 8. DjangoのDBデータをjsonから書き戻す

```sh
python3 manage.py migrate
python3 manage.py loaddata dump.json
```

```sh
django.db.utils.IntegrityError: Problem installing fixture '/code/dump.json': Could not load APP.MODELNAME(pk=PK_NUM): (1062, "Duplicate entry 'ONE_FIELD_NUM' for key 'ONE_FIELD'")
```

OneToOneFieldにunique_together制約をかけていたのがよくなかった（OneToOneは多対1に使えない）みたいなので、ForeignKeyに変えた。また、この時点で`mariadb`では動いたものの、`jsurf/rpi-mariadb`ではutf8mb4にしているせいかKey lengthまわりのエラーが消えずに動かなかったので、すべての文字列のunique制約を外してしまうことにしたほか、こちらではmigrateが途中で止まってしまうのでmigrations以下のファイルを直接書き変えなければならなかった。別のPCで処理したDBを直接送りつけても動かなかったので、互換性にも不安が残るが...。何度も試行錯誤してようやくloaddataすることができた。

- [python - Duplicate key error on non-primary key field - Stack Overflow](https://stackoverflow.com/questions/42029195/duplicate-key-error-on-non-primary-key-field)

## 9. ホストのWebサーバ（リバースプロキシ）とDjangoの間にgunicornをはさむ

`requirements.txt`に`gunicorn`を追加する。

`docker-compose.yml`を編集する。メモリを食う（と思う）のでワーカー数`-w`は必要に応じて調整する。

```yaml
        # command: /usr/local/bin/gunicorn -w 4 -b 0.0.0.0:8000 MY_PROJECT.wsgi -t 300
        command: bash -c "sleep 5 && gunicorn -w 4 -b 0.0.0.0:8000 MY_PROJECT.wsgi -t 300"
```

`manage.py`と同様に、`wsgi.py`の上部に以下を追加する。

```python
if os.environ.get('DATABASE_ENGINE') == 'django.db.backends.mysql':
    import pymysql
    pymysql.install_as_MySQLdb()
```

- [gunicornでPython製Webアプリケーションを動作させよう（DjangoとFlask） - Make組ブログ](https://blog.hirokiky.org/entry/2018/10/06/151830)
- [nginx + gunicorn + Django タイムアウト処理 - Qiita](https://qiita.com/ume1126/items/4f062c90283a958a95bc)
- [CentOS7+Nginx+GunicornでDjangoを起動 - Narito Blog](https://narito.ninja/blog/detail/21/)
- [EC2にNginx + Gunicorn + SupervisorでDjangoアプリケーションをデプロイする - Qiita](https://qiita.com/xKxAxKx/items/da9dcc8caa36e1c303c7)
- [shell - What's the difference between semicolon and double ampersand && - Unix & Linux Stack Exchange](https://unix.stackexchange.com/questions/187145/whats-the-difference-between-semicolon-and-double-ampersand)

## 10. 定期実行スクリプトをDocker上で動かす

（20/02/15 追記）

- [Python Scheduleライブラリでジョブ実行 - Qiita](https://qiita.com/Kai-Suzuki/items/0c5c0e5cbdb4075fe482)

以下でbusybox crondを使った設定をしたが、ログ周りが不便なのと結局本番用のスクリプトはうまく動いてくれていなかったみたいなので、Pythonで定期実行スクリプトを書いて、Djangoコンテナと同じ構成のコンテナをもう一個作って実行することにした。しかし冗長なので、Djangoコンテナ側に定期実行用のエンドポイントを作ってHTTPリクエストを飛ばすだけのコンテナにした方がいいかもしれない..。

--- 
（旧版）

今回はDjangoと同じコンテナ内で定期実行を走らせる。

定期実行スクリプトはこれまでだいたいpythonでループを書いちゃうか、systemdのtimerで動かしていた。今回のものはsystemd/timerだったので、これをDockerコンテナ内に移行しようとしたが、ホストからDockerコンテナ内のスクリプトを動かすのは`exec`でできるものの、Dockerコンテナ内でsystemd/timerを動かすのはよくわからない。

- [laravel 5 - How do I get schedule:run to work with systemd in a docker container? - Stack Overflow](https://stackoverflow.com/questions/49331943)
- [Running scheduled tasks in Docker containers using systemd timer-units](https://matthiasadler.info/blog/running-scheduled-tasks-in-docker-containers-with-systemd/)
- [systemdからdockerコンテナを起動＋timerで定期実行 – 進捗ダメな人のブログ](https://hogehuga.net/linux/304/)
- [systemd in docker container without --privileged - Qiita](https://qiita.com/shusugmt/items/92ece6874ba5aeff2b41)

ともあれ、`python:3`のベースOSはDebianでsystemdがなさそう（init.d）なので、cronで定期実行することにする。

- [Docker上でpythonのプログラムをcronで定期実行する - Qiita](https://qiita.com/sey323/items/407cfa303f9ec217c677)
- [crontabのガイドライン - Qiita](https://qiita.com/onomame/items/71646c5517a39bcd01cc)
- [cron実行時のカレントディレクトリは、実行ユーザのホームディレクトリ - Qiita](https://qiita.com/doranekohc/items/c9bca52eed96916be9b6)
- [Docker + Cron環境を実現する３つの方法 - Qiita](https://qiita.com/YuukiMiyoshi/items/bb7f14436d60d4bd8a8b)
- [稼働済みdockerコンテナでcronを動かす - Qiita](https://qiita.com/nousk/items/fe316cac5035104e101f)
- [Cronの使い方とテクニックと詰まったところ - Qiita](https://qiita.com/UNILORN/items/a1a3f62409cdb4256219)
- [Dockerコンテナをホスト側のcronで実行する - 202号室の手記](http://paperlefthand.hateblo.jp/entry/2017/06/14/214834)
- [Docker コンテナ内でタスクを cron 起動する - Qiita](https://qiita.com/rerofumi/items/fc0126c4e985b78f769b)
- [Docker で /etc/cron.d を使って cron を実行する - Qiita](https://qiita.com/nishimuraatsushi/items/5202c5644ffeca7d3c7c)
- [Dockerでcronを回す時ハマったこと - Qiita](https://qiita.com/jerrywdlee/items/d4468f076bdea236bf3b)
- [crontabして何も見つからないのに設定ファイルはあるとき - helen's blog](https://helen.hatenablog.com/entry/2016/07/12/124738)
- [【違い】/etc/crontabと/var/spool/cron/[user] - Qiita](https://qiita.com/takahashi-kazuki/items/77d669d4414c6ec185f9)
- [/etc/crontabと/etc/cron.d設定ファイルの書き方 | server-memo.net](https://www.server-memo.net/tips/etc-crontab.html)
- [crondの使い方 - Qiita](https://qiita.com/kentakozuka/items/81cbcdc80e5d0bb3a530)
- [crontab -e は「絶対に」使ってはいけない - ろば電子が詰まつてゐる](https://ozuma.hatenablog.jp/entry/20120711/1342014448)
- [docker - コマンドはdocker-compose.ymlとDockerfileのどちらで定義するほうがいい？ - スタック・オーバーフロー](https://ja.stackoverflow.com/questions/30495)
- [debian - Crontab never running while in /etc/cron.d - Unix & Linux Stack Exchange](https://unix.stackexchange.com/questions/296347/crontab-never-running-while-in-etc-cron-d)
- [linux - docker cron not working - Server Fault](https://serverfault.com/a/924780)
- [linux - Why is my crontab not working, and how can I troubleshoot it? - Server Fault](https://serverfault.com/a/449652)

cronを使うのは初めてなのもあって、そうとう迷走した。

- [Dockerコンテナ上でCronを動かしたい - Qiita](https://qiita.com/nanamen/items/5c4ef45e2ee84bafdd1d)
- [DockerでcronしたいときはBusyBox crondが便利 - shimoju.diary](https://shimoju.org/2017/10/09/busybox-crond/)
- [debian ベースの Docker コンテナで busybox の cron を実行 - ngyukiの日記](https://ngyuki.hatenablog.com/entry/2017/04/18/195254)

Dockerで指定した環境変数を引き継ぎたいので、busyboxに含まれるcrondを使う。

まず、実行ディレクトリに以下のようなファイル`crontab`を作成する。

```sh
# * * * * * cd /code && echo `env` >> env.txt
0 */6 * * * cd /code && /usr/local/bin/python3 AUTORUN_SCRIPT.py
```

上は1分ごとに環境変数をファイルに書き出す設定（デバッグ用）で、下は6時間ごとに/code/AUTORUN_SCRIPT.pyをrootユーザ、ワーキングディレクトリ/codeで自動実行する設定。時刻はJSTでOK。

- [crontabのガイドライン - Qiita](https://qiita.com/onomame/items/71646c5517a39bcd01cc)

次に、Dockerfileにcrondのインストールと設定ファイルの追加を定義する。/var/spool/cron/crontabs/rootはディレクトリではなくファイルになるのが正解。

```dockerfile
# Dockerfile
...
RUN apt update && apt install -y \
  busybox-static
ENV TZ Asia/Tokyo
COPY crontab /var/spool/cron/crontabs/root
...
```

それから、Dockerコンテナ起動時にcrondが起動するようにする。今回はdocker-composeを使っているので、`Dockerfile`内の`CMD`は実行されないのに注意。代わりに、`docker-compose.yml`内の`command`にcrondの起動コマンドを追加する。`crond`はバックグラウンド実行されるので、続けて自動的に`gunicorn`が起動する。

```yaml
# docker-compose.yml
...
        # command: bash -c "busybox crond && gunicorn -w 4 -b 0.0.0.0:8000 MY_PROJECT.wsgi -t 300"
        command: bash -c "sleep 5 && busybox crond && gunicorn -w 4 -b 0.0.0.0:8000 MY_PROJECT.wsgi -t 300"
...
```

DjangoのDBをいじるので`AUTORUN_SCRIPT.py`中にpymysqlのinstallを追加する（`manage.py`、`wsgi.py`と同じ）などして、実験用のスクリプトが動作することが確認できた。デバッグ用のcron設定を消して設定完了。

## 11. 起動時自動実行とバックグラウンド実行
`docker-compose.yml`に`restart: always`を追加してホスト起動時に自動的に開始するようにして、`sudo docker-compose up -d`でバックグラウンドで起動。あとはホストをrebootしても大丈夫（`sudo docker-compose ps`、`sudo docker ps`）。

- [Docker Compose restart の挙動 - 技術備忘記](http://junchang1031.hatenablog.com/entry/2016/05/18/000605)
- [docker-composeでOS起動時にコンテナを自動起動する方法 - Qiita](https://qiita.com/eijenson/items/25b35916afa38cdf9cea)

## 結果

無事にハードウェア（Raspberry Pi）の移行、DBの移行、DBエンジンの移行、Dockerへの移行＆永続化に成功した。

ハードウェアのスペック向上、MySQLへの移行によってパフォーマンスが改善した（ように思われる）ほか、DB操作を並行して行うことができるようになった（みたいな）ので、同時にDBにアクセスしたときに発生していた`Database is locked`エラーが見られなくなった。

個人用プロジェクトなのでログ設定端折ったりだとかunique切ったりだとかしてるけど...。ここはそうとう時間かけたのであきらめた。ただ、loaddataしたあとでならmigrationでunique戻したりできるかもしれない。

あとはcronを別のコンテナに分けた方がいいのかな、と思いつつ、Djangoプロジェクトとまったく同じ依存関係なので分けずにまとめてしまった。これどう分けるんだ...？