From 971bea40e91ba551c6cfbf0403851b9eb51fad05 Mon Sep 17 00:00:00 2001 From: Tobi DEGNON Date: Sun, 1 Sep 2024 17:05:31 +0100 Subject: [PATCH] docs: write deployment guide --- docs/_static/snippets/settings.py | 418 ++++++++++++++++++++++++ docs/the_cli/start_project/deploy.rst | 281 +++++++++++++++- docs/the_cli/start_project/packages.rst | 2 +- justfile | 1 + pyproject.toml | 1 - 5 files changed, 686 insertions(+), 17 deletions(-) create mode 100644 docs/_static/snippets/settings.py diff --git a/docs/_static/snippets/settings.py b/docs/_static/snippets/settings.py new file mode 100644 index 0000000..db3bd30 --- /dev/null +++ b/docs/_static/snippets/settings.py @@ -0,0 +1,418 @@ +import multiprocessing +import os +import sys +from email.utils import parseaddr +from pathlib import Path + +import sentry_sdk +from environs import Env +from marshmallow.validate import Email +from marshmallow.validate import OneOf +from myjourney.core.sentry import sentry_profiles_sampler +from myjourney.core.sentry import sentry_traces_sampler +from sentry_sdk.integrations.django import DjangoIntegration +from sentry_sdk.integrations.logging import LoggingIntegration + +# 0. Setup +# -------------------------------------------------------------------------------------------- + +BASE_DIR = Path(__file__).resolve(strict=True).parent.parent + +APPS_DIR = BASE_DIR / "myjourney" + +env = Env() +env.read_env(Path(BASE_DIR, ".env").as_posix()) + +# We should strive to only have two possible runtime scenarios: either `DEBUG` +# is True or it is False. `DEBUG` should be only true in development, and +# False when deployed, whether or not it's a production environment. +DEBUG = env.bool("DEBUG", default=False) + +# 1. Django Core Settings +# ----------------------------------------------------------------------------------------------- +# https://docs.djangoproject.com/en/4.0/ref/settings/ + +ALLOWED_HOSTS = env.list( + "ALLOWED_HOSTS", default=["*"] if DEBUG else ["localhost"], subcast=str +) + +ASGI_APPLICATION = "myjourney.asgi.application" + +# https://grantjenks.com/docs/diskcache/tutorial.html#djangocache +if "CACHE_LOCATION" in os.environ: + CACHES = { + "default": { + "BACKEND": "diskcache.DjangoCache", + "LOCATION": env.str("CACHE_LOCATION"), + "TIMEOUT": 300, + "SHARDS": 8, + "DATABASE_TIMEOUT": 0.010, # 10 milliseconds + "OPTIONS": {"size_limit": 2**30}, # 1 gigabyte + } + } + +CSRF_COOKIE_SECURE = not DEBUG + +DATABASES = { + "default": env.dj_db_url("DATABASE_URL", default="sqlite:///db.sqlite3"), +} +DATABASES["default"]["ATOMIC_REQUESTS"] = True + +if not DEBUG: + DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) + DATABASES["default"]["CONN_HEALTH_CHECKS"] = True + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +DEFAULT_FROM_EMAIL = env.str( + "DEFAULT_FROM_EMAIL", + default="tobidegnon@proton.me", + validate=lambda v: Email()(parseaddr(v)[1]), +) + +EMAIL_BACKEND = ( + "django.core.mail.backends.console.EmailBackend" + if DEBUG + else "anymail.backends.amazon_ses.EmailBackend" +) + +DJANGO_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.humanize", + "django.forms", +] + +THIRD_PARTY_APPS = [ + "allauth", + "allauth.account", + "allauth.socialaccount", + "crispy_forms", + "crispy_tailwind", + "django_htmx", + "template_partials", + "django_tailwind_cli", + "django_q", + "django_q_registry", + "health_check", + "health_check.db", + "health_check.cache", + "health_check.storage", + "health_check.contrib.migrations", + "heroicons", + "compressor", + "unique_user_email", + "django_extensions", +] + +LOCAL_APPS = [ + "myjourney.core", + "myjourney.entries", +] + +if DEBUG: + # Development only apps + THIRD_PARTY_APPS = [ + "debug_toolbar", + "whitenoise.runserver_nostatic", + "django_browser_reload", + "django_fastdev", + "django_watchfiles", + *THIRD_PARTY_APPS, + ] + +INSTALLED_APPS = LOCAL_APPS + THIRD_PARTY_APPS + DJANGO_APPS + +if DEBUG: + INTERNAL_IPS = [ + "127.0.0.1", + "10.0.2.2", + ] + +LANGUAGE_CODE = "en-us" + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "plain_console": { + "format": "%(levelname)s %(message)s", + }, + "verbose": { + "format": "%(asctime)s %(name)-12s %(levelname)-8s %(message)s", + }, + }, + "handlers": { + "stdout": { + "class": "logging.StreamHandler", + "stream": sys.stdout, + "formatter": "verbose", + }, + }, + "loggers": { + "django": { + "handlers": ["stdout"], + "level": env.log_level("DJANGO_LOG_LEVEL", default="INFO"), + }, + "myjourney": { + "handlers": ["stdout"], + "level": env.log_level("MYJOURNEY_LOG_LEVEL", default="INFO"), + }, + }, +} + +MEDIA_ROOT = env.path("MEDIA_ROOT", default=APPS_DIR / "media") + +MEDIA_URL = "/media/" + +# https://docs.djangoproject.com/en/dev/topics/http/middleware/ +# https://docs.djangoproject.com/en/dev/ref/middleware/#middleware-ordering +MIDDLEWARE = [ + # should be first + "django.middleware.cache.UpdateCacheMiddleware", + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + # order doesn't matter + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "allauth.account.middleware.AccountMiddleware", + "django_htmx.middleware.HtmxMiddleware", + # should be last + "django.middleware.cache.FetchFromCacheMiddleware", +] +if DEBUG: + MIDDLEWARE.remove("django.middleware.cache.UpdateCacheMiddleware") + MIDDLEWARE.remove("django.middleware.cache.FetchFromCacheMiddleware") + MIDDLEWARE.append("django_browser_reload.middleware.BrowserReloadMiddleware") + + MIDDLEWARE.insert( + MIDDLEWARE.index("django.middleware.common.CommonMiddleware") + 1, + "debug_toolbar.middleware.DebugToolbarMiddleware", + ) + +ROOT_URLCONF = "myjourney.urls" + +SECRET_KEY = env.str( + "SECRET_KEY", default="django-insecure-FjgA_gBuJvehOf6pIfHZwkTs0JOqvRVDKIxqmtTEELM" +) + +SECURE_HSTS_INCLUDE_SUBDOMAINS = not DEBUG + +SECURE_HSTS_PRELOAD = not DEBUG + +# https://docs.djangoproject.com/en/dev/ref/middleware/#http-strict-transport-security +# 2 minutes to start with, will increase as HSTS is tested +# example of production value: 60 * 60 * 24 * 7 = 604800 (1 week) +SECURE_HSTS_SECONDS = 0 if DEBUG else env.int("SECURE_HSTS_SECONDS", default=60 * 2) + +# https://noumenal.es/notes/til/django/csrf-trusted-origins/ +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + +SECURE_SSL_REDIRECT = not DEBUG + +SERVER_EMAIL = env.str( + "SERVER_EMAIL", + default=DEFAULT_FROM_EMAIL, + validate=lambda v: Email()(parseaddr(v)[1]), +) + +SESSION_COOKIE_SECURE = not DEBUG + +STORAGES = { + "default": { + "BACKEND": "storages.backends.s3.S3Storage", + "OPTIONS": { + "access_key": env.str("AWS_ACCESS_KEY_ID", default=None), + "bucket_name": env.str("AWS_STORAGE_BUCKET_NAME", default=None), + "region_name": env.str("AWS_S3_REGION_NAME", default=None), + "secret_key": env.str("AWS_SECRET_ACCESS_KEY", default=None), + }, + }, + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, +} +if DEBUG and not env.bool("USE_S3", default=False): + STORAGES["default"] = { + "BACKEND": "django.core.files.storage.FileSystemStorage", + } + +# https://nickjanetakis.com/blog/django-4-1-html-templates-are-cached-by-default-with-debug-true +DEFAULT_LOADERS = [ + "django.template.loaders.filesystem.Loader", + "django.template.loaders.app_directories.Loader", +] + +CACHED_LOADERS = [("django.template.loaders.cached.Loader", DEFAULT_LOADERS)] + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [str(APPS_DIR / "templates")], + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + "builtins": [ + "template_partials.templatetags.partials", + "heroicons.templatetags.heroicons", + ], + "debug": DEBUG, + "loaders": [ + ( + "template_partials.loader.Loader", + DEFAULT_LOADERS if DEBUG else CACHED_LOADERS, + ) + ], + }, + }, +] + +TIME_ZONE = "UTC" + +USE_I18N = False + +USE_TZ = True + +WSGI_APPLICATION = "myjourney.wsgi.application" + +# 2. Django Contrib Settings +# ----------------------------------------------------------------------------------------------- + +# django.contrib.auth +AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + "allauth.account.auth_backends.AuthenticationBackend", +] + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] +if DEBUG: + AUTH_PASSWORD_VALIDATORS = [] + +# django.contrib.staticfiles +STATIC_ROOT = APPS_DIR / "staticfiles" + +STATIC_URL = "/static/" + +STATICFILES_DIRS = [APPS_DIR / "static"] + +STATICFILES_FINDERS = ( + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", + "compressor.finders.CompressorFinder", +) + +# 3. Third Party Settings +# ------------------------------------------------------------------------------------------------- + +# django-allauth +ACCOUNT_AUTHENTICATION_METHOD = "email" + +ACCOUNT_DEFAULT_HTTP_PROTOCOL = "http" if DEBUG else "https" + +ACCOUNT_EMAIL_REQUIRED = True + +ACCOUNT_LOGOUT_REDIRECT_URL = "account_login" + +ACCOUNT_SESSION_REMEMBER = True + +ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False + +ACCOUNT_UNIQUE_EMAIL = True + +ACCOUNT_USERNAME_REQUIRED = False + +LOGIN_REDIRECT_URL = "home" + +# django-anymail +if not DEBUG: + ANYMAIL = { + "AMAZON_SES_CLIENT_PARAMS": { + "aws_access_key_id": env.str("AWS_ACCESS_KEY_ID", default=None), + "aws_secret_access_key": env.str("AWS_SECRET_ACCESS_KEY", default=None), + "region_name": env.str("AWS_S3_REGION_NAME", default=None), + } + } + +# django-compressor +COMPRESS_OFFLINE = not DEBUG +COMPRESS_FILTERS = { + "css": [ + "compressor.filters.css_default.CssAbsoluteFilter", + "compressor.filters.cssmin.rCSSMinFilter", + "refreshcss.filters.RefreshCSSFilter", + ], + "js": ["compressor.filters.jsmin.rJSMinFilter"], +} + +# django-crispy-forms +CRISPY_ALLOWED_TEMPLATE_PACKS = "tailwind" + +CRISPY_TEMPLATE_PACK = "tailwind" + +# django-debug-toolbar +DEBUG_TOOLBAR_CONFIG = { + "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"], + "SHOW_TEMPLATE_CONTEXT": True, + "SHOW_COLLAPSED": True, + "UPDATE_ON_FETCH": True, + "ROOT_TAG_EXTRA_ATTRS": "hx-preserve", +} + +# django-q2 +Q_CLUSTER = { + "name": "ORM", + "workers": multiprocessing.cpu_count() * 2 + 1, + "timeout": 60 * 10, # 10 minutes + "retry": 60 * 12, # 12 minutes + "queue_limit": 50, + "bulk": 10, + "orm": "default", +} + +# sentry +if (SENTRY_DSN := env.url("SENTRY_DSN", default=None)).scheme and not DEBUG: + sentry_sdk.init( + dsn=SENTRY_DSN.geturl(), + environment=env.str( + "SENTRY_ENV", + default="development", + validate=OneOf(["development", "production"]), + ), + integrations=[ + DjangoIntegration(), + LoggingIntegration(event_level=None, level=None), + ], + traces_sampler=sentry_traces_sampler, + profiles_sampler=sentry_profiles_sampler, + send_default_pii=True, + ) + +# 4. Project Settings +# ----------------------------------------------------------------------------------------------------- + +ADMIN_URL = env.str("ADMIN_URL", default="admin/") diff --git a/docs/the_cli/start_project/deploy.rst b/docs/the_cli/start_project/deploy.rst index 817ba18..8b2f68a 100644 --- a/docs/the_cli/start_project/deploy.rst +++ b/docs/the_cli/start_project/deploy.rst @@ -3,29 +3,280 @@ Deployment ========== +.. warning:: -.. todo:: + If this is your first time deploying a django project or you are a beginner, this is likely going to be confusing, I recommend checkout my `Utilmate deployment guide for django `_ + and follow the process described there, it's going to be more manuuall but you'l; get to learn more and you''l have an easier time degubgging any issues that arise. + This guide is based on the same prinicples but is meant to automate a bunch of the process and might feel overwheling if you haven't aleardy dable around the land of deploying a few times. - Explain how deployment is configured for a project generated with falco +There are two options configured option to deploy project generated with falco, the first option is centered around the traditional vps stack deployment but with a lot of tools to automate the process, the second option +is centered around deploying using docker, this should also work with any PAAS service that support docker like caprover, coolify, digital ocean app platform, fly.io, heroku, render, etc. - - s8-overlay - - caprover - - postgres / litestream +Common Setup +------------ +Let's first discuss the few common points between the two options. -.. deploying the project to caprover what is confugured by default, but you are free to change this, mode details on the `deployment guide `_. -.. build python wheel of your project, these a -.. create binary of your project using `pyapp `_ only for x86_64 linux, but you can easily add more platforms if needed. +Static files +************ +In both cases staticfiles are serve by whitenoise, so there is not much for you to do here, static files are collected when the docker image is built, and bundle directly with the binary. +.. tip:: + All the commands related to building the app in anywa for production when possible are referenced in the ``justfile``, if you think something is missing, `let me know `_. -.. The ``deploy`` folder contains some files that are needed for deployment, mainly docker related. If Docker isn't part of your deployment plan, this directory can be safely removed. -.. However, you might want to retain the ``gunicorn.conf.py`` file inside that directory, which is a basic Gunicorn configuration file that could be useful regardless of your chosen deployment strategy. +Media files +*********** -.. The project comes for docker and s6-overlay configuration for deployment. All deployment related files are in the ``deploy``folder. -.. s6-overay is an init service, uses for processes supervisation meant for -.. container. It is build around the s6 system. For more details on how s6-overlay check the dedicated guide on it. -.. All you need to known is that the container produced by the image, is meant to run your django project using gunicorn and django-q2 for background tasks -.. and scheduling feature. For more details on django-q2 checkout the guides on task quues and schedulers in django. +By default media files are configured to be be stored using the filesystemd storage, so by default they will be saved at whatever location is defined by ``MEDIA_ROOT`` +and the value of this settings is configurable via environment variable with the same name. +`django-storages `_ is also include in the project, with the default configuration set to use `s3 `_. +This is what I personally use, to make use of this instead of the filesystem storage you need to set the environment variables below + +.. tabs:: + + .. tab:: Environment variables + + .. code-block:: bash + :caption: Example + + USE_S3=True + AWS_ACCESS_KEY_ID=your_access_key + AWS_SECRET_ACCESS_KEY=your_secret_key + AWS_STORAGE_BUCKET_NAME=your_bucket_name + AWS_S3_REGION_NAME=your_region_name + + .. tab:: settings.py ( media root ) + + .. literalinclude:: /_static/snippets/settings.py + :linenos: + :lineno-start: 167 + :lines: 167-167 + + .. tab:: settings.py ( storage backend ) + + .. literalinclude:: /_static/snippets/settings.py + :linenos: + :lineno-start: 228 + :lines: 228-245 + +This `guide `_ is an excellent resource to help you setup an s3 bucket for your media files. + +.. important:: + + If you are using docker and the filesystem storage make sure to add a volume to the container to persist the media files and + If you are using caprover check out this `guide `_ on how + to serve the media files with nginx + +Datababse +********* + +There is no specific setup done for the database, you deal with this how you want. If you go with a managed solution like `aiven `_ they usually provide a backup solution, +it you also go with a PaaS to host your plateform fo your solution they usually provide a database service with automatic backup. + +If you are using postgresql in production, a simple solution is to use `django-db-backup `_ with ``django-q2`` for the task that automatically backups, if you are using sqlite +I recommend `django-litestream `_. +I've being ridin myserlf the sqlite bandwagon recently and for my personal projects my go to has being ``sqlite + litestream``, I even have a branch for this on the `tailwind falco blueprint `_. +This is evenetually going come to come to the default falco setup. + + +Cache +***** + +The cache backend is configure to use `diskcache `_. + + DiskCache is an Apache2 licensed disk and file backed cache library, written in pure-Python, and compatible with Django. + + -- diskcache github page + +By default it's not enable, all you have to do to enable it is to add the ``LOCATION`` environment variable with the path to the cache directory. + +.. tabs:: + + .. tab:: Environment variable + + .. code-block:: bash + :caption: Example + + CACHE_LOCATION=.diskcache + + + .. tab:: settings.py + + .. literalinclude:: /_static/snippets/settings.py + :linenos: + :lineno-start: 41 + :lines: 41-51 + + +If you are running in docker, you can tadd a volume to the container to persist the cache files. + +Sending Emails +************** + +`django-anymail `_ is what is used by the project for emails sending, they support a lot of emails, provider, by default the project +is configured to use `ses `_. The same environmments variables + +.. tabs:: + + .. tab:: Email Backend + + .. literalinclude:: /_static/snippets/settings.py + :linenos: + :lineno-start: 72 + :lines: 72-76 + + .. tab:: Anymail config + + .. literalinclude:: /_static/snippets/settings.py + :linenos: + :lineno-start: 351 + :lines: 351-359 + +It uses the environments variables so the same keys as for media files, this might be considered bad pratice by some, feel free to change them. + +Environment Variables +********************* + +These are te mininum required environment variables to make your project run: + +.. code-block:: bash + + SECRET_KEY=your_secret_key + ALLOWED_HOSTS=your_host + DATABASE_URL=your_database_url + ADMIN_URL=your_admin_url # not required but recommended + + +VPS Stack +--------- + +steps: + +1 - Rename `someuser` to server user in ``deploy/etc/systemd/system/myjourney.service`` +2 + +When you push the build binary file to a vps, you can use it as the example above shows if you move it to a folder on your path, just strip out the ``just run`` part. + +.. code-block:: shell + :caption: Example of pushing the binary to a vps + + curl -L -o /usr/local/bin/myjourney https://github.com/user/repo/releases/download/latest/myjourney-x86_64-linux + chmod +x /usr/local/bin/myjourney + myjourney # Run at least once with no argument so that it can install itself + myjourney self metadata # will print project name and version + +.. todo:: Reminder for self + + - merge this + +Docker Based +------------ + +The dockerfile is located at ``deploy/Dockerfile`` and there is no ``compose.yml`` file. The setup is a bit unorthodox and use `s6-overlay `_ to run everything needed for +the project in a single container. + +.. admonition:: s6-overlay + :class: note dropdown + + `s6 `_ is an `init `_ system, think like `sytstemd `_ and + ``s6-overlay`` is a set of tools and utilities to make it easier to run ``s6`` in a container environnment. A common linux tool peope often use in the django eocsytem that could serve as a replacement for example is `supervisord `_. + The ``deploy/etc/s6-overlay`` file contains the all the ``s6`` configuration, it will be copied in the the container in the ``/etc/s6-overlay`` directory. When the containers starts it will run a one-shot script (in the ``s6-overlay/scrips`` folder) + that will run the setup function in the ``__main__.py`` file, then two lon process wil be run, one for the gunicorn server and one for the django-q2 worker. + + + +There is the github action file at ``.github/workflows/cd.yml`` that will do the actual deployment. This action is run everytime a new `git tag is pushed to the repository `_. + +CapRover +******** + +Assuming you already have a caprover instance setup, all you have to do here is update you gihub repository with the correct credentials. +Here is an example of the content of the ``deploy-to-caprover`` job. + +.. literalinclude:: ../../../demo/.github/workflows/cd.yml + :lines: 9-21 + :language: yaml + +Checkout the the `action readme `_ for more informations, but there is essentially two things to do, add two secrets to your github repository: + +``CAPROVER_SERVER_URL``: The url of your caprover server, for example ``https://caprover.example.com`` +``CAPROVER_APP_TOKEN``: This can be generated on the ``deployment`` page of your caprover app, there should be an ``Enable App Token`` button. + +If you are deploying from a private repository, the is also `instructions `_ to allow caprover to pull from your private repository. + +And tha't basically it, if you are a caprover user, you know the rest of the drill. + +Other PAAS +********** + +If you using a PAAS solution that support docker, the first thing you need to do is update the ``.github/workflows/cd.yml`` action file to build your image and push it to a registry. +If you want the `github registry `_ your new job might look something like +this using `this action `_: + +.. code-block:: yaml + + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/username/projectname # CHANGE THIS + # generate Docker tags based on the following events/attributes + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Checkout Code Repository + uses: actions/checkout@v4 + + - name: Build and push + uses: docker/build-push-action@v6 + with: + push: true + context: . + file: deploy/Dockerfile + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + + + +I put up the action above based on these examples, yo can look them up to adjust the action to your needs: + +- https://docs.docker.com/build/ci/github-actions/push-multi-registries/ +- https://docs.docker.com/build/ci/github-actions/manage-tags-labels/ + +.. note:: + + You can also build the image locally with ``just build-docker-image`` and then push it manually on the registry of your choice. + + +At this point the process will be plateform dependent, but usually you should be abloe to specify the Image to pull from and that's should be it. +Eventually in the future I might add more specific guides for some of the most popular PAAS solutions. diff --git a/docs/the_cli/start_project/packages.rst b/docs/the_cli/start_project/packages.rst index 5641f3f..193a300 100644 --- a/docs/the_cli/start_project/packages.rst +++ b/docs/the_cli/start_project/packages.rst @@ -333,7 +333,7 @@ This file can essentially replace your ``manage.py`` file, but the ``manage.py`` The binary file that ``pyapp`` builds is a script that bootstraps itself the first time it is run, meaning it will create its own isolated virtual environment with **its own Python interpreter**. It installs the project (your falco project is setup as a python package) and its dependencies. When the binary is built, either via the provided GitHub Action or the ``just`` recipe / command, - you also get a wheel file (the standard format for Python packages). If you publish that wheel file on PyPI, you can use the binary's ``self update`` command to update itself. + you also get a wheel file (the standard format for Python packages). If you publish that wheel file on PyPI, you can use the binary's ``self update`` command to update the project. Let's assume you generated a project with the name ``myjourney``: diff --git a/justfile b/justfile index 4325e73..7be0e02 100644 --- a/justfile +++ b/justfile @@ -41,6 +41,7 @@ generate-demo *OVERWRITE: generate-docs-assets: generate-demo just tree cp demo/myjourney/myjourney/entries/urls.py docs/_static/snippets/urls.py + cp demo/myjourney/myjourney/settings.py docs/_static/snippets/settings.py # Generate project tree files tree: generate-demo diff --git a/pyproject.toml b/pyproject.toml index 45ece95..4d39d34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -126,7 +126,6 @@ path = "src/falco/__about__.py" exclude = [ "blueprints/*", "demo/*", - "demo/demo/*/migrations/*", "docs/*", ] lint.extend-ignore = [