MAD Study Hub

Home / MAD1 / Week 12

Week 12 · Deployment: Git, WSGI, servers and the cloud

Official syllabus line: Application Deployment
About 90 minEnd Term material (see exam radar)BSCS2003 (+ project BSCS2003P) · Thejesh G N, Nitin Chandrachoodan
Before you start: MAD1 weeks 4 to 11 (a working Flask app with a database and tests). Comfort with a terminal.

The one-minute story

Think of it like this

Deployment is opening your shop to the public. Until now your app was a dish cooked in your own kitchen (flask run on your laptop) and tasted only by you. Deploying means moving it into a proper restaurant: a real kitchen that can serve many customers at once (a production server like gunicorn), a front desk that greets customers and sends them to the right counter (nginx), a lock on the door (HTTPS), a recipe book so anyone can cook the same dish (requirements, Dockerfile), a log book (logging), and a safe for the money (backups).

And Git is the notebook where every change to the recipe is written down, with the date and the name of the cook, so you can go back to any earlier version and so that ten cooks can work on one recipe without erasing each other.

By the end of this week you can:

  • use the Git workflow (init, add, commit, branch, merge, push, pull, clone) and predict what each command does;
  • rebuild an environment from requirements.txt and keep secrets in environment variables;
  • explain why flask run is not for production and how WSGI, gunicorn and nginx fit together;
  • read a simple Dockerfile and a CI file, and tell PaaS, IaaS and SaaS apart;
  • change a database model safely with Flask-Migrate, and prepare a deployment checklist and a README for your project.

Words you need

Words you need
  • Version control - a system that records every change to your files so you can go back and collaborate.
  • Repository (repo) - a folder tracked by Git, with its history in a hidden .git folder.
  • Commit - one saved snapshot of the project with a message.
  • Branch - a movable pointer to a commit; a separate line of work.
  • Remote - a copy of the repo on a server (GitHub); the usual name is origin.
  • Virtual environment - an isolated folder of Python packages for one project.
  • WSGI - the standard interface between a Python web app and a web server.
  • gunicorn - a production WSGI server for Python apps.
  • Reverse proxy - a server (nginx) that receives requests and forwards them to your app.
  • Container - a lightweight, isolated box that runs one program with all it needs.
  • Docker image - the read-only recipe/template from which containers are started.
  • CI/CD - continuous integration and continuous delivery or deployment: automatic test and release on every push.
  • Migration - a versioned script that changes the database structure.

Idea 1: Git and GitHub

Git is a distributed version control system: every developer has a full copy of the whole history on their own machine, so Git works offline for everything except talking to a remote. GitHub (and GitLab, Bitbucket) is a website that hosts Git repositories and adds pull requests, issues and so on. Git and GitHub are not the same thing.

Three places a file can be:

  1. Working directory: your files on disk.
  2. Staging area (index): the list of changes chosen for the next commit (git add).
  3. Repository: the saved commits (git commit).

A file can be in both the working directory and the staging area at the same time (that is just "staged"). git add moves a change into the staging area; it does not send anything to GitHub.

git config --global user.name  "Asha"                 # one-time identity
git config --global user.email "asha@example.com"     # exact command to change your e-mail
git init                    # turn this folder into a repo (starts on branch main or master)
git status                  # what changed, what is staged
git add app.py              # stage one file   (git add .  stages everything)
git commit -m "Add login"   # save the staged snapshot
git log --oneline           # history, one line per commit
git diff                    # unstaged changes (git diff --staged: staged ones)

Real run in a fresh folder with app.py, .gitignore and .env: git status -s first shows ?? .env, ?? .gitignore, ?? app.py (untracked). After git add app.py it shows A app.py (staged). After git commit -m first, git log --oneline shows one line: <id> first.

The idea

The daily loop: git status then git add then git commit -m "message" then git pull then git push.

  • init creates a repo; clone copies an existing remote repo to your machine (and sets origin).
  • add stages; commit saves; push uploads your commits to the remote; pull downloads the remote's new commits and merges them into your current branch (fetch + merge).
  • git remote add origin <url> links your repo to GitHub; git push -u origin main pushes and remembers the link.

Branches and merging

A branch is only a pointer to a commit; creating one is nearly free. You work on a feature branch, then merge it into main.

git branch                  # list branches (* marks the current one)
git branch feature1         # CREATE a branch (does not switch)
git checkout feature1       # switch to it   (newer: git switch feature1)
git checkout -b feature2    # create AND switch in one step (newer: git switch -c feature2)
# ... edit, add, commit on the branch ...
git checkout main           # go back to main
git merge feature1          # merge feature1 INTO the branch you are standing on (main)
git branch -d feature1      # delete the merged branch

Real run: after git switch -c feature1, one commit there, and back on main, git branch printed feature1 and * main; git merge feature1 printed Fast-forward (main had no new commits of its own, so Git just moved the pointer). The log then had 2 commits.

git merge X merges X into the current branch, not into main and not into "master". So to merge feature1 and feature2 into main:

git checkout main
git merge feature1
git merge feature2

(Choosing the commands where you first git checkout feature1 and then merge feature2 is wrong: you would merge into feature1.)

Merge conflicts. If both branches changed the same lines, Git stops and marks the file:

<<<<<<< HEAD
B
=======
A
>>>>>>> a

(Real output from a test where main wrote B and branch a wrote A in x.txt: Git said CONFLICT (add/add) and Automatic merge failed.) You edit the file to keep what you want, remove the markers, then git add x.txt and git commit.

Push rejected. If a teammate pushed first and you did not pull, your git push is rejected (! [rejected] main -> main (fetch first); real run). You must git pull (merge their work), fix conflicts if any, then push. The rule is "pull before you push".

Solved example: 1. Predict the Git workflow

Question. You work on a team project and run, in order: git status, git add ., git commit -m "Fixed header bug", git push, git pull. A teammate pushed a change just before your push. What is wrong?

Solution. The push is refused because your local branch is behind the remote. The correct habit is git pull before git push (the real exam answer: "the pull should have been run before the push to avoid possible conflicts"). git status does not show remote conflicts, and push never merges automatically.

Other commands the papers ask about

CommandMeaning
git rm --cached fileremove from the staging area / tracking but keep the file on disk
git rm fileremove from tracking and delete the file
git deldoes not exist (a favourite wrong option)
git restore filethrow away unstaged edits in a file
git stashput uncommitted work aside for a moment
git branch namecreate a branch
git checkout -b namecreate and switch
git config --global user.email "..."set the e-mail used in commits

The first branch of a new repo is called main (new default) or master (older Git); both answers are accepted in the papers.

.gitignore is a text file listing files Git must not track: secrets (.env), virtual environments (venv/), caches (__pycache__/), local databases (*.db), uploads.

venv/
__pycache__/
*.pyc
.env
*.db
instance/

A pull request (PR) is a GitHub request to merge your branch into another (usually main): teammates review the code, CI runs the tests, and then someone clicks Merge. Never commit passwords or API keys; if you did, change them, because Git history remembers.

Watch out - real exam trap
  • git add is not "send to GitHub" (that is git push). Staging area is local.
  • Git is distributed, not centralised; it works offline. Git is not GitHub.
  • git merge X merges X into the branch you are on. Check the current branch first (* in git branch).
  • git rm --cached keeps the file on disk. git rm deletes it.
  • Branches are pointers to commits, not copies of the whole project.
  • git checkout -b name both creates and switches.

Idea 2: Virtual environments, requirements and configuration

Different projects need different package versions. A virtual environment (venv) gives each project its own packages.

python -m venv venv                 # create (folder named venv)
source venv/bin/activate            # Linux/macOS      (Windows: venv\Scripts\activate)
pip install flask flask-sqlalchemy gunicorn
pip freeze > requirements.txt       # write exact installed versions to a file
pip install -r requirements.txt     # on another machine: rebuild the same set
deactivate

pip freeze lists the installed packages as name==version, which is what goes into requirements.txt (a question asks which command lists the packages installed from requirements.txt in the venv: pip freeze). Pin versions (Flask==3.0.3) so the server runs what you tested. Do not commit the venv/ folder; commit requirements.txt.

Configuration and secrets in environment variables. The same code runs on your laptop and on the server, but the settings differ (secret key, database URL, debug flag). Do not hard-code them; read them from the environment.

import os
from flask import Flask

app = Flask(__name__)
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "dev-only")
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("DATABASE_URL", "sqlite:///app.db")
app.config["DEBUG"] = os.environ.get("FLASK_DEBUG") == "1"
print(app.config["SECRET_KEY"], app.config["DEBUG"])

Real runs: python cfg.py prints dev-only False; SECRET_KEY=abc FLASK_DEBUG=1 python cfg.py prints abc True. On the server you set the real values in the environment (or a .env file that is in .gitignore, loaded with python-dotenv). One code base, different config per environment: development, testing, production.

Idea 3: Why not flask run in production? WSGI, gunicorn, nginx

flask run (or app.run()) starts Flask's built-in development server. It is great for coding, but it is not meant for real traffic: it is designed for one developer, handles few requests at a time, is not hardened for security, and with debug mode on can expose an interactive debugger that lets anyone run code. Never run debug=True in production.

WSGI (Web Server Gateway Interface) is the agreed interface: a web server calls a Python callable with the request information and gets back a response. A Flask app object is such a callable. Here is the whole idea in a few lines (real run: the client got b'Hello from /hi'):

def application(environ, start_response):
    body = ("Hello from " + environ["PATH_INFO"]).encode()
    start_response("200 OK", [("Content-Type", "text/plain"),
                              ("Content-Length", str(len(body)))])
    return [body]

Because Flask, Django and others all speak WSGI, any WSGI server can run any of them. gunicorn (Linux and macOS) and uWSGI are production WSGI servers: they start several worker processes, restart crashed workers and handle many connections.

pip install gunicorn
gunicorn -w 4 -b 127.0.0.1:8000 app:app     # 4 workers; module "app", variable "app"

app:app means "in the file app.py, use the object called app". If you use an application factory: gunicorn "app:create_app()".

Reverse proxy. In front of gunicorn we put a fast web server, nginx (or Apache), which: terminates HTTPS, serves static files directly (CSS, JS, images) without bothering Python, forwards other requests to gunicorn, and can balance load between several app servers.

server {
    listen 80;
    server_name example.com;

    location /static/ {
        alias /srv/myapp/static/;          # nginx serves these itself
    }
    location / {
        proxy_pass http://127.0.0.1:8000;  # everything else goes to gunicorn
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
}

The path of one request: browser, nginx (port 80/443), gunicorn (port 8000), Flask app, database, and back.

Nginx vs Apache (a real MSQ): both are open source. Apache is versatile and has many modules and dynamic per-directory configuration. Nginx is generally faster for static files and high traffic with low memory use (it is event-driven). So "Apache is better for a large volume of concurrent connections than Nginx" is the false statement.

HTTPS. HTTP sends text anyone on the network can read. HTTPS = HTTP over TLS: the traffic is encrypted and the server proves its identity with a certificate issued by a certificate authority. Let's Encrypt gives free certificates (the certbot tool installs and renews them). Browsers show a padlock; cookies marked Secure are sent only over HTTPS. Many features (service workers, geolocation) require HTTPS.

The idea

Production request path. Internet then nginx (HTTPS, static files, proxy) then gunicorn (WSGI server with workers) then Flask (your code) then database. The dev server replaces the middle two layers with one weak one.

Idea 4: Docker, cloud and CI/CD

Why Docker? "It works on my machine" fails when the server has a different Python or missing libraries. A Docker image packages your app with its exact Python, libraries and settings. A container is a running instance of an image: isolated, lightweight (it shares the host's kernel, unlike a full virtual machine), and starts in seconds.

# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:app"]

Line by line: start from an official Python image; work in /app; copy requirements.txt first and install (Docker caches this layer so rebuilds are fast); copy the rest of the code; document port 8000; the default command is gunicorn bound to 0.0.0.0 (all interfaces, needed inside a container).

docker build -t myapp .                      # build an image named myapp
docker run -p 8000:8000 myapp                # run a container; host port 8000 to container port 8000
docker ps                                    # list running containers

docker-compose describes several containers that run together in one YAML file, for example the web app, a database and nginx:

services:
  web:
    build: .
    ports: ["8000:8000"]
    environment:
      DATABASE_URL: postgresql://user:pass@db/app
    depends_on: [db]
  db:
    image: postgres:16
    volumes: ["dbdata:/var/lib/postgresql/data"]
volumes:
  dbdata:

docker compose up starts everything; a volume keeps database files when the container is deleted (container storage is temporary).

Cloud service models

ModelYou manageThe provider managesExample
IaaS (infrastructure)OS, runtime, app, dataservers, storage, networka rented virtual machine (AWS EC2)
PaaS (platform)your app and data onlyOS, runtime, scalingRender, Heroku, PythonAnywhere: "give me the code"
SaaS (software)nothing; you just use iteverythingGmail, Google Docs

Cloud means renting computing by the hour instead of buying machines; benefits: scalability, pay-as-you-use, reliability. For a student project, PaaS is the easiest.

CI/CD. Continuous integration (CI): every push is automatically built and tested, and the result reported, so errors are found early. Continuous delivery: after the tests pass, a release package is produced automatically and is ready to deploy. Continuous deployment: every passing change is automatically deployed into production. A small GitHub Actions file:

# .github/workflows/test.yml
name: tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: pytest

On every push GitHub starts a fresh machine, installs the requirements and runs your pytest tests from week 10; a red cross on the commit means a test failed.

Idea 5: Migrations, logging, backups

Database migrations. Earlier you used db.create_all(). It creates missing tables but never changes an existing one. If you add a column to a model, the old database file does not get it. Migrations are numbered scripts that evolve the schema step by step, keeping the data. The tool is Alembic; Flask-Migrate wraps it for Flask.

# app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
db = SQLAlchemy(app)
migrate = Migrate(app, db)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False)
flask db init                       # once: creates the migrations/ folder
flask db migrate -m "create user"   # compare models with the database, write a script
flask db upgrade                    # APPLY the script to the database
# later you add:   email = db.Column(db.String(120))   to User
flask db migrate -m "add email"     # new script: Detected added column 'user.email'
flask db upgrade                    # apply it
flask db downgrade                  # undo the latest one
flask db current                    # which revision is the database at

All of this was run for real. The second migration's script contains upgrade() with add_column('email', ...) and downgrade() with drop_column('email'). After the second upgrade flask db current printed 38103fadeb9c (head); after downgrade it showed the first revision again. Remember: migrate = generate script, upgrade = apply it (a very common confusion). Always read the generated script (autogenerate can miss renames, and SQLite cannot drop or alter columns directly, so Alembic uses its batch mode). Commit migrations/ to Git; on the server run flask db upgrade at each deployment.

Logging. In production nobody sits at the terminal, so write logs to a file or to the platform's log viewer. Use the logging module rather than print:

import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger(__name__)

log.info("user %s logged in", "asha")
log.warning("disk almost full")
log.error("payment failed")

Levels in order: DEBUG, INFO, WARNING, ERROR, CRITICAL; setting INFO hides DEBUG. Never log passwords or tokens. Monitoring means watching health: is the site up (uptime check), how slow are the responses, how many errors; tools send an alert when something breaks (for example Sentry for errors, UptimeRobot for uptime). Gunicorn also writes an access log and an error log.

Backups. Servers die, disks fail, people run a wrong DELETE. Keep regular, automatic, off-site copies of the database and of uploaded files, and test a restore at least once, because a backup you cannot restore is not a backup. For SQLite you can copy the file when the app is idle, or use sqlite3 app.db ".backup backup.db".

Solved example: 2. Add a column the right way

Question. Your live app has a User table with data. You add phone = db.Column(db.String(15)) to the model. What do you do, in order?

Solution. (1) Stop thinking about create_all(): it will not alter the existing table. (2) flask db migrate -m "add phone" and open the new script to check that it only adds the column. (3) flask db upgrade on your laptop, run the tests. (4) Commit the script. (5) On the server: pull, pip install -r requirements.txt, flask db upgrade, restart gunicorn. (6) If something goes wrong: flask db downgrade, restore from backup.

Solved example: 3. Read a deployment

Question. What happens when the command gunicorn -w 2 -b 127.0.0.1:8000 app:app is running behind the nginx config of Idea 3, and a browser requests /static/logo.png and then /login?

Solution. /static/logo.png matches location /static/, so nginx returns the file from disk and gunicorn never sees it. /login matches location /, so nginx forwards it to 127.0.0.1:8000; one of the two gunicorn workers runs the Flask view and sends the response back via nginx to the browser.

In your project

For your project viva: deployment checklist and README. Before you submit: (1) requirements.txt is complete and pinned; (2) .gitignore excludes venv/, .env, *.db, __pycache__/; (3) no secret or DEBUG=True in committed code, config read from environment variables; (4) tests pass (pytest) and migrations are committed; (5) the app runs with gunicorn, not flask run; (6) static files are served by nginx or the platform; (7) HTTPS is on; (8) logging is configured; (9) there is a database backup plan; (10) the repository has a clean commit history and a README.

README essentials: project name and one-line purpose; features; technology used (Flask, SQLAlchemy, ...); how to set up (clone, create venv, pip install -r requirements.txt, set environment variables, flask db upgrade); how to run and how to run the tests; default login for the demo; folder structure; screenshots; author and date. An examiner often tests whether a stranger could run the project in 5 minutes using only your README.

Your turn

Your turn: 1. Merge the branches

git branch shows feature1, * feature2, main. Write the commands to put both features into main.

Need a hint?

Merging goes into the current branch.

Show the full solution
git checkout main
git merge feature1
git merge feature2

You must first switch to main; the * shows you are on feature2, so merging there would be wrong.

Your turn: 2. Git true or false

(a) git add moves the local repo to the remote. (b) git init turns a folder into a repo. (c) Files in the staging area can be committed. (d) Git can be used only online. (e) Branches are pointers to commits.

Need a hint?

Which actions touch the network?

Show the full solution

(a) False (that is git push). (b) True. (c) True. (d) False; only push, pull, fetch and clone need a network. (e) True.

Your turn: 3. Stop tracking a file

You committed secrets.env by mistake but want to keep it on your disk. What do you do?

Need a hint?

Remove it from tracking only.

Show the full solution

git rm --cached secrets.env, add secrets.env to .gitignore, then git commit -m "stop tracking secrets". Also change the secret, because the old commits still contain it.

Your turn: 4. Predict the config
import os
print(os.environ.get("MODE", "dev"))

What does it print when run as MODE=prod python x.py, and when run as plain python x.py?

Need a hint?

The second argument is a default.

Show the full solution

prod in the first case, dev in the second (the default is used when the variable is missing).

Your turn: 5. IaaS, PaaS or SaaS

(a) You rent a virtual machine and install Python yourself. (b) You push code to Render and it runs it. (c) You use Google Docs.

Need a hint?

Who manages the operating system?

Show the full solution

(a) IaaS. (b) PaaS. (c) SaaS.

Exam radar

Exam radar

The keyword match for this week finds about 18 distinct graded questions, nearly all in End Term papers (about 45 End Term keyword hits, about 1 in Quiz 2 and none in Quiz 1). That is a small sample and a coarse count (one hit was a stray HTML list question), so do not read too much into it. What it shows:

  • Git commands and workflow (about 12 of the 18): merge two branches into main (the same question appears in three papers), git rm --cached, git checkout -b, git branch, git config --global user.email, "git is centralised" (false), "Git and GitHub are the same" (false), the default branch name (main or master), "the pull should come before the push".
  • Continuous integration / delivery / deployment statements (2 questions; both statements correct).
  • Nginx vs Apache (1 MSQ).
  • pip freeze to list the packages in the virtual environment (1 question).
  • No past question was matched on Docker, gunicorn, WSGI, migrations, HTTPS or cloud models in this sample, so treat those as viva and project topics; they are still in the week's scope.
Watch out - real exam trap
  • The order for merging into main: checkout main first, then merge each feature.
  • git del is not a command. git rm --cached removes from staging only.
  • Git is distributed; GitHub is a hosting service.
  • git checkout -b name = create and switch.
  • CI = automatically build and test on each change; continuous deployment = automatic release to production.
  • Apache is versatile with modules; nginx is the fast, low-memory one for static files and many connections.

Quick quiz

Quick quiz

You are on main and want the work of feature1 and feature2 in main. Which sequence is correct?

  • git merge main then git merge feature1 then git merge feature2
  • git checkout feature1 then git merge feature2 then git merge main
  • git checkout main then git merge feature1 then git merge feature2
  • git push feature1 then git push feature2
git merge X merges X into the current branch, so be on main first.

Which command removes a file from the staging area but keeps it in the directory?

  • git rm file
  • git del file
  • git rm --cached file
  • git del --cached file
--cached limits git rm to the index; git del is not a Git command.

Select all TRUE statements about Git. (select all that apply)

  • Files in the staging area can be committed.
  • git init converts a local folder into a Git repository.
  • git add moves the local repository to the remote repository.
  • Git can be used only online.
git add only stages. Git works offline; only push, pull and clone need the network.

Why should you not deploy a Flask app with app.run(debug=True)?

  • The development server is not built for real traffic and debug mode can expose an interactive debugger.
  • Flask cannot run on Linux.
  • It makes the database slower.
  • HTTPS cannot be used with Flask.
Use a WSGI server such as gunicorn behind nginx, with debug off.

In a Flask-Migrate workflow, which command actually changes the database structure?

  • flask db init
  • flask db migrate
  • flask db upgrade
  • flask db current
migrate only writes the migration script; upgrade applies it.

Which statements are correct? (select all that apply)

  • Apache is versatile and supports many modules.
  • Nginx is generally faster for static files with low memory use.
  • Both Apache and Nginx are open-source web servers.
  • Apache is always better than Nginx for a very large number of concurrent connections.
Nginx's event-driven design is the usual choice for many connections and static files.

Viva: say it out loud

Viva: say it out loud
How did you use Git in your project?

I made a repository with git init, committed in small steps with clear messages, and pushed to GitHub. I worked on branches for features and merged them into main, and I kept secrets and the virtual environment out with a .gitignore.

What is the difference between Git and GitHub?

Git is the version control tool that runs on my computer and keeps the history. GitHub is a website that hosts Git repositories so that a team can share them, review pull requests and run automation.

What happens if two people change the same line and merge?

Git cannot decide, so it reports a merge conflict and puts markers in the file. I open the file, keep the right version, remove the markers, then add and commit the result. Pulling often keeps conflicts small.

Why is the Flask development server not used in production, and what do you use?

It is built for one developer, handles little traffic and is not hardened. In production I run the app with a WSGI server like gunicorn with several workers, behind nginx, which serves static files and handles HTTPS.

What is WSGI?

It is the standard interface between a Python web application and a web server. The server calls the app as a function with the request details and gets a response back, so any WSGI server can run any WSGI framework.

What is Docker and why use it?

Docker packages my app with the exact Python version and libraries into an image, and a container is a running copy of it. So it behaves the same on my laptop and on the server. A Dockerfile is the recipe for the image and docker-compose runs several containers together.

How do you change the database structure of a live app?

I change the model, run flask db migrate to generate a migration script, check it, and apply it with flask db upgrade. The script is version controlled, I can undo with flask db downgrade, and I take a backup before upgrading production.

How do you keep secrets such as the secret key out of your code?

I read them from environment variables with os.environ, set them on the server, and keep any .env file in .gitignore, so they never reach GitHub.

Recap

Remember this
  • Git is distributed version control: working directory, staging area, repository. Loop: status, add, commit, pull, push. GitHub is only a host.
  • Branch = pointer to a commit. git checkout -b creates and switches; git merge X merges X into the current branch; conflicts are fixed by hand then committed. Pull before push.
  • venv isolates packages; pip freeze > requirements.txt and pip install -r requirements.txt recreate them. Secrets and settings come from environment variables, never from Git.
  • flask run is a dev server only. Production: nginx (HTTPS, static files, proxy) in front of gunicorn (WSGI, workers) running Flask.
  • Docker: Dockerfile builds an image; a container runs it; docker-compose runs several together. IaaS / PaaS / SaaS differ in how much you manage.
  • CI tests every push; continuous delivery prepares the release; continuous deployment ships it.
  • Migrations: flask db init, migrate (write script), upgrade (apply), downgrade (undo).
  • Logs, monitoring and tested backups; deployment checklist and README for the project.
Code and facts to keep
  • Git setup: git config --global user.name "N", git config --global user.email "e"
  • Git basics: git init, git status, git add ., git commit -m "msg", git log --oneline, git diff
  • Branches: git branch, git branch x, git checkout x or git switch x, git checkout -b x, git merge x, git branch -d x
  • Remote: git clone URL, git remote add origin URL, git push -u origin main, git pull
  • Untrack but keep: git rm --cached file; ignore: .gitignore
  • Env: python -m venv venv, pip freeze > requirements.txt, pip install -r requirements.txt
  • Config: os.environ.get("NAME", "default")
  • Serve: gunicorn -w 4 -b 127.0.0.1:8000 app:app; nginx proxy_pass http://127.0.0.1:8000;
  • Docker: docker build -t myapp ., docker run -p 8000:8000 myapp, docker compose up
  • Migrate: flask db init, flask db migrate -m "msg", flask db upgrade, flask db downgrade
  • Logging levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
Connected weeks (learn them together)

Practise with real exam questions

These come straight from past papers (with the real figures and the official answer key). Open 33 real questions on this week   All End Term papers