さくらVPSのCentOS7でDjango+uwsgi+nginx+sqlite3を動かす

# はじめに
さくらVPSの1Gプランに各ソフトウェアのバージョンをインストールして環境を構築しています。


|名前|バージョン|
|:-:|:-:|
|CentOS|7.8.2003 (Core)|
|python|3.6.8|
|django|3.1|
|uwsgi|2.0.19.1|
|nginx|1.16.1|
|sqlite|3.30.0|

# さくらVPSをセットアップ
前提条件として標準OSのCentOS7 x86_64をインストールしてスタートアップガイドのサーバの初期設定ガイドまでが完了しているものとします。
なお、サーバの初期設定ガイド6「パケットフィルターを設定しましょう」でWebを有効にしてください。

# Djangoをインストール
[Djangoのサイト](https://www.djangoproject.com/)でインストール方法を確認します。
Djangoを使用するにはPythonが必要なので、まずはインストールされているPythonのバージョンを確認します。

```
$ python --version
Python 2.7.5
```

FAQの「どのバージョンの Python で Django が使えますか?」によるとPython 2.7.5で使えるDjangoは1.11となっていますが、1.11は2020年4月1日でサポートが終了となっているのでPython 3を使うことにします。

```
$ python3 --version
-bash: python3: command not found
```

Python 3がインストールされていないのでインストールします。

```
$ sudo yum -y install python3
```

インストールされたPython 3のバージョンを確認します。

```
$ python3 --version
Python 3.6.8
```
Python 3.6.8なので最新のDjango 3.1まで使用することが出来ます。
Pythonの準備が出来たので次にDjangoをインストールします。

```
$ sudo pip3 install django
```

インストールしたDjangoのバージョンを確認します。

```
$ django-admin --version
3.1
```

とりあえずプロジェクトを作って動作するか確認してみます。

```
$ django-admin startproject mysite
$ cd mysite
$ python3 manger.py runserver
```

何やらエラーが発生してしまいました。

```
Watching for file changes with StatReloader
Exception in thread django-main-thread:
Traceback (most recent call last):
  File "/usr/lib64/python3.6/threading.py", line 916, in _bootstrap_inner
    self.run()
  File "/usr/lib64/python3.6/threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File "/usr/local/lib64/python3.6/site-packages/django/utils/autoreload.py", line 53, in wrapper
    fn(*args, **kwargs)
  File "/usr/local/lib64/python3.6/site-packages/django/core/management/commands/runserver.py", line 110, in inner_run
    autoreload.raise_last_exception()
  File "/usr/local/lib64/python3.6/site-packages/django/utils/autoreload.py", line 76, in raise_last_exception
    raise _exception[1]
  File "/usr/local/lib64/python3.6/site-packages/django/core/management/__init__.py", line 357, in execute
    autoreload.check_errors(django.setup)()
  File "/usr/local/lib64/python3.6/site-packages/django/utils/autoreload.py", line 53, in wrapper
    fn(*args, **kwargs)
  File "/usr/local/lib64/python3.6/site-packages/django/__init__.py", line 24, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "/usr/local/lib64/python3.6/site-packages/django/apps/registry.py", line 114, in populate
    app_config.import_models()
  File "/usr/local/lib64/python3.6/site-packages/django/apps/config.py", line 211, in import_models
    self.models_module = import_module(models_module_name)
  File "/usr/lib64/python3.6/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 994, in _gcd_import
  File "<frozen importlib._bootstrap>", line 971, in _find_and_load
  File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 678, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/usr/local/lib64/python3.6/site-packages/django/contrib/auth/models.py", line 2, in <module>
    from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
  File "/usr/local/lib64/python3.6/site-packages/django/contrib/auth/base_user.py", line 48, in <module>
    class AbstractBaseUser(models.Model):
  File "/usr/local/lib64/python3.6/site-packages/django/db/models/base.py", line 122, in __new__
    new_class.add_to_class('_meta', Options(meta, app_label))
  File "/usr/local/lib64/python3.6/site-packages/django/db/models/base.py", line 326, in add_to_class
    value.contribute_to_class(cls, name)
  File "/usr/local/lib64/python3.6/site-packages/django/db/models/options.py", line 206, in contribute_to_class
    self.db_table = truncate_name(self.db_table, connection.ops.max_name_length())
  File "/usr/local/lib64/python3.6/site-packages/django/db/__init__.py", line 28, in __getattr__
    return getattr(connections[DEFAULT_DB_ALIAS], item)
  File "/usr/local/lib64/python3.6/site-packages/django/db/utils.py", line 214, in __getitem__
    backend = load_backend(db['ENGINE'])
  File "/usr/local/lib64/python3.6/site-packages/django/db/utils.py", line 111, in load_backend
    return import_module('%s.base' % backend_name)
  File "/usr/lib64/python3.6/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "/usr/local/lib64/python3.6/site-packages/django/db/backends/sqlite3/base.py", line 70, in <module>
    check_sqlite_version()
  File "/usr/local/lib64/python3.6/site-packages/django/db/backends/sqlite3/base.py", line 67, in check_sqlite_version
    raise ImproperlyConfigured('SQLite 3.8.3 or later is required (found %s).' % Database.sqlite_version)
django.core.exceptions.ImproperlyConfigured: SQLite 3.8.3 or later is required (found 3.7.17).
```

エラーメッセージにはバージョン3.8.3以降のSQLiteを要求しているのに見つかったのは3.7.17だと出ています。
実際にSQLiteのバージョンを確認してみます。

```
$ sqlite3 -version
3.7.17 2013-05-20 00:56:22 118a3b35693b134d56ebd780123b7fd6f1497668
```

確かにバージョンが3.7.17で古いのでバージョンアップする必要があります。

## SQLiteをバージョンアップ

[SQLiteのサイト](https://www.sqlite.org/index.html)よりソースコードをダウンロードしてインストールします。

```
$ cd ~/
$ wget https://www.sqlite.org/2020/sqlite-autoconf-3330000.tar.gz
$ tar zxvf sqlite-autoconf-3330000.tar.gz
$ cd sqlite-autoconf-3330000
$ ./configure
$ make
$ sudo make install
```

インストールしたSQLiteのバージョンを確認してみます。

```
$ /usr/local/bin/sqlite3 -version
3.33.0 2020-08-14 13:23:32 fca8dc8b578f215a969cd899336378966156154710873e68b3d9ac5881b0ff3f
```

SQLiteのバージョンアップが出来たのでLD_LIBRARY_PATHでライブラリのパスを指定してもう一度実行してみます。

```
$ cd ~/mysite
$ LD_LIBRARY_PATH=/usr/local/lib python3 manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
August 20, 2020 - 18:26:21
Django version 3.1, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
```

今度はちゃんと実行できたようです。
サーバーにアクセス出来るか確認してみます。

```
$ curl -i http://127.0.0.1:8000
HTTP/1.1 200 OK
Date: Thu, 20 Aug 2020 18:35:41 GMT
Server: WSGIServer/0.2 CPython/3.6.8
Content-Type: text/html
X-Frame-Options: DENY
Content-Length: 16351
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin


<!doctype html>

<html>
    <head>
...
```

LD_LIBRARY_PATHを毎回指定するのは手間なのでldconfigで共有ライブりのパスを登録します。
まず現在の登録状態を確認します。

```
$ ldconfig -p | grep sqlite
        libsqlite3.so.0 (libc6,x86-64) => /lib64/libsqlite3.so.0
```

/etc/ld.so.conf.d以下に設定ファイルを追加します。

```/etc/ld.so.conf.d/sqlite3.33.0.conf
/usr/local/lib
```

登録内容を更新します。

```
$ sudo ldconfig
```

もう一度登録状態を確認します。

```
$ ldconfig -p | grep sqlite
        libsqlite3.so.0 (libc6,x86-64) => /usr/local/lib/libsqlite3.so.0
        libsqlite3.so.0 (libc6,x86-64) => /lib64/libsqlite3.so.0
        libsqlite3.so (libc6,x86-64) => /usr/local/lib/libsqlite3.so
```

これでLD_LIBRARY_PATHを指定しなくてもよくなりました。

# uwsgiをインストール
Web Server Gateway Interfaceのuwsgiをインストールします。

```
$ sudo pip3 install uwsgi
```

何やらエラーが発生しました。

```
    In file included from plugins/python/python_plugin.c:1:0:
    plugins/python/uwsgi_python.h:2:20: fatal error: Python.h: No such file or directory
     #include <Python.h>
                        ^
    compilation terminated.
    In file included from plugins/python/pyutils.c:1:0:
    plugins/python/uwsgi_python.h:2:20: fatal error: Python.h: No such file or directory
     #include <Python.h>
                        ^
    compilation terminated.

    ----------------------------------------
Command "/usr/bin/python3 -u -c "import setuptools, tokenize;__file__='/tmp/pip-build-evpbzqre/uwsgi/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" install --record /tmp/pip-9qkvdn3u-record/install-record.txt --single-version-externally-managed --compile" failed with error code 1 in /tmp/pip-build-evpbzqre/uwsgi/
```

原因はpython3-develがインストールされていないことなのでインストールします。

```
$ sudo yum -y install python3-devel
```

もう一度uwsgiをインストールします。

```
$ sudo pip3 install uwsgi
```

問題なくインストール出来たのでuwsgiのバージョンを確認します。

```
$ uwsgi --version
2.0.19.1
```

動作確認をするためにHello Worldを返すだけのテストスクリプトを用意します。

```python:test.py
def application(env, start_response):
    start_response('200 OK', [('Content-Type','text/html')])
    return [b"Hello World"]
```

テストスクリプトを実行します。

```
$ uwsgi --http :8000 --wsgi-file test.py
```

実行できているか確認するためローカルホストにポート番号8000でアクセスします。

```
$ curl -i localhost:8000
HTTP/1.1 200 OK
Content-Type: text/html

Hello World
```

問題なく動作したので今度はDjangoを動かしてみます。

```
$ cd ~/mysite
$ uwsgi --http :8000 --module mysite.wsgi
```

何やらエラーが発生しました。

```
*** Starting uWSGI 2.0.19.1 (64bit) on [Sun Aug 23 00:36:53 2020] ***
compiled with version: 4.8.5 20150623 (Red Hat 4.8.5-39) on 22 August 2020 13:27:06
os: Linux-3.10.0-1127.18.2.el7.x86_64 #1 SMP Sun Jul 26 15:27:06 UTC 2020
nodename: サーバー名
machine: x86_64
clock source: unix
detected number of CPU cores: 2
current working directory: /home/ユーザー名/mysite
detected binary path: /usr/local/bin/uwsgi
!!! no internal routing support, rebuild with pcre support !!!
*** WARNING: you are running uWSGI without its master process manager ***
your processes number limit is 3881
your memory page size is 4096 bytes
detected max file descriptor number: 1024
lock engine: pthread robust mutexes
thunder lock: disabled (you can enable it with --thunder-lock)
uWSGI http bound on :8000 fd 4
spawned uWSGI http 1 (pid: 11759)
uwsgi socket 0 bound to TCP address 127.0.0.1:40228 (port auto-assigned) fd 3
Python version: 3.6.8 (default, Apr  2 2020, 13:34:55)  [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]
*** Python threads support is disabled. You can enable it with --enable-threads ***
Python main interpreter initialized at 0x165d4f0
your server socket listen backlog is limited to 100 connections
your mercy for graceful operations on workers is 60 seconds
mapped 72904 bytes (71 KB) for 1 cores
*** Operational MODE: single process ***
Traceback (most recent call last):
  File "./mysite/wsgi.py", line 16, in <module>
    application = get_wsgi_application()
  File "/usr/local/lib64/python3.6/site-packages/django/core/wsgi.py", line 12, in get_wsgi_application
    django.setup(set_prefix=False)
  File "/usr/local/lib64/python3.6/site-packages/django/__init__.py", line 24, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "/usr/local/lib64/python3.6/site-packages/django/apps/registry.py", line 114, in populate
    app_config.import_models()
  File "/usr/local/lib64/python3.6/site-packages/django/apps/config.py", line 211, in import_models
    self.models_module = import_module(models_module_name)
  File "/usr/lib64/python3.6/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "/usr/local/lib64/python3.6/site-packages/django/contrib/auth/models.py", line 2, in <module>
    from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
  File "/usr/local/lib64/python3.6/site-packages/django/contrib/auth/base_user.py", line 48, in <module>
    class AbstractBaseUser(models.Model):
  File "/usr/local/lib64/python3.6/site-packages/django/db/models/base.py", line 122, in __new__
    new_class.add_to_class('_meta', Options(meta, app_label))
  File "/usr/local/lib64/python3.6/site-packages/django/db/models/base.py", line 326, in add_to_class
    value.contribute_to_class(cls, name)
  File "/usr/local/lib64/python3.6/site-packages/django/db/models/options.py", line 206, in contribute_to_class
    self.db_table = truncate_name(self.db_table, connection.ops.max_name_length())
  File "/usr/local/lib64/python3.6/site-packages/django/db/__init__.py", line 28, in __getattr__
    return getattr(connections[DEFAULT_DB_ALIAS], item)
  File "/usr/local/lib64/python3.6/site-packages/django/db/utils.py", line 214, in __getitem__
    backend = load_backend(db['ENGINE'])
  File "/usr/local/lib64/python3.6/site-packages/django/db/utils.py", line 111, in load_backend
    return import_module('%s.base' % backend_name)
  File "/usr/lib64/python3.6/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "/usr/local/lib64/python3.6/site-packages/django/db/backends/sqlite3/base.py", line 70, in <module>
    check_sqlite_version()
  File "/usr/local/lib64/python3.6/site-packages/django/db/backends/sqlite3/base.py", line 67, in check_sqlite_version
    raise ImproperlyConfigured('SQLite 3.8.3 or later is required (found %s).' % Database.sqlite_version)
django.core.exceptions.ImproperlyConfigured: SQLite 3.8.3 or later is required (found 3.7.17).
unable to load app 0 (mountpoint='') (callable not found or import error)
*** no app loaded. going in full dynamic mode ***
*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI worker 1 (and the only) (pid: 11758, cores: 1)
```

エラーメッセージはバージョン3.8.3以降のSQLiteを要求しているのに見つかったのは3.7.17だと出ています。
なぜか先ほどバージョンアップしたはずなのに同じメッセージが出ています。
原因はuwsgiにRPATHが設定されているからです。

```
$ objdump -p /usr/local/bin/uwsgi | grep RPATH
  RPATH                /usr/lib64
```

RPATHが設定されていると指定されたディレクトリからしか共有ライブラリをロードしなくなります。
RPATHに/usr/local/libを追加してuwsgiを作り直します。

```
$ sudo pip3 uninstall uwsgi
$ sudo LDFLAGS="-L/usr/local/lib -Wl,-rpath,/usr/local/lib" pip3 install uwsgi
```

RPATHがどうなったか確認しましす。

```
$ objdump -p /usr/local/bin/uwsgi | grep RPATH
  RPATH                /usr/local/lib:/usr/lib64
```

もう一度実行してみます。

```
$ cd ~/mysite
$ uwsgi --http :8000 --module mysite.wsgi
```

問題なく実行することが出来ました。

# nginxをインストール
Webサーバーのnginxをインストールします。

```
$ sudo yum -y install nginx
```

インストールしたnginxのバージョンを確認します。

```
$ nginx -v
nginx version: nginx/1.16.1
```

設定ファイルを用意します。
サーバー名にはhttp://サーバー名/のサーバー名をユーザー名には自分のユーザー名に置き換えてください。

```nginx:/etc/nginx/conf.d/mysite.conf
upstream django {
  server 127.0.0.1:8000;
}

server {
  listen 80;

  server_name サーバー名;

  location / {
    uwsgi_pass django;
    include /home/ユーザー名/mysite/uwsgi_params;
  }
}
```

uwsgi用のパラメータファイルが用意されているのでプロジェクトにコピーします。

```
$ cp /etc/nginx/uwsgi_params ~/mysite
```

一通り準備が出来たのでnginxを再起動させます。

```
$ sudo systemctl restart nginx
```

Djangoを実行してみます。

```
$ cd ~/mysite
$ uwsgi --socket :8000 --module mysite.wsgi
```

実行出来ているようなのでhttp://サーバー名/にアクセスしてみます。
![django7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252533/de14e15c-0506-7786-d4ed-ecc882e5ebd8.png)
何やらエラーが発生しています。
原因はALLOWED_HOSTSを設定せずに実行したせいです。
ALLOWED_HOSTSを設定します。

```python:mysite/settings.py
ALLOWED_HOSTS = ['サーバー名']
```

設定したらもう一度実行してみます。

![django8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252533/96e52454-6714-2ebc-aa51-aa5274ffbcce.png)
ロケットが飛んでいるページが表示されました。
ロケットのページが表示されるのはDEBUGがTrueになっているからなのでFalseに変えてみます。

```python:mysite/settings.py
DEBUG = False
```

設定したらもう一度実行します。
![django9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252533/92682e6a-7cb6-8361-455f-3fdeceaabf9e.png)
今度は何も表示されなくなりました。
トップページはまだ用意されていないので代わりに管理ページを表示してみます。
http://サーバー名/admin/にアクセスします。
![django10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252533/a706cde2-c49e-f2f5-100f-3e346ba5d051.png)
画像などの静的ファイルが用意されていないので文字だけの表示になっています。
静的ファイルを用意します。
まず静的ファイルを格納するディレクトリを用意します。

```
$ sudo mkdir /usr/share/nginx/html/static
$ sudo chown ユーザー名 /usr/share/nginx/html/static
```

Djangoに静的ファイルの格納先を設定します。

```python:mysite/settings.py
STATIC_URL = '/static/'

STATIC_ROOT = '/usr/share/nginx/html/static'
```

静的ファイルを格納先に集めます。

```
$ python3 manage.py collectstatic
```

nginx側にも静的ファイルの格納先を設定します。

```nginx:/etc/nginx/conf.d/mysite.conf
  location /static {
    alias /usr/share/nginx/html/static;
  }

  location / {
```

サーバーの設定を変更したら適用するためにサーバーを再起動します。

```
$ sudo systemctl restart nginx
```

準備が出来たので実行してアクセスしてみます。
![django11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252533/4ac839f9-2d4a-a1d9-7f63-fb522f3c6c63.png)
表示されました。
最後にunixソケットでアクセスするように変更してみます。

```nginx:/etc/nginx/conf.d/mysite.conf
upstream django {
  server unix:///home/ユーザー名/mysite/mysite.sock;
#  server 127.0.0.1:8000;
}
```
設定を適用させるためnginxを再起動します。

```
$ sudo systemctl restart nginx
```

実行してアクセスしてみます。
![django12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252533/2ce0062c-4288-e25b-fbab-94ca9f847cc3.png)
502 Bad Gatewayが発生しました。
エラーログを見てみます。

```
$ sudo cat /var/log/nginx/error.log
2020/08/31 03:41:54 [crit] 12700#0: *2 connect() to unix:///home/ユーザー名/mysite/mysite.sock failed (13: Permission denied) while connecting to upstream, ...
```
Permission deniedが発生していることがわかります。
原因はnginxがユーザーのディレクトリのアクセス権限がないためです。
nginxがアクセスできるようにユーザーのグループにnginxを追加します。

```
$ sudo usermod -aG ユーザー名 nginx
$ chmod 750 /home/ユーザー名
$ sudo systemctl restart nginx
```

もう一度実行してアクセスしてみます。
![django11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252533/b06bbcce-1103-69bb-628f-2ffae25b594e.png)
今度は表示されました。

以上、さくらVPSのCentOS7でDjango+uwsgi+nginx+sqliteを動かすでした。