Skip to content

Commit 52f221e

Browse files
webplodeOmX
andcommitted
Add production deployment bundle
A versioned devops bundle documents and runs the openapi.io.vn CPAB production stack with Postgres, CPAB, and nginx exposing only the public /v1 API path. Constraint: Keep the origin HTTP-only behind Cloudflare or an upstream TLS terminator. Confidence: high Scope-risk: narrow Tested: POSTGRES_PASSWORD=dummy-password JWT_SECRET=dummy-jwt-secret docker compose -f devops/docker-compose.production.yml config Tested: git diff --cached --check Co-authored-by: OmX <omx@oh-my-codex.dev>
1 parent 059b0b5 commit 52f221e

3 files changed

Lines changed: 279 additions & 0 deletions

File tree

devops/README.md

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# CPAB Production VPS Deployment
2+
3+
This deployment runs CPAB behind nginx on `openapi.io.vn`.
4+
5+
- Public API: `https://openapi.io.vn/v1/*`
6+
- No dashboard/admin/user routes are exposed by this nginx deployment.
7+
- Public VPS port: `80`
8+
- Private Docker-only CPAB port: `8318`
9+
- Cloudflare or the upstream nginx terminates public HTTPS and connects to this origin over HTTP.
10+
11+
## 1. VPS Prerequisites
12+
13+
Install Docker Engine and the Docker Compose plugin on the VPS.
14+
15+
Open inbound TCP port `80` on the VPS firewall/security group. Do not expose port `8318`; it is only used inside the Docker network.
16+
17+
## 2. Copy Files To The VPS
18+
19+
From this repository root on your local machine:
20+
21+
```bash
22+
SSH_PORT=<your-vps-ssh-port>
23+
ssh -p "$SSH_PORT" <ssh-user>@<vps-ip> 'sudo mkdir -p /opt/cpab && sudo chown "$USER:$USER" /opt/cpab'
24+
rsync -az -e "ssh -p $SSH_PORT" devops/ <ssh-user>@<vps-ip>:/opt/cpab/
25+
```
26+
27+
If you prefer `scp`, its SSH port flag is uppercase `-P`:
28+
29+
```bash
30+
SSH_PORT=<your-vps-ssh-port>
31+
scp -P "$SSH_PORT" -r devops/* <ssh-user>@<vps-ip>:/opt/cpab/
32+
```
33+
34+
On the VPS:
35+
36+
```bash
37+
cd /opt/cpab
38+
```
39+
40+
The compose file expects this layout:
41+
42+
```text
43+
/opt/cpab/
44+
|-- docker-compose.production.yml
45+
`-- nginx/
46+
`-- openapi.io.vn.conf
47+
```
48+
49+
## 3. Create The Runtime Environment
50+
51+
Create `/opt/cpab/.env` on the VPS:
52+
53+
```bash
54+
POSTGRES_USER=cpab
55+
POSTGRES_PASSWORD=replace-with-a-strong-database-password
56+
POSTGRES_DB=cpab
57+
JWT_SECRET=replace-with-output-of-openssl-rand-hex-32
58+
JWT_EXPIRY=720h
59+
CORS_ORIGINS=https://openapi.io.vn
60+
HTTP_PORT=80
61+
```
62+
63+
Generate a JWT secret if needed:
64+
65+
```bash
66+
openssl rand -hex 32
67+
```
68+
69+
## 4. Configure Cloudflare DNS
70+
71+
Create an `A` record:
72+
73+
```text
74+
Name: openapi
75+
Target: <vps-public-ip>
76+
Proxy status: Proxied
77+
```
78+
79+
The inner nginx config does not enforce Cloudflare source IP checks. If there is an upstream/front nginx, point it at this service over HTTP and avoid HTTP-to-HTTPS redirects for this origin path.
80+
81+
## 5. Configure Cloudflare TLS
82+
83+
Cloudflare automatically issues and renews the public edge certificate for browser/client traffic to `https://openapi.io.vn`.
84+
85+
This deployment listens on origin port `80` only, so the layer directly in front of this service must connect over HTTP. If Cloudflare connects directly to this service, use Cloudflare SSL/TLS mode `Flexible` for this hostname/zone, or another Cloudflare rule that makes the origin request use HTTP.
86+
87+
Do not use `Full` or `Full (strict)` while this origin only listens on port `80`; those modes make Cloudflare try HTTPS to the origin.
88+
89+
No origin certificate files are needed for this HTTP-only deployment.
90+
91+
## 6. Configure Cloudflare Access
92+
93+
The nginx config only exposes `/v1` and `/v1/*`. Everything else returns `404` before reaching CPAB.
94+
95+
If you still create a Cloudflare Zero Trust application, configure a public bypass for the API path:
96+
97+
```text
98+
Domain: openapi.io.vn
99+
Path: /v1*
100+
Policy: Bypass, Include Everyone
101+
```
102+
103+
Do not add a protected `/*` Access application for this hostname unless you also change nginx to expose those dashboard/admin/user paths. With the current nginx config, non-`/v1` paths are intentionally unavailable.
104+
105+
## 7. Start The Stack
106+
107+
On the VPS:
108+
109+
```bash
110+
cd /opt/cpab
111+
docker compose -f docker-compose.production.yml pull
112+
docker compose -f docker-compose.production.yml up -d
113+
```
114+
115+
Check container status:
116+
117+
```bash
118+
docker compose -f docker-compose.production.yml ps
119+
```
120+
121+
Check nginx syntax after any config change:
122+
123+
```bash
124+
docker compose -f docker-compose.production.yml exec nginx nginx -t
125+
```
126+
127+
Reload nginx without restarting the whole stack:
128+
129+
```bash
130+
docker compose -f docker-compose.production.yml exec nginx nginx -s reload
131+
```
132+
133+
## 8. Smoke Test
134+
135+
From your local machine:
136+
137+
```bash
138+
curl -I https://openapi.io.vn/v1/models
139+
curl -I https://openapi.io.vn/dashboard
140+
```
141+
142+
Expected behavior:
143+
144+
- `/v1/*` reaches CPAB publicly, subject to CPAB API key authentication for protected API operations.
145+
- `/dashboard`, `/user`, `/admin`, `/v0/*`, `/v1beta/*`, and other non-`/v1` paths return `404`.
146+
147+
## 9. Updating
148+
149+
To pull the latest `abwebplode/cpab:development` image and restart:
150+
151+
```bash
152+
cd /opt/cpab
153+
docker compose -f docker-compose.production.yml pull cpab
154+
docker compose -f docker-compose.production.yml up -d cpab
155+
```
156+
157+
## Cloudflare SSL Automation Answer
158+
159+
Yes, Cloudflare automatically issues the public edge SSL certificate for `openapi.io.vn` when the DNS record is active on Cloudflare. That covers client-to-Cloudflare HTTPS.
160+
161+
This HTTP-only origin deployment does not need Cloudflare to install any certificate inside nginx, because nginx no longer listens on `443`. Cloudflare serves HTTPS publicly and connects to the VPS over HTTP port `80`.
162+
163+
If you later want encrypted Cloudflare-to-origin traffic again, re-enable nginx `443`, mount an origin certificate, and switch Cloudflare back to `Full (strict)`.
164+
165+
## References
166+
167+
- Cloudflare Universal SSL: https://developers.cloudflare.com/ssl/edge-certificates/universal-ssl/
168+
- Cloudflare Flexible mode: https://developers.cloudflare.com/ssl/origin-configuration/ssl-modes/flexible/
169+
- Cloudflare Access application paths: https://developers.cloudflare.com/cloudflare-one/access-controls/policies/app-paths/
170+
- Cloudflare Access policies: https://developers.cloudflare.com/cloudflare-one/access-controls/policies/
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
name: cpab-production
2+
3+
services:
4+
postgres:
5+
image: postgres:17
6+
container_name: cpab-postgres
7+
restart: unless-stopped
8+
environment:
9+
POSTGRES_USER: ${POSTGRES_USER:-cpab}
10+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in the environment or a .env file}
11+
POSTGRES_DB: ${POSTGRES_DB:-cpab}
12+
volumes:
13+
- cpab-postgres-data:/var/lib/postgresql/data
14+
healthcheck:
15+
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-cpab} -d ${POSTGRES_DB:-cpab}"]
16+
interval: 5s
17+
timeout: 5s
18+
retries: 5
19+
networks:
20+
- cpab-internal
21+
22+
cpab:
23+
image: abwebplode/cpab:development
24+
container_name: cpab
25+
restart: unless-stopped
26+
pull_policy: always
27+
environment:
28+
DB_CONNECTION: "postgres://${POSTGRES_USER:-cpab}:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in the environment or a .env file}@postgres:5432/${POSTGRES_DB:-cpab}?sslmode=disable"
29+
CORS_ORIGINS: ${CORS_ORIGINS:-https://openapi.io.vn}
30+
JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET to a strong random value}
31+
JWT_EXPIRY: ${JWT_EXPIRY:-720h}
32+
depends_on:
33+
postgres:
34+
condition: service_healthy
35+
healthcheck:
36+
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:8318/healthz || exit 1"]
37+
interval: 10s
38+
timeout: 5s
39+
retries: 6
40+
start_period: 20s
41+
expose:
42+
- "8318"
43+
networks:
44+
- cpab-internal
45+
46+
nginx:
47+
image: nginx:1.27-alpine
48+
container_name: cpab-nginx
49+
restart: unless-stopped
50+
depends_on:
51+
cpab:
52+
condition: service_healthy
53+
ports:
54+
- "${HTTP_PORT:-80}:80"
55+
volumes:
56+
- ./nginx/openapi.io.vn.conf:/etc/nginx/conf.d/default.conf:ro
57+
networks:
58+
- cpab-internal
59+
60+
networks:
61+
cpab-internal:
62+
driver: bridge
63+
64+
volumes:
65+
cpab-postgres-data:

devops/nginx/openapi.io.vn.conf

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# HTTP-only origin behind the upstream nginx. Only /v1 is proxied to CPAB.
2+
3+
upstream cpab_backend {
4+
server cpab:8318;
5+
keepalive 32;
6+
}
7+
8+
server {
9+
listen 80;
10+
listen [::]:80;
11+
server_name openapi.io.vn;
12+
13+
client_max_body_size 100m;
14+
15+
location = /v1 {
16+
proxy_pass http://cpab_backend;
17+
proxy_http_version 1.1;
18+
proxy_buffering off;
19+
proxy_read_timeout 600s;
20+
proxy_send_timeout 600s;
21+
proxy_set_header Connection "";
22+
proxy_set_header Host $host;
23+
proxy_set_header X-Real-IP $remote_addr;
24+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
25+
proxy_set_header X-Forwarded-Proto $scheme;
26+
}
27+
28+
location ^~ /v1/ {
29+
proxy_pass http://cpab_backend;
30+
proxy_http_version 1.1;
31+
proxy_buffering off;
32+
proxy_read_timeout 600s;
33+
proxy_send_timeout 600s;
34+
proxy_set_header Connection "";
35+
proxy_set_header Host $host;
36+
proxy_set_header X-Real-IP $remote_addr;
37+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
38+
proxy_set_header X-Forwarded-Proto $scheme;
39+
}
40+
41+
location / {
42+
return 404;
43+
}
44+
}

0 commit comments

Comments
 (0)