chore: deploy once through Compose
This commit is contained in:
172
DEPLOYMENT.md
172
DEPLOYMENT.md
@@ -1,142 +1,72 @@
|
||||
# Deployment
|
||||
|
||||
For the portfolio builder's first installation, owner commands, persistent
|
||||
SQLite storage, and backup/restore, see [SELF_HOSTING.md](SELF_HOSTING.md).
|
||||
Set the public `SITE_URL` in `<DEPLOY_PATH>/runtime.env` before deployment.
|
||||
## Install or update
|
||||
|
||||
The Gitea Actions workflow in `.gitea/workflows/ci-cd.yml` verifies every pull
|
||||
request and every push to `main`. A successful push to `main` builds one
|
||||
verified Docker image on the shared host daemon, smoke-tests it, and activates
|
||||
that exact commit-tagged image over SSH without uploading source or rebuilding.
|
||||
On a server with Docker Engine and Compose v2, clone/pull the repository, copy
|
||||
`.env.example` to `.env`, set `SITE_URL`, and run:
|
||||
|
||||
Release manifests are stored in `<DEPLOY_PATH>/releases/<commit-sha>`. Each
|
||||
manifest records the immutable image tag and Gitea run number. The `current`
|
||||
symlink is changed only after the new container is healthy. If startup fails,
|
||||
the deployment script stops short of automatic rollback: startup may have
|
||||
migrated the database. Restore the pre-upgrade backup with the matching older
|
||||
image using the procedure in `SELF_HOSTING.md`.
|
||||
After health succeeds, the Git checkout at `<DEPLOY_PATH>` is fast-forwarded to
|
||||
the exact deployed commit. Deployment refuses to overwrite tracked server-side
|
||||
changes or a checkout that has diverged from `origin/main`.
|
||||
```sh
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
## Gitea Actions setup
|
||||
Create the owner separately when needed:
|
||||
|
||||
Enable **Repository Actions** in the repository settings and register the
|
||||
dedicated trusted runner maintained in
|
||||
`https://git.amiirkhl.ir/amiirkhl/portfolio-gitea-runner`. The
|
||||
workflow uses the `portfolio-ci` label. Its reusable job image provides Node.js
|
||||
22, Bash, Git, OpenSSH, Docker CLI, Buildx, and Compose. Runner capacity remains
|
||||
one because Gitea, builds, and production share a 2-core/4-GB server.
|
||||
```sh
|
||||
docker compose exec portfolio-app node maintenance.mjs create-user
|
||||
```
|
||||
|
||||
The runner and job container use the host Docker socket. Restrict the label to
|
||||
this trusted repository and disable Actions for fork pull requests. The workflow
|
||||
also skips pull requests whose head repository differs from this repository.
|
||||
`create-owner` and `reset-password` remain available. Startup never provisions
|
||||
an account. See [SELF_HOSTING.md](SELF_HOSTING.md) for private imports, persistent
|
||||
volumes, optional Caddy integration, backups, and the one-time upgrade steps.
|
||||
|
||||
After the first successful verification run, protect `main` in Gitea and make
|
||||
the **Verify production build** status check required. This prevents a pull
|
||||
request from merging when linting, type-checking, tests, the production build,
|
||||
or the dependency audit fails.
|
||||
## Gitea Actions
|
||||
|
||||
## Gitea repository secrets
|
||||
Keep the existing trusted `portfolio-ci` runner with Docker/Compose, Bash, Git,
|
||||
and OpenSSH. No dedicated Buildx builder, shared image daemon, release manifests,
|
||||
or release-directory symlinks are required. The deployment server needs Git,
|
||||
Bash, `flock`, Docker/Compose, access to pull this repository, and a checkout on
|
||||
`main` at `DEPLOY_PATH`. Configure its `.env` once, just as for manual deployment.
|
||||
|
||||
Add these secrets under the repository's **Settings > Actions > Secrets**:
|
||||
Keep these repository Actions secrets:
|
||||
|
||||
| Secret | Value |
|
||||
| --- | --- |
|
||||
| `DEPLOY_HOST` | Server hostname or IP address |
|
||||
| `DEPLOY_HOST` | Server hostname/IP |
|
||||
| `DEPLOY_USER` | SSH user allowed to run Docker |
|
||||
| `DEPLOY_PORT` | SSH port; optional, defaults to `22` |
|
||||
| `DEPLOY_PATH` | Absolute server directory, for example `/srv/portfolio` |
|
||||
| `DEPLOY_SSH_KEY` | Private SSH key for the deployment user |
|
||||
| `DEPLOY_KNOWN_HOSTS` | Pinned SSH host-key line for the server |
|
||||
| `DEPLOY_PORT` | Optional SSH port, default 22 |
|
||||
| `DEPLOY_PATH` | Absolute server checkout directory |
|
||||
| `DEPLOY_SSH_KEY` | Deployment SSH private key |
|
||||
| `DEPLOY_KNOWN_HOSTS` | Verified server SSH host-key entry |
|
||||
|
||||
Generate the known-hosts value from a trusted machine and verify its fingerprint
|
||||
before saving it:
|
||||
The server's checkout needs its own Git read access; the deployment SSH key is
|
||||
only for reaching the server. Protect the trusted runner from untrusted fork
|
||||
code. Fork pull requests do not run this workflow.
|
||||
|
||||
```sh
|
||||
ssh-keyscan -p 22 your-server.example.com
|
||||
```
|
||||
- **Pull request or manual workflow:** build the production image, including
|
||||
TypeScript validation. No browser tests, unit suites, lint, or audit jobs.
|
||||
- **Push to main:** SSH to the server, serialize deployments, fast-forward to
|
||||
the triggering commit, back up an existing running installation, then run
|
||||
`docker compose up --build -d --remove-orphans --wait --wait-timeout 60`.
|
||||
An obsolete queued commit is skipped. The image is tagged with its commit SHA.
|
||||
|
||||
Do not copy an unverified key directly into the secret; doing so would remove
|
||||
the protection against a man-in-the-middle attack.
|
||||
Each deployment builds once on the server, where Docker retains its dependency,
|
||||
native-library, and Next.js caches. `NPM_REGISTRY` in the server `.env` selects a
|
||||
mirror if needed. The optional Gitea variable of the same name configures PR
|
||||
builds. Avoid pruning build caches between deployments.
|
||||
|
||||
## One-time server setup
|
||||
## Timing and recovery
|
||||
|
||||
The server needs Docker Engine with the Compose v2 and Buildx plugins. The
|
||||
runner and deployment user must reach the same Docker daemon, and the deployment
|
||||
user must run `docker` without an interactive password prompt.
|
||||
The target is under 180 seconds for cached UI-change deployments. Cold builds,
|
||||
network transfers, native-library upgrades, and large backups may take longer.
|
||||
This change does not benchmark or enforce a wall-clock guarantee. Job timeouts
|
||||
allow a first build to finish rather than killing it at the routine-update target.
|
||||
|
||||
Create the deployment root and the external network once:
|
||||
Backups are retained under `backups/before-<commit>-<timestamp>` in the server
|
||||
checkout and are ignored by Git. They contain the database, uploads, and generated
|
||||
secret; preserve an externally configured secret separately. Copy backups to
|
||||
separate storage through your normal backup process.
|
||||
|
||||
```sh
|
||||
sudo mkdir -p /srv/portfolio/releases
|
||||
sudo chown -R deploy:deploy /srv/portfolio
|
||||
docker network create caddy_proxy
|
||||
```
|
||||
|
||||
Replace `deploy:deploy` and `/srv/portfolio` with the values used for
|
||||
`DEPLOY_USER` and `DEPLOY_PATH`. Add the public half of `DEPLOY_SSH_KEY` to the
|
||||
deployment user's `~/.ssh/authorized_keys`.
|
||||
|
||||
The standalone Next.js container joins `caddy_proxy` under the `portfolio-app`
|
||||
service name. Caddy must also join that network and proxy to
|
||||
`portfolio-app:3000`. No host port is published by this project.
|
||||
|
||||
A minimal Caddy route is:
|
||||
|
||||
```caddyfile
|
||||
portfolio.example.com {
|
||||
reverse_proxy portfolio-app:3000
|
||||
}
|
||||
```
|
||||
|
||||
Caddy forwards the browser's `Accept` header by default; keep that behavior so
|
||||
Next.js can negotiate WebP responses. Do not replace the cache headers emitted
|
||||
for `/_next/image`. The named `next-image-cache` volume persists `.next/cache`
|
||||
between container replacements so optimized variants can be reused.
|
||||
|
||||
The application expects immutable media URLs. When an admin or future upload
|
||||
workflow replaces an image, write it to a UUID or content-versioned URL instead
|
||||
of overwriting the previous object at the same URL. Next.js has no per-image
|
||||
optimizer-cache invalidation API.
|
||||
|
||||
## Manual deployment
|
||||
|
||||
For an emergency manual deployment from a checkout on the server:
|
||||
|
||||
```sh
|
||||
docker compose \
|
||||
--project-name portfolio \
|
||||
--file compose.yaml \
|
||||
--file compose.build.yaml \
|
||||
up --detach --build --remove-orphans
|
||||
```
|
||||
|
||||
This legacy path builds `amiirkhl-portfolio:latest` and is intentionally kept
|
||||
outside the normal CI deployment. Existing source-based release directories can
|
||||
still be started with their original Compose file during migration. Rollback is
|
||||
manual: verify schema compatibility or restore the pre-upgrade backup together
|
||||
with its matching application image. Older release directories do not contain
|
||||
`release.env` image metadata.
|
||||
|
||||
## Pipeline timing verification
|
||||
|
||||
After migrating the runner, record one cold run and three normal source-change
|
||||
runs. Expand the **Set up job** and **Complete job** sections in Gitea when
|
||||
capturing the results. For each run, record checkout, verification build, audit,
|
||||
runtime image creation, smoke test, deployment health wait, and completion time.
|
||||
All three warm runs should finish below 180 seconds without OOM events or
|
||||
production health failures. If they do not, collect `docker stats`, `free -h`,
|
||||
`df -h`, `docker system df`, `docker buildx du --builder portfolio-builder`, and
|
||||
the runner logs before changing or removing any quality gate.
|
||||
|
||||
Old versioned release directories are deliberately retained for rollback. They
|
||||
can be removed manually after a newer release has been proven stable; keep the
|
||||
directory referenced by the `current` symlink.
|
||||
|
||||
## Runtime verification
|
||||
|
||||
The production image contains `.next/standalone`, `.next/static`, and `public`,
|
||||
and runs the generated `server.js` with Node.js 22 on port 3000. CI smoke-tests
|
||||
the English and Persian routes, a built static asset, and `/_next/image` with
|
||||
WebP negotiation before deployment.
|
||||
If startup fails after a schema migration, do not simply downgrade the image.
|
||||
Restore the matching pre-upgrade backup and older image using the recovery
|
||||
instructions in SELF_HOSTING.md. A build failure leaves the running container
|
||||
intact; the checkout remains at the attempted commit so the deployment can be retried.
|
||||
|
||||
Reference in New Issue
Block a user