Build docker image closes #28

This commit is contained in:
2026-03-29 13:34:28 +02:00
parent 081eba0fad
commit 8dc1f27b17
10 changed files with 201 additions and 111 deletions
+12 -14
View File
@@ -1,7 +1,8 @@
FROM python:3.12-alpine3.23
# Add postgress dependencies
# Add dependencies
#RUN apk add --no-cache postgresql-libs postgresql-client
RUN apk add --no-cache tini
# Install pipenv
RUN pip install pipenv
@@ -10,31 +11,28 @@ RUN pip install pipenv
ENV PYTHONUNBUFFERED=1 \
DOCKER=true
#Create app dir and install requirements.
RUN mkdir /opt/rsvp
RUN mkdir /opt/rsvp/static
#Create app dir
RUN mkdir -p /opt/rsvp
RUN mkdir -p /opt/rsvp/static
WORKDIR /opt/rsvp
# Copy Pipfile and Pipfile.lock
COPY Pipfile Pipfile.lock ./
# Install dependencies
# Install python dependencies
RUN pipenv install --deploy --system
# Copy application code
COPY . .
RUN python manage.py collectstatic --no-input
RUN python -m blacknoise.compress /opt/rsvp/static/
#This port will be used by nginx.
EXPOSE 8000 8000
RUN mkdir /data
RUN mkdir /data/media
RUN mkdir -p /data
# The volume containing dynamic data
VOLUME /data
#This port will be used by daphne
EXPOSE 8000
# Run the application
CMD ["daphne", "rsvpproject.asgi:application"]
RUN chmod +x start.sh
ENTRYPOINT ["/sbin/tini", "--", "/opt/rsvp/start.sh"]
+1
View File
@@ -10,6 +10,7 @@ django = "*"
django-markdown = "*"
pillow = "*"
blacknoise = "*"
twisted = {extras = ["http2", "tls"], version = "*"}
[dev-packages]
Generated
+33 -2
View File
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "7e3d07ddaedc66aca9c1608f0fbe329c346a3a763ed0a7af8983ae7fa5f76b20"
"sha256": "07e2a33b6016199f2401424d35bc436e5355db12dd734f543d2633b4dd31a22d"
},
"pipfile-spec": 6,
"requires": {
@@ -337,6 +337,29 @@
"markers": "python_version >= '3.10'",
"version": "==6.0.1"
},
"h2": {
"hashes": [
"sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1",
"sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd"
],
"version": "==4.3.0"
},
"hpack": {
"hashes": [
"sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496",
"sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"
],
"markers": "python_version >= '3.9'",
"version": "==4.1.0"
},
"hyperframe": {
"hashes": [
"sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5",
"sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"
],
"markers": "python_version >= '3.9'",
"version": "==6.1.0"
},
"hyperlink": {
"hashes": [
"sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b",
@@ -541,6 +564,13 @@
"markers": "python_version >= '3.10'",
"version": "==12.1.1"
},
"priority": {
"hashes": [
"sha256:6bc1961a6d7fcacbfc337769f1a382c8e746566aaa365e78047abe9f66b2ffbe",
"sha256:be4fcb94b5e37cdeb40af5533afe6dd603bd665fe9c8b3052610fc1001d5d1eb"
],
"version": "==1.3.0"
},
"py-ubjson": {
"hashes": [
"sha256:b9bfb8695a1c7e3632e800fb83c943bf67ed45ddd87cd0344851610c69a5a482"
@@ -603,6 +633,7 @@
},
"twisted": {
"extras": [
"http2",
"tls"
],
"hashes": [
@@ -640,7 +671,7 @@
"sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466",
"sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"
],
"markers": "python_version < '3.13'",
"markers": "python_version >= '3.9'",
"version": "==4.15.0"
},
"ujson": {
+8
View File
@@ -33,6 +33,14 @@ Requirements (installed from Pipfile):
- Daphne
- BlackNoise
## Publish docker image
Build
`docker build -t gitea.furb.it/kennyboy55/friend-event-rsvp:dev .`
How to tag:
`docker push gitea.furb.it/kennyboy55/friend-event-rsvp:dev`
## Run
Run the server during development: `DEBUG=1 python manage.py runserver`
+9
View File
@@ -0,0 +1,9 @@
services:
app:
image: gitea.furb.it/kennyboy55/friend-event-rsvp:dev
container_name: friend-event-rsvp
ports:
- "8000:8000"
env_file: .env
volumes:
- ./data:/data
+15
View File
@@ -0,0 +1,15 @@
# ---------------------------------------------------------------------------
# This template contains only required options.
# ---------------------------------------------------------------------------
# random secret key, use for example `base64 /dev/urandom | head -c50` to generate one
SECRET_KEY=
# your default timezone See https://timezonedb.com/time-zones for a list of timezones
TZ=Europe/Amsterdam
# allowed hosts (see documentation), must be set to your hostname(s)
ALLOWED_HOSTS=events.mydomain.com
# enable debug log
#DEBUG=1
+65 -79
View File
@@ -1,4 +1,4 @@
# Generated by Django 6.0 on 2025-12-20 13:32
# Generated by Django 6.0.3 on 2026-03-27 12:24
import datetime
import django.db.models.deletion
@@ -38,6 +38,7 @@ class Migration(migrations.Migration):
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('title', models.CharField(max_length=255)),
('description', models.TextField(blank=True)),
('location', models.CharField(max_length=255)),
('start_time', models.DateTimeField()),
('end_time', models.DateTimeField(blank=True, null=True)),
('guest_limit', models.PositiveIntegerField(blank=True, null=True)),
@@ -66,7 +67,7 @@ class Migration(migrations.Migration):
('slug', models.SlugField(max_length=255)),
('description', models.TextField(blank=True)),
('image', models.ImageField(blank=True, null=True, upload_to=events.models.event_image_path)),
('allow_rsvp_without_invite', models.BooleanField(default=True)),
('allow_rsvp_without_invite', models.BooleanField(default=False)),
('allow_comments', models.BooleanField(default=True)),
('default_email_updates_optin', models.BooleanField(default=True)),
('guest_limit', models.PositiveIntegerField(blank=True, null=True)),
@@ -78,21 +79,6 @@ class Migration(migrations.Migration):
'ordering': ['-id'],
},
),
migrations.CreateModel(
name='Comment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=255)),
('text', models.TextField()),
('notify_subscribers', models.BooleanField(default=False)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='events.event')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='activitygroup',
name='event',
@@ -103,46 +89,6 @@ class Migration(migrations.Migration):
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activities', to='events.event'),
),
migrations.CreateModel(
name='GuestListTemplate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('title', models.CharField(max_length=255)),
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='GuestListItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('email', models.EmailField(blank=True, max_length=254)),
('is_main_invitee', models.BooleanField(default=True)),
('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='events.guestlisttemplate')),
],
),
migrations.CreateModel(
name='Invite',
fields=[
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('code', models.CharField(db_index=True, max_length=64, unique=True)),
('recipient_name', models.CharField(blank=True, max_length=255)),
('recipient_email', models.EmailField(blank=True, max_length=254)),
('personalized_message', models.TextField(blank=True)),
('allow_bring_guests', models.BooleanField(default=False)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites', to='events.event')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Guest',
fields=[
@@ -154,7 +100,21 @@ class Migration(migrations.Migration):
('is_main_invitee', models.BooleanField(default=False)),
('created_by_host', models.BooleanField(default=False)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='guests', to='events.event')),
('invite', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='guests', to='events.invite')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Comment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('text', models.TextField()),
('notify_subscribers', models.BooleanField(default=False)),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='events.event')),
('guest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='events.guest')),
],
options={
'abstract': False,
@@ -187,25 +147,6 @@ class Migration(migrations.Migration):
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='events.question')),
],
),
migrations.CreateModel(
name='RSVP',
fields=[
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('edit_token', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('responder_email', models.EmailField(blank=True, max_length=254)),
('send_email_updates', models.BooleanField(default=True)),
('status', models.CharField(choices=[('yes', 'Yes'), ('no', 'No'), ('maybe', 'Maybe'), ('pending', 'Pending')], default='pending', max_length=10)),
('notes', models.TextField(blank=True)),
('activities', models.ManyToManyField(related_name='rsvps', through='events.ActivitySelection', to='events.activity')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rsvps', to='events.event')),
('invite', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='rsvps', to='events.invite')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Response',
fields=[
@@ -214,15 +155,60 @@ class Migration(migrations.Migration):
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('value_text', models.TextField(blank=True)),
('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='events.activity')),
('choice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='events.questionchoice')),
('guest', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='events.guest')),
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='events.question')),
('rsvp', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='events.rsvp')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='ResponseChoice',
fields=[
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('choice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='response_choices', to='events.questionchoice')),
('response', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='response_choices', to='events.response')),
],
options={
'unique_together': {('response', 'choice')},
},
),
migrations.AddField(
model_name='response',
name='choices',
field=models.ManyToManyField(blank=True, related_name='responses', through='events.ResponseChoice', to='events.questionchoice'),
),
migrations.CreateModel(
name='RSVP',
fields=[
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('invite', models.SlugField(max_length=255, unique=True)),
('edit_token', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('responder_name', models.CharField(blank=True, max_length=255)),
('responder_email', models.EmailField(max_length=254)),
('send_email_updates', models.BooleanField(default=True)),
('personalized_message', models.TextField(blank=True)),
('allow_bring_guests', models.BooleanField(default=False)),
('page', models.CharField(choices=[('email', 'Email'), ('guests', 'Guests'), ('activities', 'Activities'), ('questions', 'Questions'), ('notes', 'Notes'), ('completed', 'Completed')], default='email', max_length=16)),
('furthest_page', models.CharField(choices=[('email', 'Email'), ('guests', 'Guests'), ('activities', 'Activities'), ('questions', 'Questions'), ('notes', 'Notes'), ('completed', 'Completed')], default='email', max_length=16)),
('status', models.CharField(choices=[('unopened', 'Not Opened'), ('not_started', 'Not Started'), ('incomplete', 'Incomplete'), ('not_coming', 'Not coming'), ('joining', 'Yes, joining')], default='unopened', max_length=16)),
('notes', models.TextField(blank=True)),
('activities', models.ManyToManyField(related_name='rsvps', through='events.ActivitySelection', to='events.activity')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rsvps', to='events.event')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='response',
name='rsvp',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='events.rsvp'),
),
migrations.AddField(
model_name='guest',
name='rsvp',
+14 -5
View File
@@ -8,7 +8,7 @@ https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
"""
import os
from pathlib import Path
import logging
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
@@ -17,14 +17,23 @@ from channels.security.websocket import AllowedHostsOriginValidator
from blacknoise import BlackNoise
from django.core.asgi import get_asgi_application
logger = logging.getLogger(__file__)
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rsvpproject.settings')
STATIC_DIR = Path(__file__).parent.parent / "static"
MEDIA_DIR = Path(__file__).parent.parent / "media"
from django.conf import settings
if settings.DEBUG:
logger.warning("DEBUG enabled")
if settings.DOCKER:
logger.warning("DOCKER enabled")
logger.info(f"ALLOWED_HOSTS={settings.ALLOWED_HOSTS}")
django_asgi_app = BlackNoise(get_asgi_application())
django_asgi_app.add(STATIC_DIR, "/static")
django_asgi_app.add(MEDIA_DIR, "/media")
django_asgi_app.add(settings.STATIC_ROOT, "/static")
django_asgi_app.add(settings.MEDIA_ROOT, "/media")
from events.routing import websocket_urlpatterns
+27 -11
View File
@@ -13,8 +13,24 @@ https://docs.djangoproject.com/en/6.0/ref/settings/
import os
from pathlib import Path
def extract_bool(env_key, default):
return bool(int(os.getenv(env_key, default)))
bool_map = {"true": True, "false": False}
def extract_bool(env_key, default:bool):
retval = default
env_val = os.getenv(env_key)
if env_val:
# Try as int
try:
bool_as_num = int(env_val)
retval = bool(bool_as_num)
# Try as string
except ValueError:
# Convert string to boolean using the dictionary
retval = bool_map.get(env_val.lower(), default)
return retval
def extract_comma_list(env_key, default=None):
@@ -26,14 +42,6 @@ def extract_comma_list(env_key, default=None):
else:
return []
BASE_DIR = Path(__file__).resolve().parent.parent
DOCKER_DIR = BASE_DIR
if extract_bool('DOCKER', False):
# We are running in docker
DOCKER_DIR = Path('/data')
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
@@ -42,11 +50,19 @@ SECRET_KEY = os.getenv('SECRET_KEY', 'INSECURE_STANDARD_KEY_SET_IN_ENV')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = extract_bool('DEBUG', False)
DOCKER = extract_bool('DOCKER', False)
ALLOWED_HOSTS = extract_comma_list('ALLOWED_HOSTS', ['127.0.0.1'])
ALLOWED_HOSTS = extract_comma_list('ALLOWED_HOSTS', '127.0.0.1')
CSRF_TRUSTED_ORIGINS = extract_comma_list('CSRF_TRUSTED_ORIGINS')
BASE_DIR = Path(__file__).resolve().parent.parent
DOCKER_DIR = BASE_DIR
if DOCKER:
# We are running in docker
DOCKER_DIR = Path('/data')
# Application definition
INSTALLED_APPS = [
+17
View File
@@ -0,0 +1,17 @@
#!/bin/sh
echo "Migrating database"
python manage.py migrate
echo "Collecting and optimizing static files, this may take a while..."
python manage.py collectstatic --noinput --clear
python -m blacknoise.compress static/
echo "Creating all necessary directories..."
mkdir -p /data/media
echo "Done"
daphne -b 0.0.0.0 -p 8000 rsvpproject.asgi:application