All checks were successful
Test and deploy / Verify production build (push) Successful in 4m54s
Fast-forward the deployment checkout to the exact healthy image commit while refusing tracked changes, divergence, or an unexpected branch. Ignore release metadata stored beside the checkout.
136 lines
5.7 KiB
Markdown
136 lines
5.7 KiB
Markdown
# Deployment
|
|
|
|
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.
|
|
|
|
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 restarts the previous image without rebuilding it.
|
|
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`.
|
|
|
|
## Gitea Actions setup
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
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 repository secrets
|
|
|
|
Add these secrets under the repository's **Settings > Actions > Secrets**:
|
|
|
|
| Secret | Value |
|
|
| --- | --- |
|
|
| `DEPLOY_HOST` | Server hostname or IP address |
|
|
| `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 |
|
|
|
|
Generate the known-hosts value from a trusted machine and verify its fingerprint
|
|
before saving it:
|
|
|
|
```sh
|
|
ssh-keyscan -p 22 your-server.example.com
|
|
```
|
|
|
|
Do not copy an unverified key directly into the secret; doing so would remove
|
|
the protection against a man-in-the-middle attack.
|
|
|
|
## One-time server setup
|
|
|
|
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.
|
|
|
|
Create the deployment root and the external network once:
|
|
|
|
```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. Automatic
|
|
rollback begins with the first successful image-based release because 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.
|