# Deployment

Picture of the Day runs on ordinary shared or VPS hosting: Python 3.10+, a writable disk, and a process that can speak HTTP. Docker is not required.

On **cPanel**, Phusion Passenger loads `passenger_wsgi.py`. FastAPI is ASGI, so that file wraps the app as WSGI. On a VPS you can still run Uvicorn directly.

## What production needs

- Python 3.10 or newer (cPanel → Setup Python App)
- Ability to install packages (`pip`) into the app virtualenv
- Writable directories for `data/` and `images/`
- A one-time Node build for `frontend/` (Node does **not** need to keep running)

## 1. Files on the server

Upload the project (without `.venv`, `node_modules`, and local `data/*.db` if you are starting empty). Prefer an application root **outside** `public_html` (for example `/home/USER/pictureoftheday`) so `.env`, SQLite, and originals are not web-accessible. If the app must live in a web root, keep the committed `.htaccess` deny rules.

Create `.env` from `.env.example`:

```
SECRET_KEY=<long random string>
ADMIN_USERNAME=yourname
ADMIN_PASSWORD=<strong password used only to create the first admin>
ADMIN_PATH=manage
DATABASE_URL=sqlite:///./data/picture.db
SITE_URL=https://example.com
HTTPS=true
TRUST_PROXY=true
TIMEZONE=America/New_York
MAX_UPLOAD_BYTES=20971520
IMAGE_QUALITY=82
```

Generate `SECRET_KEY`:

```bash
python -c "import secrets; print(secrets.token_urlsafe(48))"
```

Change `ADMIN_PATH` if you want a less obvious administration URL. Hiding the path is not authentication; the password still is.

After the first successful start, change the admin password in **Settings**. The environment password is used to create the initial user only.

Point the Python application URL at the **domain root or a subdomain** (`/`). Templates and scripts use site-root paths (`/gallery`, `/static/...`). A subdirectory URL such as `/picture` will not work without further changes.

## 2. cPanel (Passenger WSGI)

1. Software → **Setup Python App**.
2. Choose Python **3.10** or newer.
3. Application root: the project directory (where `passenger_wsgi.py` lives).
4. Application URL: `/` on the domain or subdomain that should serve the site.
5. Application startup file: `wsgi.py` (**not** `passenger_wsgi.py`).
6. Entry point / WSGI callable: `application`.

   Passenger always executes `passenger_wsgi.py`. cPanel rewrites it as a loader for the startup file. If the startup file is also `passenger_wsgi.py`, the loader imports itself. The real callable is `lp_wsgi.py` (lazy a2wsgi after Passenger forks; `SCRIPT_NAME="/"` cleared; trailing slash stripped). Never `from wsgi import application`. After Save, **Stop** then **Start**. `stderr.log` should show `potd wsgi: loading`. “It works! Python v3.x” means the cPanel stub is still loaded. A circular import on module `wsgi` means the wrong `wsgi.py` is on the server — re-upload `wsgi.py` and `lp_wsgi.py`. Keep `DirectorySlash Off` in `.htaccess`.
7. Create the app. cPanel writes a CloudLinux Passenger block into `.htaccess` in the URL directory. Do not delete that block.
8. Open the virtualenv it created (the UI shows a `source /home/USER/virtualenv/.../bin/activate` command) and install dependencies:

```bash
pip install -r requirements.txt
```

9. Create writable directories if they are missing:

```bash
mkdir -p data images/originals images/optimized images/thumbs images/display static/dist tmp
```

The application user must be able to write `data/` and `images/`. Originals in `images/originals/` must not be served as static files.

10. Place `.env` in the application root (same folder as `passenger_wsgi.py`).
11. Restart the Python app in the cPanel UI, or:

```bash
touch tmp/restart.txt
```

Passenger loads `passenger_wsgi.py`, which should load `wsgi.py` → `lp_wsgi.py`. That module changes into the application root, loads `.env`, and exposes FastAPI through `a2wsgi` on the first request.

If the first request is a 500, check the Passenger / stderr log from the Python App screen. Typical causes: missing `a2wsgi`, missing `.env`, or `data/` / `images/` not writable.

Rebuild after code changes: restart the Python app (or `touch tmp/restart.txt`).

## 3. Frontend build

On any machine with Node:

```bash
cd frontend
npm install
npm run build
```

This writes hashed-free files to `static/dist/main.css`, `static/dist/main.js`, and `static/dist/admin.js`. Copy that folder to the server if you built elsewhere. Production does not need a Node process.

## 4. Database

SQLite is the default. Paths are resolved from the project directory, not Passenger's working directory. On first start the application creates tables.

To apply the checked-in migration instead (useful when moving to PostgreSQL later):

```bash
cd /home/USER/pictureoftheday
source /home/USER/virtualenv/pictureoftheday/3.10/bin/activate
alembic upgrade head
```

PostgreSQL later:

```
DATABASE_URL=postgresql+psycopg://user:pass@localhost:5432/pictureoftheday
pip install "psycopg[binary]"
```

The SQLAlchemy models do not depend on SQLite-only types.

## 5. VPS / Uvicorn (optional)

From the project root so `backend` is importable:

```bash
uvicorn backend.app.main:app --host 127.0.0.1 --port 8000 --proxy-headers --forwarded-allow-ips='*'
```

Put HTTPS in front with Caddy, nginx, Apache, or the host’s reverse proxy. Set `HTTPS=true` and `SITE_URL` to the public `https://` origin so cookies and RSS/sitemap links are correct.

### systemd

```
[Service]
WorkingDirectory=/var/www/pictureoftheday
Environment=PATH=/var/www/pictureoftheday/.venv/bin
ExecStart=/var/www/pictureoftheday/.venv/bin/uvicorn backend.app.main:app --host 127.0.0.1 --port 8000 --proxy-headers
Restart=on-failure
```

### Apache reverse proxy (if you are not using Passenger)

```
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:8000/
ProxyPassReverse / http://127.0.0.1:8000/
```

Do **not** alias `/images/originals` as a public static directory. Only `/static` and the app’s `/media/{t|d|o}/…` routes should be public.

## 6. HTTPS

Terminate TLS at cPanel AutoSSL / Let’s Encrypt or the proxy. Set `HTTPS=true` so the session cookie is marked Secure. With `TRUST_PROXY=true` (the default), the app honors `X-Forwarded-Proto` from Apache.

The application uses `X-Forwarded-For` for login rate limiting; keep Apache/Passenger as the only public listener.

## 7. Scheduled tasks

Optional. The homepage selects today’s photograph lazily. To preselect it (or to recover if the site is idle across midnight), cPanel → Cron Jobs:

```
0 0 * * * cd /home/USER/pictureoftheday && /home/USER/virtualenv/pictureoftheday/3.10/bin/python -m scripts.select_potd
```

Adjust the virtualenv path to the one shown in Setup Python App.

## 8. After deploy checklist

- Visit `/` and confirm the hero image loads
- Visit `/rss.xml` in a reader or browser
- Sign in at `/<ADMIN_PATH>`
- Upload a test photograph and confirm the public URL does not contain the original filename
- Confirm `/<ADMIN_PATH>` is not linked from the public header
- Change the default admin password
- Confirm `https://your-domain/.env` is not downloadable

See [BACKUP.md](BACKUP.md) for restore.
