Docker on Amazon Web Services
上QQ阅读APP看书,第一时间看更新

Generating static web content

If you browse to http://localhost:8000/todos, although the application is no longer returning an error, the formatting of the web page still is broken. The problem here is that Django requires you to run a separate manage.py management task called collectstatic, which generates static content and places it at the location defined by the STATIC_ROOT setting. The release settings for our application define the file location for this as /public/static, so we somehow need to run the collectstatic task before our application starts up. Note that Django serves all static content from the /static URL path, for example http://localhost:8000/static.

There are a couple of approaches that you can use to solve this:

  • Create an entrypoint script that runs on startup and executes the collectstatic task before starting the application.
  • Create an external volume and run a container that executes the collectstatic task, generating static files in the volume. Then start the application with the external volume mounted, ensuring it has access to static content. 

Both of these approaches are valid, however, to introduce the concept of Docker volumes and how you can use them in Docker Compose, we will adopt the second approach.

To define a volume in Docker Compose, you use the top-level volumes parameter, which allows you to define one or more named volumes:

version: '2.4'

volumes:
public:
driver: local

services:
test:
...
...
release:
...
...
app:
extends:
service: release
depends_on:
db:
condition: service_healthy
volumes:
- public:/public
ports:
- 8000:8000
command:
- uwsgi
- --http=0.0.0.0:8000
- --module=todobackend.wsgi
- --master
- --check-static=/public
migrate:
...
...
db:
...
...

In the preceding example, you add a volume called public and specify the driver as local, meaning it is a standard Docker volume. You then use the volumes parameter in the app service to mount the public volume to the /public path in the container, and finally you configure uwsgi to serve requests for static content from the /public path, which avoids expensive application calls to the Python interpreter to serve static content.

After tearing down your current Docker Compose environment, all that is required to generate static content is the docker-compose run command:

> docker-compose down -v
...
...
> docker-compose up migrate
...
...
> docker-compose run app python3 manage.py collectstatic --no-input
Starting todobackend_db_1 ... done
Copying '/usr/lib/python3.6/site-packages/django/contrib/admin/static/admin/js/prepopulate.js'
Traceback (most recent call last):
File "manage.py", line 15, in <module>
execute_from_command_line(sys.argv)
File "/usr/lib/python3.6/site-packages/django/core/management/__init__.py", line 371, in execute_from_command_line
utility.execute()
...
...
PermissionError: [Errno 13] Permission denied: '/public/static'

In the preceding example, the collectstatic task fails because, by default, volumes are created as root and the container runs as the app user. To resolve this, we need to pre-create the /public folder in Dockerfile and make the app user the owner of this folder:

# Test stage
...
...
# Release stage
FROM alpine
LABEL application=todobackend
...
...
# Copy and install application source and pre-built dependencies
COPY --from=test --chown=app:app /build /build
COPY --from=test --chown=app:app /app /app
RUN pip3 install -r /build/requirements.txt -f /build --no-index --no-cache-dir
RUN rm -rf /build

# Create public volume
RUN mkdir /public
RUN chown app:app /public
VOLUME /public

# Set working directory and application user
WORKDIR /app
USER app

Note that the approach shown above only works for volumes that are created using Docker volume mounts, which is what Docker Compose uses if you don't specify a host path on your Docker Engine.   If you specify a host path, the volume is bind mounted, which causes the volume to have root ownership by default, unless you pre-create the path on the host with the correct permissions.  We will encounter this issue later on when we use the Elastic Container Service, so keep this in mind.

Because you modified the Dockerfile, you need to tell Docker Compose to rebuild all images, which you can do by using the docker-compose build command:

> docker-compose down -v
...
...
> docker-compose build
Building test
Step 1/13 : FROM alpine AS test
...
...
Building release
...
...
Building app
...
...
Building migrate
...
...
> docker-compose up migrate
...
...
> docker-compose run app python3 manage.py collectstatic --no-input
Copying '/usr/lib/python3.6/site-packages/django/contrib/admin/static/admin/js/prepopulate.js'
Copying '/usr/lib/python3.6/site-packages/django/contrib/admin/static/admin/js/SelectFilter2.js'
Copying '/usr/lib/python3.6/site-packages/django/contrib/admin/static/admin/js/change_form.js'
Copying '/usr/lib/python3.6/site-packages/django/contrib/admin/static/admin/js/inlines.min.js'
...
...
> docker-compose up app

If you now browse to http://localhost:8000, the correct static content should be displayed.

When you define a local volume in Docker Compose, the volume will be automatically be destroyed when you run the docker-compose down -v command. If you wish to persist storage independently of Docker Compose, you can define an external volume, which you are then responsible for creating and destroying.  See  https://docs.docker.com/compose/compose-file/compose-file-v2/#external for more details.