Skip to content

Commit c556975

Browse files
authored
Feat: initial appcontainer setup (#38)
2 parents 8a70f2e + 89e5a3b commit c556975

File tree

13 files changed

+246
-64
lines changed

13 files changed

+246
-64
lines changed

.devcontainer/Dockerfile

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,8 @@
1-
ARG PYTHON_VERSION=3.12
1+
FROM caltrans/pems:app
22

3-
FROM python:${PYTHON_VERSION}
4-
5-
ENV PYTHONDONTWRITEBYTECODE=1 \
6-
PYTHONUNBUFFERED=1 \
7-
USER=caltrans
8-
9-
# create non-root $USER and home directory
10-
RUN useradd --create-home --shell /bin/bash $USER && \
11-
chown -R $USER /home/$USER
12-
13-
# switch to $USER
14-
USER $USER
15-
16-
# enter src directory
17-
WORKDIR /home/$USER/src
18-
19-
# update PATH for local pip installs
20-
ENV PATH="$PATH:/home/$USER/.local/bin"
21-
22-
# upgrade pip
23-
RUN python -m pip install --upgrade pip
24-
25-
# copy assets
263
COPY . .
27-
284
# install devcontainer requirements
295
RUN pip install -e .[dev,test]
306

317
# install docs requirements
328
RUN pip install --no-cache-dir -r docs/requirements.txt
33-
34-
# install pre-commit environments in throwaway Git repository
35-
# https://stackoverflow.com/a/68758943
36-
RUN git init . && \
37-
pre-commit install-hooks && \
38-
rm -rf .git
39-
40-
CMD sleep infinity
41-
42-
ENTRYPOINT []

.devcontainer/devcontainer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"dockerComposeFile": ["../compose.yml"],
44
"service": "dev",
55
"forwardPorts": ["docs:8000", "kibana:5601"],
6-
"workspaceFolder": "/home/caltrans/src",
6+
"workspaceFolder": "/caltrans/app",
77
"postStartCommand": ["/bin/bash", "bin/reset_db.sh"],
88
"postAttachCommand": ["/bin/bash", ".devcontainer/postAttach.sh"],
99
"customizations": {

.devcontainer/postAttach.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ set -eux
33

44
# initialize pre-commit
55
git config --global --add safe.directory /home/$USER/src
6-
pre-commit install --overwrite
6+
pre-commit install --install-hooks --overwrite

.dockerignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
.git/
21
*.egg-info
32
*.db

.env.sample

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ DJANGO_DB_RESET=true
88
DJANGO_STORAGE_DIR=.
99
DJANGO_DB_FILE=django.db
1010

11+
# Other Django config
12+
DJANGO_DEBUG=true
13+
1114
# uncomment to start the elasticstack services with compose
1215
# COMPOSE_PROFILES=elasticstack
1316

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
"[python]": {
2121
"editor.defaultFormatter": "ms-python.black-formatter"
2222
},
23+
"[toml]": {
24+
"editor.defaultFormatter": "tamasfe.even-better-toml"
25+
},
2326
"python.languageServer": "Pylance",
2427
"python.testing.pytestArgs": ["tests/pytest"],
2528
"python.testing.pytestEnabled": true,

appcontainer/Dockerfile

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
ARG PYTHON_VERSION=3.12
2+
3+
# multi-stage build
4+
#
5+
# stage 1: builds the pems package from source
6+
# using the git metadata for version info
7+
FROM python:${PYTHON_VERSION} AS build_wheel
8+
WORKDIR /build
9+
10+
# upgrade pip
11+
RUN python -m pip install --upgrade pip && \
12+
pip install build
13+
14+
# copy source files
15+
COPY . .
16+
RUN git config --global --add safe.directory /build
17+
18+
# build package
19+
RUN python -m build
20+
21+
# multi-stage build
22+
#
23+
# stage 2: installs the pems package in a fresh base container
24+
# using the pre-built package, and copying only needed source
25+
FROM python:${PYTHON_VERSION} AS app_container
26+
27+
ENV PYTHONDONTWRITEBYTECODE=1 \
28+
PYTHONUNBUFFERED=1 \
29+
USER=caltrans
30+
31+
EXPOSE 8000
32+
33+
# create non-root $USER and home directory
34+
RUN useradd --create-home --shell /bin/bash $USER && \
35+
# setup $USER permissions for nginx
36+
mkdir -p /var/cache/nginx && \
37+
chown -R $USER:$USER /var/cache/nginx && \
38+
mkdir -p /var/lib/nginx && \
39+
chown -R $USER:$USER /var/lib/nginx && \
40+
mkdir -p /var/log/nginx && \
41+
chown -R $USER:$USER /var/log/nginx && \
42+
touch /var/log/nginx/error.log && \
43+
chown $USER:$USER /var/log/nginx/error.log && \
44+
touch /var/run/nginx.pid && \
45+
chown -R $USER:$USER /var/run/nginx.pid && \
46+
# setup directories and permissions for gunicorn, (eventual) app
47+
mkdir -p /$USER/app && \
48+
mkdir -p /$USER/run && \
49+
chown -R $USER:$USER /$USER && \
50+
# install server components
51+
apt-get update && \
52+
apt-get install -qq --no-install-recommends build-essential nginx gettext && \
53+
python -m pip install --upgrade pip
54+
55+
# enter source directory
56+
WORKDIR /$USER
57+
58+
COPY LICENSE app/LICENSE
59+
60+
# switch to non-root $USER
61+
USER $USER
62+
63+
# update env for local pip installs
64+
# see https://docs.python.org/3/using/cmdline.html#envvar-PYTHONUSERBASE
65+
# since all `pip install` commands are in the context of $USER
66+
# $PYTHONUSERBASE is the location used by default
67+
ENV PATH="$PATH:/$USER/.local/bin" \
68+
PYTHONUSERBASE="/$USER/.local"
69+
70+
# copy gunicorn config file
71+
COPY appcontainer/gunicorn.conf.py run/gunicorn.conf.py
72+
ENV GUNICORN_CONF "/$USER/run/gunicorn.conf.py"
73+
74+
# overwrite default nginx.conf
75+
COPY appcontainer/nginx.conf /etc/nginx/nginx.conf
76+
77+
WORKDIR /$USER/app
78+
79+
# copy runtime files
80+
COPY --from=build_wheel /build/dist /build/dist
81+
COPY manage.py manage.py
82+
COPY bin bin
83+
COPY pems pems
84+
85+
# install source as a package
86+
RUN pip install $(find /build/dist -name pems*.whl)
87+
88+
# configure container executable
89+
ENTRYPOINT ["/bin/bash"]
90+
CMD ["bin/start.sh"]

appcontainer/gunicorn.conf.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""
2+
The Gunicorn configuration file
3+
More information: https://docs.gunicorn.org/en/stable/settings.html
4+
"""
5+
6+
import multiprocessing
7+
8+
# the unix socket defined in nginx.conf
9+
bind = "unix:/caltrans/run/gunicorn.sock"
10+
11+
# Recommend (2 x $num_cores) + 1 as the number of workers to start off with
12+
workers = multiprocessing.cpu_count() * 2 + 1
13+
14+
# send logs to stdout and stderr
15+
accesslog = "-"
16+
errorlog = "-"
17+
18+
# Preloading can save some RAM resources as well as speed up server boot times,
19+
# at the cost of not being able to reload app code by restarting workers
20+
preload_app = True

appcontainer/nginx.conf

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
worker_processes auto;
2+
error_log stderr warn;
3+
pid /var/run/nginx.pid;
4+
5+
events {
6+
worker_connections 1024;
7+
accept_mutex on;
8+
}
9+
10+
http {
11+
include mime.types;
12+
default_type application/octet-stream;
13+
sendfile on;
14+
gzip on;
15+
keepalive_timeout 5;
16+
17+
log_format main '$remote_addr - $remote_user [$time_local] '
18+
'"$request" $status $body_bytes_sent '
19+
'"$http_referer" "$http_user_agent" "$gzip_ratio"';
20+
21+
access_log /dev/stdout main;
22+
23+
upstream app_server {
24+
# fail_timeout=0 means we always retry an upstream even if it failed
25+
# to return a good HTTP response
26+
server unix:/caltrans/run/gunicorn.sock fail_timeout=0;
27+
}
28+
29+
server {
30+
listen 8000;
31+
32+
keepalive_timeout 65;
33+
34+
# 404 known scraping path targets
35+
# case-insensitive regex matches the given path fragment anywhere in the request path
36+
location ~* /(\.?git|api|app|assets|ats|bootstrap|bower|cgi|content|cpanel|credentials|debug|docker|doc|env|example|jenkins|robots|swagger|web|yq) {
37+
access_log off;
38+
log_not_found off;
39+
return 404;
40+
}
41+
42+
# 404 known scraping file targets
43+
# case-insensitive regex matches the given file extension anywhere in the request path
44+
location ~* /.*\.(ash|asp|axd|cgi|com|db|env|json|php|ping|sqlite|xml|ya?ml) {
45+
access_log off;
46+
log_not_found off;
47+
return 404;
48+
}
49+
50+
location /favicon.ico {
51+
access_log off;
52+
log_not_found off;
53+
expires 1y;
54+
add_header Cache-Control public;
55+
}
56+
57+
# path for static files
58+
location /static/ {
59+
alias /caltrans/app/static/;
60+
expires 1y;
61+
add_header Cache-Control public;
62+
}
63+
64+
location / {
65+
# checks for static file, if not found proxy to app
66+
try_files $uri @proxy_to_app;
67+
}
68+
69+
# app path
70+
location @proxy_to_app {
71+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
72+
proxy_set_header X-Forwarded-Proto $scheme;
73+
proxy_set_header Host $http_host;
74+
# we don't want nginx trying to do something clever with
75+
# redirects, we set the Host: header above already.
76+
proxy_redirect off;
77+
proxy_pass http://app_server;
78+
}
79+
}
80+
}

bin/start.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env bash
2+
set -eu
3+
4+
# initialize Django
5+
6+
bin/init.sh
7+
8+
# start the web server
9+
10+
nginx
11+
12+
# start the application server
13+
14+
python -m gunicorn -c $GUNICORN_CONF pems.wsgi

compose.yml

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,34 @@
11
name: pems
22

33
services:
4+
app:
5+
build:
6+
context: .
7+
dockerfile: appcontainer/Dockerfile
8+
image: caltrans/pems:app
9+
env_file: .env
10+
ports:
11+
- "8000"
12+
413
dev:
514
build:
615
context: .
716
dockerfile: .devcontainer/Dockerfile
8-
image: caltrans/pems:main
17+
image: caltrans/pems:dev
918
env_file: .env
19+
# https://code.visualstudio.com/docs/remote/create-dev-container#_use-docker-compose
20+
entrypoint: sleep infinity
1021
volumes:
11-
- ./:/home/caltrans/src
22+
- ./:/caltrans/app
1223

1324
docs:
14-
image: caltrans/pems:main
25+
image: caltrans/pems:dev
1526
entrypoint: mkdocs
1627
command: serve --dev-addr "0.0.0.0:8000"
1728
ports:
1829
- "8000"
1930
volumes:
20-
- ./:/home/caltrans/src
31+
- ./:/caltrans/app
2132

2233
esconfig:
2334
profiles: ["elasticstack"]

manage.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,17 @@
33
import os
44
import sys
55

6+
from pems import __version__ as VERSION
7+
68

79
def main():
810
"""Run administrative tasks."""
911
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pems.settings")
12+
13+
if len(sys.argv) == 2 and sys.argv[1] == "--version" or sys.argv[1] == "-v":
14+
print(f"pems, {VERSION}")
15+
return
16+
1017
try:
1118
from django.core.management import execute_from_command_line
1219
except ImportError as exc:

0 commit comments

Comments
 (0)