Skip to content

Commit fe3cec1

Browse files
committed
Add production deployment configuration
- Add deploy/ folder with Dockerfile.prod, nginx, uwsgi configs - Add production.ini (secrets externalized to secrets.ini) - Add entrypoint that merges production.ini + secrets.ini at startup - Add build-deploy.yml GitHub Actions workflow - Add dependabot.yml
1 parent 299dbb9 commit fe3cec1

8 files changed

Lines changed: 724 additions & 0 deletions

File tree

.github/dependabot.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: github-actions
4+
directory: /
5+
schedule:
6+
interval: weekly
7+
groups:
8+
actions:
9+
patterns:
10+
- "*"

.github/workflows/build-deploy.yml

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
name: Build and Deploy CKAN
2+
3+
on:
4+
push:
5+
branches: [master, ckan211-prod-deploy]
6+
tags: ['v*']
7+
workflow_dispatch:
8+
inputs:
9+
image_tag:
10+
description: 'Image tag to deploy (e.g., sha-abc1234 or v1.0.0)'
11+
required: true
12+
type: string
13+
environment:
14+
description: 'Target environment'
15+
required: true
16+
type: choice
17+
options:
18+
- staging
19+
- production
20+
21+
env:
22+
ACR_NAME: adracr
23+
IMAGE_NAME: ckan
24+
25+
jobs:
26+
build:
27+
if: github.event_name != 'workflow_dispatch'
28+
runs-on: ubuntu-latest
29+
outputs:
30+
image_tag: ${{ steps.set-env.outputs.image_tag }}
31+
environment: ${{ steps.set-env.outputs.environment }}
32+
steps:
33+
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
34+
35+
- name: Determine environment and image tag
36+
id: set-env
37+
run: |
38+
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
39+
echo "environment=production" >> $GITHUB_OUTPUT
40+
echo "image_tag=${{ github.ref_name }}" >> $GITHUB_OUTPUT
41+
else
42+
echo "environment=staging" >> $GITHUB_OUTPUT
43+
echo "image_tag=sha-$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
44+
fi
45+
46+
- name: Docker meta
47+
id: meta
48+
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.6.1
49+
with:
50+
images: ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}
51+
tags: |
52+
type=sha
53+
type=ref,event=tag
54+
55+
- name: Login to ACR
56+
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
57+
with:
58+
registry: ${{ env.ACR_NAME }}.azurecr.io
59+
username: ${{ secrets.ACR_USERNAME }}
60+
password: ${{ secrets.ACR_PASSWORD }}
61+
62+
- name: Build and push
63+
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.10.0
64+
with:
65+
context: .
66+
file: deploy/Dockerfile.prod
67+
push: true
68+
tags: ${{ steps.meta.outputs.tags }}
69+
labels: ${{ steps.meta.outputs.labels }}
70+
71+
deploy:
72+
if: always() && (needs.build.result == 'success' || github.event_name == 'workflow_dispatch')
73+
needs: build
74+
runs-on: ubuntu-latest
75+
environment:
76+
name: ${{ github.event_name == 'workflow_dispatch' && inputs.environment || needs.build.outputs.environment }}
77+
url: ${{ steps.params.outputs.url }}
78+
steps:
79+
- name: Set deploy params
80+
id: params
81+
run: |
82+
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
83+
ENV="${{ inputs.environment }}"
84+
echo "image_tag=${{ inputs.image_tag }}" >> $GITHUB_OUTPUT
85+
else
86+
ENV="${{ needs.build.outputs.environment }}"
87+
echo "image_tag=${{ needs.build.outputs.image_tag }}" >> $GITHUB_OUTPUT
88+
fi
89+
90+
if [[ "$ENV" == "production" ]]; then
91+
echo "namespace=adr-p" >> $GITHUB_OUTPUT
92+
echo "url=https://p-ckan.azurefd.net" >> $GITHUB_OUTPUT
93+
else
94+
echo "namespace=adr-s" >> $GITHUB_OUTPUT
95+
echo "url=https://s-ckan-g3c2cqgzfygmfdam.z01.azurefd.net" >> $GITHUB_OUTPUT
96+
fi
97+
98+
- name: Setup kubeconfig
99+
run: |
100+
mkdir -p ~/.kube
101+
echo "${{ secrets.KUBECONFIG_BASE64 }}" | base64 -d > ~/.kube/config
102+
chmod 600 ~/.kube/config
103+
104+
- name: Deploy to AKS
105+
run: |
106+
kubectl set image deployment/ckan \
107+
ckan=${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ steps.params.outputs.image_tag }} \
108+
-n ${{ steps.params.outputs.namespace }}
109+
kubectl rollout status deployment/ckan -n ${{ steps.params.outputs.namespace }} --timeout=5m

deploy/Dockerfile.prod

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
FROM python:3.10
2+
3+
# OCI Annotations
4+
LABEL org.opencontainers.image.maintainer="support@fjelltopp.org"
5+
LABEL org.opencontainers.image.title="CKAN-ADX"
6+
LABEL org.opencontainers.image.description="Fjelltopp's ADX CKAN production image"
7+
8+
ARG CKAN_SITE_URL
9+
10+
# Set timezone
11+
ENV TZ=UTC
12+
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
13+
14+
# Setting the locale
15+
ENV LC_ALL=en_US.UTF-8
16+
RUN apt-get update && \
17+
apt-get install --no-install-recommends -y locales && \
18+
sed -i "/$LC_ALL/s/^# //g" /etc/locale.gen && \
19+
dpkg-reconfigure --frontend=noninteractive locales && \
20+
update-locale LANG=${LC_ALL}
21+
22+
# Define environment variables
23+
ENV CKAN_HOME /usr/lib/adx
24+
ENV CKAN_VENV $CKAN_HOME/venv
25+
ENV CKAN_CONFIG /etc/ckan
26+
ENV CKAN_STORAGE_PATH=/var/lib/ckan
27+
ENV PATH=${CKAN_VENV}/bin:${PATH}
28+
29+
# Install required system packages
30+
RUN apt-get -q -y update \
31+
&& apt-get -q -y install \
32+
libpq-dev \
33+
libmagic-dev \
34+
libxml2-dev \
35+
libxslt-dev \
36+
libgeos-dev \
37+
libssl-dev \
38+
libffi-dev \
39+
postgresql-client \
40+
build-essential \
41+
git-core \
42+
vim \
43+
wget \
44+
curl \
45+
xmlsec1 \
46+
jq \
47+
supervisor \
48+
nginx \
49+
&& apt-get -q clean \
50+
&& rm -rf /var/lib/apt/lists/*
51+
52+
# Add CKAN user
53+
RUN useradd -r -u 900 -m -c "ckan account" -d $CKAN_HOME -s /bin/false ckan
54+
55+
# Install pipenv and uwsgi
56+
RUN pip3 install pipenv uwsgi
57+
58+
# Copy submodules and Pipfile
59+
COPY submodules /usr/lib/adx/submodules
60+
COPY Pipfile Pipfile.lock /usr/lib/adx/
61+
62+
# Install Python dependencies
63+
WORKDIR /usr/lib/adx
64+
RUN mkdir .venv && pipenv sync -v && ln -s .venv venv
65+
66+
# Install Node.js and yarn for React build
67+
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
68+
apt-get install -y nodejs && \
69+
npm install --global yarn
70+
71+
# Build React components
72+
RUN yarn --cwd /usr/lib/adx/submodules/ckanext-unaids/ckanext/unaids/react/ && \
73+
yarn --cwd /usr/lib/adx/submodules/ckanext-unaids/ckanext/unaids/react/ build && \
74+
chmod -R 777 /usr/lib/adx/submodules/ckanext-unaids/ckanext/unaids/assets/build
75+
76+
RUN chown -R ckan:ckan /usr/lib/adx/
77+
RUN mkdir -p /var/lib/ckan/resources && chmod 777 -R /var/lib/ckan
78+
79+
# Create symlink for backward compatibility with configs that reference /usr/lib/ckan
80+
RUN ln -s /usr/lib/adx /usr/lib/ckan
81+
82+
# Copy entrypoint and config files
83+
COPY deploy/ckan-entrypoint-prod.sh /ckan-entrypoint.sh
84+
COPY ckan/adx_config.ini $CKAN_CONFIG/ckan.ini
85+
COPY ckan/adx_who.ini $CKAN_CONFIG/who.ini
86+
COPY ckan/ckan_supervisor.conf /etc/supervisor/conf.d/ckan_supervisor.conf
87+
COPY deploy/uwsgi.ini /usr/lib/adx/uwsgi.ini
88+
COPY deploy/nginx.conf /etc/nginx/nginx.conf
89+
90+
RUN chmod +x /*.sh
91+
92+
USER root
93+
EXPOSE 5000
94+
95+
ENTRYPOINT ["/ckan-entrypoint.sh"]
96+
CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"]

deploy/ckan-entrypoint-prod.sh

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# URL for the primary database, in the format expected by sqlalchemy
5+
: "${CKAN_SQLALCHEMY_URL:=}"
6+
: "${CKAN_SOLR_URL:=}"
7+
: "${CKAN_REDIS_URL:=}"
8+
: "${CKAN_DATAPUSHER_URL:=}"
9+
10+
export CKAN_HOME=/usr/lib/adx
11+
export CKAN_VENV=$CKAN_HOME/venv
12+
export PATH=${CKAN_VENV}/bin:${PATH}
13+
14+
# Combine base config with secrets using Python ConfigParser
15+
# secrets.ini values override production.ini
16+
echo "Combining configuration files..."
17+
python3 << 'PYEOF'
18+
import configparser
19+
import sys
20+
21+
config = configparser.ConfigParser()
22+
config.read('/etc/ckan/production.ini')
23+
config.read('/etc/ckan/secrets.ini') # Later values override earlier
24+
25+
with open('/tmp/ckan.ini', 'w') as f:
26+
config.write(f)
27+
PYEOF
28+
export CONFIG="/tmp/ckan.ini"
29+
export CKAN_INI="/tmp/ckan.ini"
30+
31+
abort () {
32+
echo "$@" >&2
33+
exit 1
34+
}
35+
36+
set_environment () {
37+
export CKAN_SITE_ID=${CKAN_SITE_ID}
38+
export CKAN_SITE_URL=${CKAN_SITE_URL}
39+
export CKAN_SQLALCHEMY_URL=${CKAN_SQLALCHEMY_URL}
40+
export CKAN_SOLR_URL=${CKAN_SOLR_URL}
41+
export CKAN_REDIS_URL=${CKAN_REDIS_URL}
42+
export CKAN_STORAGE_PATH=/var/lib/ckan
43+
export CKAN_DATAPUSHER_URL=${CKAN_DATAPUSHER_URL}
44+
export CKAN_DATASTORE_WRITE_URL=${CKAN_DATASTORE_WRITE_URL}
45+
export CKAN_DATASTORE_READ_URL=${CKAN_DATASTORE_READ_URL}
46+
export CKAN_SMTP_SERVER=${CKAN_SMTP_SERVER}
47+
export CKAN_SMTP_STARTTLS=${CKAN_SMTP_STARTTLS}
48+
export CKAN_SMTP_USER=${CKAN_SMTP_USER}
49+
export CKAN_SMTP_PASSWORD=${CKAN_SMTP_PASSWORD}
50+
export CKAN_SMTP_MAIL_FROM=${CKAN_SMTP_MAIL_FROM}
51+
export CKAN_MAX_UPLOAD_SIZE_MB=${CKAN_MAX_UPLOAD_SIZE_MB}
52+
if [ -n "${ADR_CKAN_SAML_IDP_CERT}" ]; then
53+
echo "${ADR_CKAN_SAML_IDP_CERT}" > /tmp/saml_idp.crt || echo "Warning: Could not write SAML IDP cert"
54+
fi
55+
}
56+
57+
# Validate required environment variables
58+
if [ -z "$CKAN_SQLALCHEMY_URL" ]; then
59+
abort "ERROR: no CKAN_SQLALCHEMY_URL specified"
60+
fi
61+
62+
if [ -z "$CKAN_SOLR_URL" ]; then
63+
abort "ERROR: no CKAN_SOLR_URL specified"
64+
fi
65+
66+
if [ -z "$CKAN_REDIS_URL" ]; then
67+
abort "ERROR: no CKAN_REDIS_URL specified"
68+
fi
69+
70+
if [ -z "$CKAN_DATAPUSHER_URL" ]; then
71+
abort "ERROR: no CKAN_DATAPUSHER_URL specified"
72+
fi
73+
74+
set_environment
75+
echo "CKAN production environment ready"
76+
77+
# Initialize CKAN database and run plugin migrations
78+
echo "Initializing CKAN database..."
79+
ckan --config="$CONFIG" db init || echo "CKAN database already initialized"
80+
81+
echo "Setting up DataStore permissions..."
82+
ckan --config="$CONFIG" datastore set-permissions | psql "${CKAN_DATASTORE_WRITE_URL}" || echo "Warning: DataStore set-permissions failed or already applied"
83+
84+
echo "Running database migrations for plugins..."
85+
ckan --config="$CONFIG" db upgrade -p pages || echo "Warning: ckanext-pages migration failed or already applied"
86+
# ckan --config="$CONFIG" versions initdb || echo "Warning: ckanext-versions initdb failed or already applied"
87+
# ckan --config="$CONFIG" validation init-db || echo "Warning: ckanext-validation init-db failed or already applied"
88+
ckan --config="$CONFIG" unaids initdb || echo "Warning: ckanext-unaids initdb failed or already applied"
89+
90+
exec "$@"

deploy/nginx.conf

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
worker_processes auto;
2+
error_log /dev/stderr warn;
3+
pid /tmp/nginx.pid;
4+
5+
events {
6+
worker_connections 1024;
7+
}
8+
9+
http {
10+
include /etc/nginx/mime.types;
11+
default_type application/octet-stream;
12+
13+
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
14+
'$status $body_bytes_sent "$http_referer" '
15+
'"$http_user_agent" "$http_x_forwarded_for"';
16+
17+
access_log /dev/stdout main;
18+
19+
sendfile on;
20+
tcp_nopush on;
21+
keepalive_timeout 65;
22+
gzip on;
23+
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
24+
25+
upstream ckan {
26+
server 127.0.0.1:8000;
27+
}
28+
29+
server {
30+
listen 5000;
31+
server_name _;
32+
33+
client_max_body_size 256M;
34+
35+
# Internal location for X-Accel-Redirect (x-sendfile)
36+
# CKAN sends X-Accel-Redirect with the full filesystem path
37+
location /var/lib/ckan/webassets/ {
38+
internal;
39+
alias /var/lib/ckan/webassets/;
40+
expires 7d;
41+
add_header Cache-Control "public, max-age=604800" always;
42+
add_header Vary "Accept-Encoding" always;
43+
}
44+
45+
# Other static assets - set proper caching headers
46+
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
47+
proxy_pass http://ckan;
48+
proxy_set_header Host $host;
49+
proxy_set_header X-Real-IP $remote_addr;
50+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
51+
proxy_set_header X-Forwarded-Proto $scheme;
52+
53+
# Override Cache-Control from origin - remove no-cache, set public caching
54+
proxy_hide_header Cache-Control;
55+
proxy_hide_header Vary;
56+
add_header Cache-Control "public, max-age=604800" always;
57+
add_header Vary "Accept-Encoding" always;
58+
}
59+
60+
# All other requests
61+
location / {
62+
proxy_pass http://ckan;
63+
proxy_set_header Host $host;
64+
proxy_set_header X-Real-IP $remote_addr;
65+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
66+
proxy_set_header X-Forwarded-Proto $scheme;
67+
proxy_read_timeout 300s;
68+
proxy_connect_timeout 75s;
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)